From b1e8e307d2912a67184626bbbf3b261b3cc08fc7 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Mon, 15 Jun 2026 10:57:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(sdk):=20=E6=8C=89=E8=B7=A8=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E8=A7=84=E8=8C=83=E9=87=8D=E6=9E=84=E6=89=80=E6=9C=89?= =?UTF-8?q?=20SDK=20=E5=8C=85=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - common: setUserInfo 成为认证枢纽,分发订阅者;移除 init()/initializeFromLicense(); initWithConfigFile 解密后调用 initialize() 拉取远程服务配置;XuqmConfig 增加服务开通标志 - push: 移除独立 initialize/registerToken/unregisterToken 等方法; 改由 _registerUserInfoHandler 订阅,setUserInfo 时自动完成厂商检测+设备注册+token上报 - im: 新增 refreshToken();注册 setUserInfo 订阅,自动登录/断连;移除对 PushSDK 的直接调用 - update: 支持 registerPlugins(批量) + registerPlugin(向后兼容); 版本号自动从 AsyncStorage 读取,移除 version 字段;新增 updatePlugin(一步完成)、 downloadAndInstallApk、setBundleCallbacks 注入宿主写入/重载能力 - license: 移除独立 initialize/initializeFromFile;依赖 getConfig() 获取 appKey/licenseUrl - 顶层 src/sdk.ts: 移除旧 login/logout 包装层,直接重导出 CommonXuqmSDK Co-Authored-By: Claude Sonnet 4.6 --- packages/common/src/config.ts | 67 ++++-- packages/common/src/index.ts | 10 +- packages/common/src/sdk.ts | 92 +++----- packages/im/src/ImSDK.ts | 42 ++-- packages/license/src/index.ts | 5 +- packages/license/src/license.ts | 75 ++----- packages/push/src/PushSDK.ts | 210 +++++++++--------- packages/push/src/index.ts | 2 +- packages/update/src/UpdateSDK.ts | 359 ++++++++++++++++--------------- packages/update/src/index.ts | 3 +- src/index.ts | 7 +- src/sdk.ts | 66 +----- 12 files changed, 445 insertions(+), 493 deletions(-) diff --git a/packages/common/src/config.ts b/packages/common/src/config.ts index 3229603..dfe411b 100644 --- a/packages/common/src/config.ts +++ b/packages/common/src/config.ts @@ -8,32 +8,59 @@ export interface XuqmConfig { appKey: string apiUrl: string imWsUrl: string - fileServiceUrl: string // fetched from remote config + fileServiceUrl: string + licenseUrl: string debug: boolean + // 服务开通状态(由平台远程配置决定) + imEnabled: boolean + pushEnabled: boolean + licenseEnabled: boolean } -let _config: XuqmConfig | null = null -let _userId: string | null = null - export interface XuqmUserInfo { - userId?: string + userId: string + userSig?: string // IM 登录凭证;未开通 IM 时可不传 name?: string email?: string phone?: string + avatar?: string } +// ─── Internal state ──────────────────────────────────────────────────────────── + +let _config: XuqmConfig | null = null +let _userId: string | null = null let _userInfo: XuqmUserInfo | null = null +const _userInfoHandlers: Array<(info: XuqmUserInfo | null) => void> = [] + +// ─── Config ──────────────────────────────────────────────────────────────────── + +export interface XuqmRemoteConfig { + imApiUrl?: string + apiUrl?: string + imWsUrl?: string + fileServiceUrl?: string + licenseUrl?: string + imEnabled?: boolean + pushEnabled?: boolean + licenseEnabled?: boolean +} export function initConfigFromRemote( options: XuqmInitOptions, - remote: { imWsUrl: string; fileServiceUrl: string; apiUrl: string }, + remote: XuqmRemoteConfig, ): void { + const apiUrl = remote.imApiUrl ?? remote.apiUrl ?? '' _config = { - appKey: options.appKey, - apiUrl: remote.apiUrl, - imWsUrl: remote.imWsUrl, - fileServiceUrl: remote.fileServiceUrl, - debug: options.debug ?? false, + appKey: options.appKey, + apiUrl, + imWsUrl: remote.imWsUrl ?? '', + fileServiceUrl: remote.fileServiceUrl ?? apiUrl, + licenseUrl: remote.licenseUrl ?? apiUrl, + debug: options.debug ?? false, + imEnabled: remote.imEnabled ?? !!remote.imWsUrl, + pushEnabled: remote.pushEnabled ?? true, + licenseEnabled: remote.licenseEnabled ?? false, } } @@ -42,6 +69,12 @@ export function getConfig(): XuqmConfig { return _config } +export function isInitialized(): boolean { + return _config !== null +} + +// ─── UserId ──────────────────────────────────────────────────────────────────── + export function setUserId(userId: string | null): void { _userId = userId } @@ -50,15 +83,17 @@ export function getUserId(): string | null { return _userId } -export function isInitialized(): boolean { - return _config !== null +// ─── UserInfo + subscribers ──────────────────────────────────────────────────── + +export function _registerUserInfoHandler(handler: (info: XuqmUserInfo | null) => void): void { + _userInfoHandlers.push(handler) } export function setUserInfo(info: XuqmUserInfo | null): void { _userInfo = info - // Sync userId for backward compatibility - if (info?.userId) { - _userId = info.userId + _userId = info?.userId ?? null + for (const handler of _userInfoHandlers) { + try { handler(info) } catch { /* sub-SDK errors must not break the chain */ } } } diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 6e44901..da6dd12 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -3,7 +3,15 @@ import './autoInit' export { XuqmSDK } from './sdk' export type { XuqmInitOptions, XuqmConfig, XuqmUserInfo } from './config' -export { getConfig, isInitialized, setUserId, getUserId } from './config' +export { + getConfig, + isInitialized, + setUserId, + getUserId, + setUserInfo, + getUserInfo, + _registerUserInfoHandler, +} from './config' export { awaitInitialization } from './sdk' export { apiRequest, configureHttp, _getToken, _saveToken, _clearToken } from './http' export { DEFAULT_TENANT_PLATFORM_URL, DEFAULT_IM_WS_URL } from './constants' diff --git a/packages/common/src/sdk.ts b/packages/common/src/sdk.ts index 04dfae8..a781c91 100644 --- a/packages/common/src/sdk.ts +++ b/packages/common/src/sdk.ts @@ -1,5 +1,5 @@ -import { initConfigFromRemote, isInitialized, type XuqmInitOptions, type XuqmUserInfo, setUserId as setCommonUserId, getUserId as getCommonUserId, setUserInfo as setCommonUserInfo, getUserInfo as getCommonUserInfo } from './config' -import { DEFAULT_IM_WS_URL, DEFAULT_TENANT_PLATFORM_URL } from './constants' +import { initConfigFromRemote, isInitialized, type XuqmInitOptions, type XuqmUserInfo, setUserInfo as setCommonUserInfo, getUserInfo as getCommonUserInfo, getUserId as getCommonUserId } from './config' +import { DEFAULT_TENANT_PLATFORM_URL } from './constants' import { configureHttp } from './http' import { decryptConfigFile } from './configCrypto' @@ -36,8 +36,14 @@ function markInitializationFailed(e: unknown): void { export const XuqmSDK = { /** - * @param options.appKey - Your application key (from the tenant platform) - * @param options.debug - Enable verbose logging + * 方式 B:手动初始化。 + * + * 请求平台获取该 appKey 的完整服务配置(IM、Push、文件服务等 URL 及开通状态)。 + * 失败时直接抛出,不降级。 + * + * @param options.appKey 应用标识 + * @param options.platformUrl 平台地址;不传则使用内置默认公有平台地址 + * @param options.debug 是否开启调试日志 */ async initialize(options: XuqmInitOptions): Promise { if (isInitialized()) return @@ -46,15 +52,20 @@ export const XuqmSDK = { const configUrl = `${baseUrl}/api/sdk/config?appKey=${options.appKey}` try { const res = await fetch(configUrl) - const json = await res.json() - const remote = json.data ?? json + if (!res.ok) throw new Error(`[XuqmSDK] Platform config request failed: ${res.status}`) + const json = await res.json() as Record + const remote = (json.data ?? json) as Record initConfigFromRemote(options, { - imWsUrl: remote.imWsUrl, - fileServiceUrl: remote.fileServiceUrl, - apiUrl: remote.imApiUrl ?? DEFAULT_TENANT_PLATFORM_URL, + apiUrl: remote.apiUrl as string | undefined, + imWsUrl: remote.imWsUrl as string | undefined, + fileServiceUrl: remote.fileServiceUrl as string | undefined, + licenseUrl: remote.licenseUrl as string | undefined, + imEnabled: remote.imEnabled as boolean | undefined, + pushEnabled: remote.pushEnabled as boolean | undefined, + licenseEnabled: remote.licenseEnabled as boolean | undefined, }) configureHttp({ - baseUrl: remote.imApiUrl ?? DEFAULT_TENANT_PLATFORM_URL, + baseUrl: (remote.apiUrl as string | undefined) ?? baseUrl, debug: options.debug, }) markInitialized() @@ -65,77 +76,36 @@ export const XuqmSDK = { }, /** - * @param options.appKey - Your application key (from the tenant platform) - * @param options.debug - Enable verbose logging - */ - init(options: XuqmInitOptions): void { - if (isInitialized()) return - initConfigFromRemote(options, { - imWsUrl: DEFAULT_IM_WS_URL, - fileServiceUrl: DEFAULT_TENANT_PLATFORM_URL, - apiUrl: DEFAULT_TENANT_PLATFORM_URL, - }) - configureHttp({ - baseUrl: DEFAULT_TENANT_PLATFORM_URL, - debug: options.debug, - }) - markInitialized() - }, - - /** - * Initialize from a decrypted license file object. - */ - initializeFromLicense(file: { appKey: string; baseUrl?: string; serverUrl?: string }, options?: { debug?: boolean }): void { - if (isInitialized()) return - const serverUrl = file.serverUrl || file.baseUrl || DEFAULT_TENANT_PLATFORM_URL - initConfigFromRemote({ appKey: file.appKey, debug: options?.debug }, { - imWsUrl: DEFAULT_IM_WS_URL, - fileServiceUrl: serverUrl, - apiUrl: serverUrl, - }) - configureHttp({ baseUrl: serverUrl, debug: options?.debug }) - markInitialized() - }, - - /** - * 从加密配置文件初始化 SDK。 + * 方式 A(内部):从加密配置文件初始化 SDK。 * - * 宿主只需传入 .xuqmconfig 文件的加密内容,SDK 自动解密并初始化。 - * 支持 XUQM-CONFIG-V1 和 XUQM-LICENSE-V1 两种格式。 + * 宿主把 .xuqmconfig 文件内容通过 autoInit 模块传入,SDK 自动解密并调用 initialize()。 + * 不对外暴露(不在 index.ts 中 export)。 * - * @param encryptedContent 加密配置文件的完整内容 - * @param options.debug 是否开启调试日志 - * - * @example - * // common bundle 入口 - * import config from './assets/app.xuqmconfig' - * await XuqmSDK.initWithConfigFile(config) + * @param encryptedContent 加密配置文件的完整内容(XUQM-CONFIG-V1 或 XUQM-LICENSE-V1 格式) */ async initWithConfigFile(encryptedContent: string, options?: { debug?: boolean }): Promise { if (isInitialized()) return const file = await decryptConfigFile(encryptedContent) - this.initializeFromLicense(file, options) + // 配置文件解密后包含 appKey 和 platformUrl(字段名 serverUrl 或 baseUrl) + const platformUrl = file.serverUrl ?? file.baseUrl ?? undefined + await this.initialize({ appKey: file.appKey, platformUrl, debug: options?.debug }) }, /** - * Wait for initialization to complete. + * 等待初始化完成。在任意子 SDK 内部调用以确保 XuqmSDK 已就绪。 */ async awaitInitialization(): Promise { if (isInitialized()) return await ensureInitPromise() }, - setUserId(userId: string | null): void { - setCommonUserId(userId) - }, - getUserId(): string | null { return getCommonUserId() }, /** - * Set user info for gray release targeting and license verification. - * Call this after user login. + * 用户认证核心枢纽。登录成功后调用一次,所有子 SDK(Push、IM、License 等)自动同步状态。 + * 登出时传 null,触发所有子 SDK 登出。 */ setUserInfo(info: XuqmUserInfo | null): void { setCommonUserInfo(info) diff --git a/packages/im/src/ImSDK.ts b/packages/im/src/ImSDK.ts index d5e5c5a..a761e93 100644 --- a/packages/im/src/ImSDK.ts +++ b/packages/im/src/ImSDK.ts @@ -1,4 +1,5 @@ -import { apiRequest, _getToken, _saveToken, getConfig, getDeviceInfo, setUserId as setCommonUserId, getUserId as getCommonUserId } from '@xuqm/rn-common' +import { apiRequest, _getToken, _saveToken, getConfig, getDeviceInfo, setUserId as setCommonUserId, getUserId as getCommonUserId, _registerUserInfoHandler } from '@xuqm/rn-common' +import type { XuqmUserInfo } from '@xuqm/rn-common' import { ImClient } from './ImClient' import { ImDatabase } from './db/ImDatabase' import type { MessageSearchParams } from './db/ImDatabase' @@ -317,17 +318,6 @@ export const ImSDK = { client.addListener({ onConnected: () => { _syncHistoryForAllConversations().catch(() => {}) - // Auto-register push token if Push module is installed and a token is pending - import('@xuqm/rn-push') - .then(({ PushSDK }) => { - const pending = PushSDK.getPendingToken?.() - if (pending) { - PushSDK.registerToken(userId, pending.token, pending.vendor).catch(() => {}) - } - }) - .catch(() => { - // Push module not installed — ignore gracefully - }) }, }) void client.connect() @@ -1504,4 +1494,32 @@ export const ImSDK = { _currentUserSig = null setCommonUserId(null) }, + + /** + * 刷新 IM userSig(过期时调用)。 + */ + async refreshToken(userSig: string): Promise { + if (!_currentUserId) throw new Error('[ImSDK] Not logged in — call setUserInfo first.') + await ImSDK.login(_currentUserId, userSig) + }, } + +// ─── 订阅 XuqmSDK.setUserInfo ────────────────────────────────────────────────── + +_registerUserInfoHandler(async (info: XuqmUserInfo | null) => { + if (!info) { + ImSDK.disconnect() + return + } + // 仅当 IM 服务已开通且 userSig 存在时自动登录 + if (!info.userSig) return + try { + const { getConfig, isInitialized } = await import('@xuqm/rn-common') + if (!isInitialized()) return + const config = getConfig() + if (!config.imEnabled) return + } catch { + return + } + await ImSDK.login(info.userId, info.userSig).catch(() => {}) +}) diff --git a/packages/license/src/index.ts b/packages/license/src/index.ts index 9f0a744..3863606 100644 --- a/packages/license/src/index.ts +++ b/packages/license/src/index.ts @@ -1,3 +1,2 @@ -export { initialize, initializeFromFile, checkLicense, getStatus, getDeviceId, clear } from './license' -export { decryptLicenseFile, decryptConfigFile } from './crypto' -export type { LicenseFile, LicenseUserInfo, LicenseStatus, LicenseResult } from './models' +export { checkLicense, getStatus, getDeviceId, clear } from './license' +export type { LicenseUserInfo, LicenseStatus, LicenseResult } from './models' diff --git a/packages/license/src/license.ts b/packages/license/src/license.ts index 7c23ec4..69dffab 100644 --- a/packages/license/src/license.ts +++ b/packages/license/src/license.ts @@ -1,56 +1,27 @@ import { Platform } from 'react-native' -import { getDeviceId as getCommonDeviceId, getDeviceInfo, awaitInitialization } from '@xuqm/rn-common' -import { decryptLicenseFile } from './crypto' +import { getDeviceId as getCommonDeviceId, getDeviceInfo, awaitInitialization, getConfig, getUserId } from '@xuqm/rn-common' import * as store from './store' -import type { LicenseFile, LicenseResult, LicenseStatus, LicenseUserInfo, RegisterRequest, RegisterResponse, VerifyRequest, VerifyResponse } from './models' +import type { LicenseResult, LicenseStatus, LicenseUserInfo, RegisterRequest, RegisterResponse, VerifyRequest, VerifyResponse } from './models' -const DEFAULT_BASE_URL = 'https://auth.dev.xuqinmin.com' const CACHE_WINDOW_MS = 10 * 60 * 1000 const STATUS_OK = 'ok' const STATUS_DENIED = 'denied' -interface LicenseConfig { - appKey: string - baseUrl: string - deviceName?: string -} - -let _config: LicenseConfig | null = null -// In-memory cache to avoid repeated AsyncStorage reads within a session let _cachedStatus: string | null = null let _cachedStatusTime = 0 -function normalize(url: string): string { - const s = url.trim() - return s.endsWith('/') ? s : s + '/' -} - -export function initialize(appKey: string, options: { baseUrl?: string; deviceName?: string } = {}): void { - _config = { - appKey, - baseUrl: normalize(options.baseUrl ?? DEFAULT_BASE_URL), - deviceName: options.deviceName, - } -} - -// Auto-initialize from license file asset. The asset is embedded as a string (e.g., via react-native-raw-text or require()). -export async function initializeFromFile(encryptedContent: string): Promise { - const file = await decryptLicenseFile(encryptedContent) - initialize(file.appKey, { baseUrl: file.baseUrl }) -} - export async function checkLicense(userInfo?: LicenseUserInfo): Promise { - if (!_config) { - await awaitInitialization() - if (!_config) return { type: 'error', message: 'LicenseSDK not initialized' } - } - const { appKey, baseUrl } = _config + // 等待 XuqmSDK 初始化完成,使用公共配置 + await awaitInitialization() + const config = getConfig() + const { licenseUrl, appKey } = config + const baseUrl = licenseUrl.endsWith('/') ? licenseUrl : licenseUrl + '/' - // In-memory cache check + // 内存缓存 if (_cachedStatus === STATUS_OK && Date.now() - _cachedStatusTime < CACHE_WINDOW_MS) { return { type: 'success', reason: 'Cached' } } - // Persistent cache check + // 持久化缓存 const persistedStatus = await store.getStatus() const persistedTime = await store.getStatusTime() if (persistedStatus === STATUS_OK && Date.now() - persistedTime < CACHE_WINDOW_MS) { @@ -59,37 +30,37 @@ export async function checkLicense(userInfo?: LicenseUserInfo): Promise(`${baseUrl}api/license/verify`, verifyReq) + const verifyResp = await _post(`${baseUrl}api/license/verify`, verifyReq) if (verifyResp.valid) { - await persistStatus(STATUS_OK) + await _persistStatus(STATUS_OK) return { type: 'success', reason: 'Verified' } } await store.setToken(null) } + const userId = getUserId() const regReq: RegisterRequest = { appKey, deviceId, - deviceName: _config.deviceName, - deviceModel: deviceInfo.model, + deviceModel: deviceInfo.model, deviceVendor: Platform.OS === 'ios' ? 'Apple' : deviceInfo.brand, - osVersion: `${Platform.OS === 'ios' ? 'iOS' : 'Android'} ${deviceInfo.osVersion}`, - userInfo, + osVersion: `${Platform.OS === 'ios' ? 'iOS' : 'Android'} ${deviceInfo.osVersion}`, + userInfo: userInfo ?? (userId ? { userId } : undefined), } - const regResp = await post(`${baseUrl}api/license/register`, regReq) + const regResp = await _post(`${baseUrl}api/license/register`, regReq) if (regResp.success && regResp.token) { await store.setToken(regResp.token) - await persistStatus(STATUS_OK) + await _persistStatus(STATUS_OK) return { type: 'success', reason: 'Registered' } } - await persistStatus(STATUS_DENIED) + await _persistStatus(STATUS_DENIED) return { type: 'error', message: regResp.message ?? 'Registration denied' } } catch (e) { if (persistedStatus === STATUS_OK) return { type: 'success', reason: 'Offline - cached ok' } @@ -105,7 +76,7 @@ export async function getStatus(): Promise { } export async function getDeviceId(): Promise { - return getOrCreateDeviceId() + return _getOrCreateDeviceId() } export async function clear(): Promise { @@ -114,7 +85,7 @@ export async function clear(): Promise { await store.clearAll() } -async function getOrCreateDeviceId(): Promise { +async function _getOrCreateDeviceId(): Promise { const existing = await store.getDeviceId() if (existing) return existing const id = await getCommonDeviceId() @@ -122,14 +93,14 @@ async function getOrCreateDeviceId(): Promise { return id } -async function persistStatus(status: string): Promise { +async function _persistStatus(status: string): Promise { const now = Date.now() _cachedStatus = status _cachedStatusTime = now await Promise.all([store.setStatus(status), store.setStatusTime(now)]) } -async function post(url: string, body: unknown): Promise { +async function _post(url: string, body: unknown): Promise { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/packages/push/src/PushSDK.ts b/packages/push/src/PushSDK.ts index f137bfe..bf036ed 100644 --- a/packages/push/src/PushSDK.ts +++ b/packages/push/src/PushSDK.ts @@ -1,147 +1,139 @@ -import { apiRequest, getConfig, getDeviceInfo, getUserId as getCommonUserId } from '@xuqm/rn-common' -import type { PushVendor } from '@xuqm/rn-common' +import { + apiRequest, + getConfig, + getDeviceInfo, + getUserId as getCommonUserId, + _registerUserInfoHandler, +} from '@xuqm/rn-common' +import type { PushVendor, XuqmUserInfo } from '@xuqm/rn-common' import { detectVendorNative, registerPushNative, addPushTokenListener } from './NativePush' export type { PushVendor } -type PendingDeviceToken = { - token: string - vendor?: PushVendor -} +// ─── Internal state ──────────────────────────────────────────────────────────── -let currentUserId: string | null = null -let currentInitializedUserId: string | null = null -let pendingToken: PendingDeviceToken | null = null +let _registeredUserId: string | null = null let _tokenUnsubscribe: (() => void) | null = null -async function registerPendingToken(): Promise { - const userId = currentUserId ?? getCommonUserId() - if (!userId || !pendingToken) return - const device = await getDeviceInfo() +// ─── Internal helpers ────────────────────────────────────────────────────────── + +async function _registerDevice(userId: string, token: string, vendor: PushVendor): Promise { const config = getConfig() + const device = await getDeviceInfo() await apiRequest('/api/push/register', { method: 'POST', params: { - appKey: config.appKey, + appKey: config.appKey, userId, - vendor: pendingToken.vendor ?? device.pushVendor, - token: pendingToken.token, - platform: device.platform, - deviceId: device.deviceId, - brand: device.brand, - model: device.model, + vendor, + token, + platform: device.platform, + deviceId: device.deviceId, + brand: device.brand, + model: device.model, osVersion: device.osVersion, }, }) + _registeredUserId = userId } -export const PushSDK = { - async initialize(userId?: string): Promise { - const nextUserId = userId ?? getCommonUserId() - if (currentInitializedUserId === nextUserId) { - currentUserId = nextUserId - return +async function _unregisterDevice(userId: string): Promise { + const config = getConfig() + const { deviceId } = await getDeviceInfo() + await apiRequest('/api/push/unregister', { + method: 'DELETE', + params: { appKey: config.appKey, userId, deviceId }, + }) + if (_registeredUserId === userId) _registeredUserId = null +} + +async function _startRegistrationFlow(userId: string): Promise { + // 1. 检测厂商并请求原生注册 + await registerPushNative() + + // 2. 监听 token 回调,收到后自动上报 + if (_tokenUnsubscribe) _tokenUnsubscribe() + _tokenUnsubscribe = addPushTokenListener(async (event) => { + const currentUserId = getCommonUserId() + if (!currentUserId) return + const vendor = (await detectVendorNative()) as PushVendor ?? (event.vendor as PushVendor) + await _registerDevice(currentUserId, event.token, vendor).catch(() => {}) + }) +} + +// ─── 订阅 XuqmSDK.setUserInfo ────────────────────────────────────────────────── + +_registerUserInfoHandler(async (info: XuqmUserInfo | null) => { + if (!info) { + // 登出:解绑 token + const userId = _registeredUserId + if (userId) { + await _unregisterDevice(userId).catch(() => {}) } - currentInitializedUserId = nextUserId - currentUserId = nextUserId - await registerPendingToken() - }, - - /** - * Cache a device token without immediately registering it. - * Call this when the native layer receives a token before the user is logged in. - */ - setPendingToken(token: string, vendor?: PushVendor): void { - pendingToken = { token, vendor } - }, - - /** - * Get the currently cached pending token, if any. - */ - getPendingToken(): PendingDeviceToken | null { - return pendingToken - }, - - async setDeviceToken(token: string, vendor?: PushVendor): Promise { - pendingToken = { token, vendor } - await registerPendingToken() - }, - - /** - * Auto-detect vendor and request native push registration. - * On Android this attempts to register with the vendor SDK (Huawei, Xiaomi, OPPO, vivo, Honor, FCM). - * On iOS this requests APNs registration. - * Listen for token updates via onPushToken(callback). - */ - async requestNativeRegistration(): Promise { - await registerPushNative() - }, - - /** - * Listen for push tokens from the native layer. - * Call setDeviceToken(token, vendor) inside the callback to register with the server. - */ - onPushToken(callback: (token: string, vendor: string) => void): () => void { if (_tokenUnsubscribe) { _tokenUnsubscribe() _tokenUnsubscribe = null } - _tokenUnsubscribe = addPushTokenListener((event) => { - callback(event.token, event.vendor) + return + } + + // 登录:启动设备注册流程(幂等,同一用户不重复注册) + if (_registeredUserId === info.userId) return + await _startRegistrationFlow(info.userId).catch(() => {}) +}) + +// ─── PushSDK 公开 API ────────────────────────────────────────────────────────── + +export const PushSDK = { + /** + * 设置离线推送开关。 + */ + async setOfflinePushEnabled(enabled: boolean): Promise { + const config = getConfig() + const userId = getCommonUserId() + if (!userId) return + await apiRequest('/api/push/settings/offline', { + method: 'PUT', + params: { appKey: config.appKey, userId, enabled: String(enabled) }, }) - return () => { - if (_tokenUnsubscribe) { - _tokenUnsubscribe() - _tokenUnsubscribe = null - } - } }, /** - * Register a push device token for the given user. - * If vendor is omitted, it is auto-detected from the device brand. - * - * @param userId - The logged-in user's ID - * @param token - The device push token from the vendor SDK - * @param vendor - Optional; auto-detected when not provided + * 设置免打扰时间段(24 小时制,如 '22:00'、'08:00')。 */ - async registerToken(userId: string, token: string, vendor?: PushVendor): Promise { - currentUserId = userId - pendingToken = { token, vendor } + async setQuietHours(start: string, end: string): Promise { const config = getConfig() - const device = await getDeviceInfo() - await apiRequest('/api/push/register', { - method: 'POST', - params: { - appKey: config.appKey, - userId, - vendor: vendor ?? device.pushVendor, - token, - platform: device.platform, - deviceId: device.deviceId, - brand: device.brand, - model: device.model, - osVersion: device.osVersion, - }, + const userId = getCommonUserId() + if (!userId) return + await apiRequest('/api/push/settings/quiet-hours', { + method: 'PUT', + params: { appKey: config.appKey, userId, start, end }, }) - pendingToken = null }, - async unregisterToken(userId: string): Promise { + /** + * 清除免打扰设置。 + */ + async clearQuietHours(): Promise { const config = getConfig() - const { deviceId } = await getDeviceInfo() - await apiRequest('/api/push/unregister', { + const userId = getCommonUserId() + if (!userId) return + await apiRequest('/api/push/settings/quiet-hours', { method: 'DELETE', - params: { appKey: config.appKey, userId, deviceId }, + params: { appKey: config.appKey, userId }, }) - if (currentUserId === userId) currentUserId = null - if (currentInitializedUserId === userId) currentInitializedUserId = null - if (pendingToken && currentUserId === null) pendingToken = null }, - async logout(userId?: string): Promise { - const targetUserId = userId ?? currentUserId ?? getCommonUserId() - if (!targetUserId) return - await PushSDK.unregisterToken(targetUserId) + /** + * 登出推送(通常无需手动调用,XuqmSDK.setUserInfo(null) 会自动触发)。 + */ + async logout(): Promise { + const userId = _registeredUserId ?? getCommonUserId() + if (!userId) return + await _unregisterDevice(userId).catch(() => {}) + if (_tokenUnsubscribe) { + _tokenUnsubscribe() + _tokenUnsubscribe = null + } }, } diff --git a/packages/push/src/index.ts b/packages/push/src/index.ts index 025237d..b913dcd 100644 --- a/packages/push/src/index.ts +++ b/packages/push/src/index.ts @@ -1,3 +1,3 @@ export { PushSDK } from './PushSDK' export type { PushVendor } from './PushSDK' -export { isNativePushAvailable, detectVendorNative, registerPushNative, addPushTokenListener } from './NativePush' +export { isNativePushAvailable } from './NativePush' diff --git a/packages/update/src/UpdateSDK.ts b/packages/update/src/UpdateSDK.ts index d7482df..c469181 100644 --- a/packages/update/src/UpdateSDK.ts +++ b/packages/update/src/UpdateSDK.ts @@ -1,75 +1,48 @@ import AsyncStorage from '@react-native-async-storage/async-storage' import { Linking, Platform } from 'react-native' -import { apiRequest, getConfig, getUserId } from '@xuqm/rn-common' +import { apiRequest, getConfig, getUserId, _registerUserInfoHandler } from '@xuqm/rn-common' import { getAppVersionCode, getAppVersionName, _devSetAppVersion } from './NativeVersion' -import { awaitInitialization } from '@xuqm/rn-common' -// ─── Types ──────────────────────────────────────────────────────────────────── +// ─── Types (public) ──────────────────────────────────────────────────────────── -/** 插件注册元数据 */ -export interface PluginMeta { - /** 插件唯一标识,如 'buz1'、'buz2' */ +/** 插件注册(版本号由 SDK 自动获取,不需要 App 传入) */ +export interface PluginRegistration { moduleId: string - /** 当前 bundle 版本号 */ - version: string } -/** - * App 整包更新信息。 - * - * 对齐 Android SDK 的 UpdateInfo 模型。 - * SDK 只返回数据,UI 由 app 层自行处理。 - */ +/** @deprecated 使用 PluginRegistration(移除 version 字段) */ +export interface PluginMeta extends PluginRegistration { + version?: string +} + export interface AppUpdateInfo { - /** 是否需要更新 */ needsUpdate: boolean - /** 最新版本名,如 '2.1.0' */ versionName?: string - /** 最新版本号(整数) */ versionCode?: number - /** APK/安装包直接下载地址(Android 有此字段时优先下载) */ downloadUrl?: string - /** 更新日志 */ changeLog?: string - /** 是否强制更新(true 时不允许跳过) */ forceUpdate?: boolean - /** iOS App Store 地址 */ appStoreUrl?: string - /** Android 应用商店地址(华为/小米/OPPO 等) */ marketUrl?: string - /** 服务端要求登录后才能检查(true 时 needsUpdate 通常为 false) */ requiresLogin?: boolean - /** SDK 内部标记:APK 是否已下载到本地(仅 Android) */ alreadyDownloaded?: boolean - /** APK 文件 SHA-256 校验值 */ apkHash?: string | null } -/** - * 插件(RN Bundle)更新信息。 - * - * 插件唯一标识 = appKey + platform + moduleId。 - * - appKey:应用标识(来自配置文件) - * - platform:ANDROID / IOS - * - moduleId:插件 ID(如 'buz1') - */ export interface PluginUpdateInfo { - /** 是否需要更新 */ needsUpdate: boolean - /** 最新版本号 */ latestVersion: string - /** bundle 下载地址 */ + currentVersion: string downloadUrl: string - /** bundle 文件 MD5 */ md5: string - /** 要求的最低 common bundle 版本 */ minCommonVersion: string - /** 更新说明 */ note: string + forceUpdate?: boolean + changeLog?: string } -/** 已缓存的 bundle 元数据 */ -export interface CachedRnBundle { +export type { CachedRnBundle } +interface CachedRnBundle { moduleId: string version: string md5: string @@ -77,14 +50,24 @@ export interface CachedRnBundle { source: string } -// ─── Internal ───────────────────────────────────────────────────────────────── +// ─── Internal state ──────────────────────────────────────────────────────────── -const _pluginRegistry = new Map() +const _pluginRegistry = new Set() + +let _writeBundleCallback: ((moduleId: string, source: string) => Promise) | null = null +let _reloadBundleCallback: ((moduleId: string) => Promise) | null = null function bundleCacheKey(moduleId: string) { return `@xuqm:bundle:${moduleId}` } +async function _getCachedVersion(moduleId: string): Promise { + const raw = await AsyncStorage.getItem(bundleCacheKey(moduleId)).catch(() => null) + if (!raw) return '0.0.0' + const cached = JSON.parse(raw) as CachedRnBundle + return cached.version ?? '0.0.0' +} + function normalizeDownloadUrl(rawUrl?: string): string | undefined { if (!rawUrl) return rawUrl if (rawUrl.includes('/api/v1/updates/api/v1/rn/files/')) { @@ -101,66 +84,117 @@ function normalizeDownloadUrl(rawUrl?: string): string | undefined { return rawUrl } +async function _downloadText(url: string, onProgress?: (p: number) => void): Promise { + const response = await fetch(url) + if (!response.ok) throw new Error(`[UpdateSDK] Download failed: ${response.status}`) + if (!onProgress) return response.text() + const contentLength = Number(response.headers.get('Content-Length') ?? '0') + const reader = response.body?.getReader() + if (!reader) return response.text() + const chunks: Uint8Array[] = [] + let received = 0 + for (;;) { + const { done, value } = await reader.read() + if (done) break + chunks.push(value) + received += value.length + if (contentLength > 0) onProgress(received / contentLength) + } + const decoder = new TextDecoder() + return chunks.map(c => decoder.decode(c, { stream: true })).join('') + decoder.decode() +} + +async function _downloadBinary(url: string, onProgress?: (p: number) => void): Promise { + const response = await fetch(url) + if (!response.ok) throw new Error(`[UpdateSDK] Download failed: ${response.status}`) + if (!onProgress) return response.arrayBuffer() + const contentLength = Number(response.headers.get('Content-Length') ?? '0') + const reader = response.body?.getReader() + if (!reader) return response.arrayBuffer() + const chunks: Uint8Array[] = [] + let received = 0 + for (;;) { + const { done, value } = await reader.read() + if (done) break + chunks.push(value) + received += value.length + if (contentLength > 0) onProgress(received / contentLength) + } + const totalLength = chunks.reduce((acc, c) => acc + c.length, 0) + const combined = new Uint8Array(totalLength) + let offset = 0 + for (const chunk of chunks) { + combined.set(chunk, offset) + offset += chunk.length + } + return combined.buffer +} + +// ─── 订阅 setUserInfo(userId 用于定向更新) ────────────────────────────────── + +_registerUserInfoHandler(() => { + // updatePlugin 检查更新时会实时调用 getUserId(),此处无需额外操作 +}) + // ─── UpdateSDK ──────────────────────────────────────────────────────────────── export const UpdateSDK = { + // ── 宿主注入(插件化写入/重载能力)────────────────────────────────────────── + + /** + * 注入 bundle 写入和重载回调。 + * 宿主在初始化时调用(如 src/core/updater.ts),注入 BundleRuntime 能力。 + * + * @example + * UpdateSDK.setBundleCallbacks({ + * writeBundle: writeBundleFile, + * reloadBundle: loadBundle, + * }) + */ + setBundleCallbacks(callbacks: { + writeBundle: (moduleId: string, source: string) => Promise + reloadBundle: (moduleId: string) => Promise + }): void { + _writeBundleCallback = callbacks.writeBundle + _reloadBundleCallback = callbacks.reloadBundle + }, + // ── 插件注册 ────────────────────────────────────────────────────────────── /** - * 注册插件元数据。在插件 bundle 入口文件顶部调用。 - * - * 插件唯一标识 = moduleId(如 'buz1')。 - * 完整定位 = appKey(配置文件)+ platform(自动检测)+ moduleId。 + * 批量注册插件。版本号由 SDK 自动从本地缓存获取,不需要 App 传入。 * * @example - * // src/plugins/buz1/bundle.ts - * UpdateSDK.registerPlugin({ moduleId: 'buz1', version: '1.0.0' }) + * UpdateSDK.registerPlugins([{ moduleId: 'buz1' }, { moduleId: 'buz2' }]) */ - registerPlugin(meta: PluginMeta): void { - _pluginRegistry.set(meta.moduleId, meta) + registerPlugins(plugins: PluginRegistration[]): void { + for (const p of plugins) { + _pluginRegistry.add(p.moduleId) + } }, /** - * 获取已注册的插件版本号。 + * 注册单个插件(向后兼容;version 字段已废弃,SDK 自动获取)。 */ - getRegisteredPluginVersion(moduleId: string): string | undefined { - return _pluginRegistry.get(moduleId)?.version + registerPlugin(meta: PluginRegistration): void { + _pluginRegistry.add(meta.moduleId) }, - /** - * 获取所有已注册的插件列表。 - */ - getRegisteredPlugins(): PluginMeta[] { - return Array.from(_pluginRegistry.values()) + getRegisteredPlugins(): string[] { + return Array.from(_pluginRegistry) }, // ── App 整包更新 ────────────────────────────────────────────────────────── - /** - * 检查 App 整包更新。 - * - * SDK 自动检测平台(iOS/Android)并传给服务端。 - * 服务端根据平台返回对应的版本信息和下载/商店地址。 - * - * 对齐 Android SDK UpdateSDK.checkAppUpdate(): - * - 返回完整更新信息,app 层自行决定 UI - * - Android:有 downloadUrl 时下载 APK;无 downloadUrl 时用 marketUrl 跳转商店 - * - iOS:用 appStoreUrl 跳转 App Store - * - * @param bypassIgnore false(默认)= 静默检查,跳过用户已忽略的版本 - * true = 用户主动检查,不跳过 - */ async checkAppUpdate(bypassIgnore?: boolean): Promise { - await awaitInitialization() const config = getConfig() - const currentVersionCode = getAppVersionCode() - const userId = getUserId()?.trim() const params: Record = { - appKey: config.appKey, - platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS', - currentVersionCode: String(currentVersionCode), + appKey: config.appKey, + platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS', + currentVersionCode: String(getAppVersionCode()), } + const userId = getUserId()?.trim() if (userId) params.userId = userId if (bypassIgnore) params.bypassIgnore = 'true' @@ -171,130 +205,117 @@ export const UpdateSDK = { return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) } }, - /** - * 打开应用商店。 - * iOS 使用 appStoreUrl,Android 使用 marketUrl。 - */ async openStore(appStoreUrl?: string, marketUrl?: string): Promise { const url = Platform.OS === 'ios' ? appStoreUrl : marketUrl if (url) await Linking.openURL(url) }, - // ── 插件更新 ────────────────────────────────────────────────────────────── + /** + * Android APK 直接下载并调起系统安装器。 + */ + async downloadAndInstallApk( + downloadUrl: string, + options?: { + onProgress?: (progress: number) => void + sha256?: string + }, + ): Promise { + if (Platform.OS !== 'android') throw new Error('[UpdateSDK] APK install is Android-only.') + // NativeApkInstall 由宿主提供(TurboModule),若未注入则 fallback 到 Linking + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { XuqmApkInstall } = require('./NativeApkInstall') as { XuqmApkInstall: { installApk(url: string, sha256?: string | null): Promise } } + await XuqmApkInstall.installApk(downloadUrl, options?.sha256 ?? null) + } catch { + // Native module not available — fallback to Linking + await Linking.openURL(downloadUrl) + } + }, + + // ── 插件(Bundle)热更新 ────────────────────────────────────────────────── /** * 检查指定插件的更新。 * - * 插件唯一标识 = appKey + platform + moduleId。 - * 插件必须已通过 registerPlugin() 注册。 - * - * @param moduleId 插件 ID(如 'buz1') - * @returns 更新信息,包含 latestVersion / downloadUrl / md5 等 - * @throws 插件未注册时抛出错误 + * 版本号由 SDK 自动从本地 AsyncStorage 缓存读取(首次为 0.0.0)。 + * 插件必须先通过 registerPlugin / registerPlugins 注册。 */ async checkPluginUpdate(moduleId: string): Promise { - await awaitInitialization() - const config = getConfig() - const meta = _pluginRegistry.get(moduleId) - if (!meta) { + if (!_pluginRegistry.has(moduleId)) { throw new Error( `[UpdateSDK] Plugin "${moduleId}" not registered. ` + - 'Call UpdateSDK.registerPlugin({ moduleId, version }) at bundle load time.', + 'Call UpdateSDK.registerPlugins([{ moduleId }]) first.', ) } + const config = getConfig() + const currentVersion = await _getCachedVersion(moduleId) + const userId = getUserId()?.trim() + const params: Record = { + appKey: config.appKey, + moduleId, + platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS', + currentVersion, + } + if (userId) params.userId = userId + const result = await apiRequest('/api/v1/rn/update/check', { skipAuth: true, - params: { - appKey: config.appKey, - moduleId, - platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS', - currentVersion: meta.version, - }, + params, }) - return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) ?? result.downloadUrl } - }, - - /** - * 下载插件 bundle 源码文本。 - * - * @param downloadUrl 下载地址 - * @param onProgress 下载进度回调 (0~1) - */ - async downloadPluginBundle(downloadUrl: string, onProgress?: (progress: number) => void): Promise { - const response = await fetch(downloadUrl) - if (!response.ok) throw new Error(`[UpdateSDK] Bundle download failed: ${response.status}`) - - if (!onProgress) return response.text() - - const contentLength = Number(response.headers.get('Content-Length') ?? '0') - const reader = response.body?.getReader() - if (!reader) return response.text() - - const chunks: Uint8Array[] = [] - let received = 0 - - for (;;) { - const { done, value } = await reader.read() - if (done) break - chunks.push(value) - received += value.length - if (contentLength > 0) { - onProgress(received / contentLength) - } + return { + ...result, + currentVersion, + downloadUrl: normalizeDownloadUrl(result.downloadUrl) ?? result.downloadUrl, } - - const decoder = new TextDecoder() - return chunks.map(chunk => decoder.decode(chunk, { stream: true })).join('') + decoder.decode() }, /** - * 缓存插件 bundle 到本地(AsyncStorage)。 + * 执行插件更新(一步完成)。 + * + * SDK 内部自动:检查版本 → 下载 bundle → 写入文件系统 → 触发重载(silent = false 时)。 + * + * 需先调用 setBundleCallbacks 注入写入/重载能力。 + * + * @param silent true = 静默更新,下次启动生效;false(默认)= 立即重载 */ - async cachePluginBundle(moduleId: string, version: string, md5: string, source: string): Promise { - const payload: CachedRnBundle = { - moduleId, version, md5, source, + async updatePlugin( + moduleId: string, + options?: { + onProgress?: (progress: number) => void + silent?: boolean + }, + ): Promise { + const info = await UpdateSDK.checkPluginUpdate(moduleId) + if (!info.needsUpdate) return + + const source = await _downloadText(info.downloadUrl, options?.onProgress) + + // 写入 AsyncStorage 缓存(版本记录) + const cachePayload: CachedRnBundle = { + moduleId, + version: info.latestVersion, + md5: info.md5, + source, downloadedAt: new Date().toISOString(), } - await AsyncStorage.setItem(bundleCacheKey(moduleId), JSON.stringify(payload)) - return payload - }, + await AsyncStorage.setItem(bundleCacheKey(moduleId), JSON.stringify(cachePayload)) - /** - * 读取已缓存的插件 bundle。 - */ - async getCachedPluginBundle(moduleId: string): Promise { - const raw = await AsyncStorage.getItem(bundleCacheKey(moduleId)) - return raw ? (JSON.parse(raw) as CachedRnBundle) : null - }, + // 写入宿主文件系统(需注入 setBundleCallbacks) + if (_writeBundleCallback) { + await _writeBundleCallback(moduleId, source) + } - /** - * 检查并下载插件更新(一步完成)。 - * - * 如果有更新,下载 bundle 并缓存到本地。 - * 调用方需要在下次启动时通过 reloadPlugin() 加载新版本。 - * - * @param moduleId 插件 ID - * @param onProgress 下载进度回调 (0~1) - * @returns 如果有更新,返回缓存的 bundle 信息;否则返回 null - */ - async checkAndCachePlugin(moduleId: string, onProgress?: (progress: number) => void): Promise { - const info = await this.checkPluginUpdate(moduleId) - if (!info.needsUpdate) return null - - const source = await this.downloadPluginBundle(info.downloadUrl, onProgress) - return this.cachePluginBundle(moduleId, info.latestVersion, info.md5, source) + // 触发重载(非静默模式) + if (!options?.silent && _reloadBundleCallback) { + await _reloadBundleCallback(moduleId) + } }, // ── 版本信息 ────────────────────────────────────────────────────────────── - /** 当前 App versionCode(原生读取) */ getAppVersionCode, - /** 当前 App versionName(原生读取) */ getAppVersionName, - /** - * 开发环境手动设置版本号(仅调试用,生产环境不要调用)。 - */ _devSetAppVersion(versionCode: number, versionName?: string): void { _devSetAppVersion(versionCode, versionName) }, diff --git a/packages/update/src/index.ts b/packages/update/src/index.ts index ea59a1f..5895205 100644 --- a/packages/update/src/index.ts +++ b/packages/update/src/index.ts @@ -1,3 +1,2 @@ export { UpdateSDK } from './UpdateSDK' -export type { PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle } from './UpdateSDK' -export { NativeBundle } from './NativeBundle' +export type { PluginRegistration, PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle } from './UpdateSDK' diff --git a/src/index.ts b/src/index.ts index 16413d1..25826ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,6 @@ export { XuqmSDK } from './sdk' -export type { UnifiedLoginOptions } from './sdk' -export type { XuqmInitOptions, XuqmUserInfo, DeviceInfo } from '@xuqm/rn-common' -export { getDeviceId, getDeviceInfo, detectPushVendor, setUserId, getUserId } from '@xuqm/rn-common' +export type { XuqmInitOptions, XuqmUserInfo, DeviceInfo, XuqmConfig } from '@xuqm/rn-common' +export { getDeviceId, getDeviceInfo, detectPushVendor, setUserId, getUserId, setUserInfo, getUserInfo } from '@xuqm/rn-common' export { ScaledImage } from '@xuqm/rn-common' export { apiRequest } from '@xuqm/rn-common' export { ImSDK, ImClient, ImDatabase, uploadFile } from '@xuqm/rn-im' @@ -24,7 +23,7 @@ export type { export { PushSDK } from '@xuqm/rn-push' export type { PushVendor } from '@xuqm/rn-push' export { UpdateSDK } from '@xuqm/rn-update' -export type { PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle } from '@xuqm/rn-update' +export type { PluginRegistration, PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle } from '@xuqm/rn-update' export { XWebViewControl, XWebViewScreen, diff --git a/src/sdk.ts b/src/sdk.ts index cdd611c..894b485 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -1,63 +1,3 @@ -import { _clearToken, setUserId as setCommonUserId, getUserId as getCommonUserId, XuqmSDK as CommonXuqmSDK } from '@xuqm/rn-common' -import { ImSDK } from '@xuqm/rn-im' -import { PushSDK } from '@xuqm/rn-push' - -export interface UnifiedLoginOptions { - userId: string - userSig: string -} - -let currentSession: UnifiedLoginOptions | null = null - -async function applyLoginSession(session: UnifiedLoginOptions): Promise { - setCommonUserId(session.userId) - try { - await ImSDK.login(session.userId, session.userSig) - } catch (error) { - setCommonUserId(null) - currentSession = null - throw error - } - currentSession = session - try { - await PushSDK.initialize(session.userId) - } catch (error) { - void error - } -} - -async function login(options: UnifiedLoginOptions): Promise { - if ( - currentSession && - currentSession.userId === options.userId && - currentSession.userSig === options.userSig - ) { - setCommonUserId(options.userId) - return - } - if (currentSession) { - await logout() - } - await applyLoginSession(options) -} - -async function logout(): Promise { - const userId = currentSession?.userId ?? getCommonUserId() - currentSession = null - setCommonUserId(null) - if (userId) { - try { - await PushSDK.logout(userId) - } catch (error) { - void error - } - } - ImSDK.disconnect() - await _clearToken() -} - -export const XuqmSDK = { - ...CommonXuqmSDK, - login, - logout, -} +// XuqmSDK 统一入口 — 直接重新导出 common 层的 XuqmSDK。 +// 原 login/logout 包装层已移除;用户认证统一通过 XuqmSDK.setUserInfo() 完成。 +export { XuqmSDK } from '@xuqm/rn-common'