feat: v0.3.0 — 自动初始化 + 插件更新 + 脚手架工具

common:
- 新增 autoInit.ts 自动初始化(对齐 Android ContentProvider 模式)
- 新增 configCrypto.ts 内置配置文件解密
- XuqmSDK 新增 initWithConfigFile / setUserInfo / getUserInfo
- 新增 crypto-types.d.ts Web Crypto 类型声明

update:
- 重写 UpdateSDK:checkAppUpdate / checkPluginUpdate / checkAndCachePlugin
- 移除 checkAndPromptAppUpdate(SDK 不做 UI)
- 新增插件脚手架 create-plugin.mjs
- 重命名 RnUpdateInfo → PluginUpdateInfo

license:
- crypto.ts 支持 XUQM-CONFIG-V1 + XUQM-LICENSE-V1 双格式
- 新增 decryptConfigFile 导出

docs:
- 重写 README.md
- 新增 docs/SDK-API参考.md
- 新增 docs/插件脚手架.md
- 新增 docs/配置文件规范.md
这个提交包含在:
XuqmGroup 2026-06-15 01:44:20 +08:00
父节点 af3aa0cd43
当前提交 ab30b28f3d
共有 22 个文件被更改,包括 4327 次插入82 次删除

370
README.md
查看文件

@ -1,43 +1,359 @@
# XuqmGroup React Native SDK # XuqmGroup React Native SDK
`rn-sdk` 的稳定入口是 `src/index.ts`,统一登录/登出层在 `src/sdk.ts` Modular React Native SDK providing IM, Push, App/Plugin Update, WebView, and License management for XuqmGroup platform applications.
旧的 `src/core`、`src/im`、`src/push`、`src/update` 目录已清理,避免继续引用废弃实现。
## 当前结构 ## Table of Contents
```text - [Overview](#overview)
XuqmGroup-RNSDK/ - [Package Structure](#package-structure)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Package Details](#package-details)
- [Development](#development)
- [Plugin Scaffolding](#plugin-scaffolding)
---
## Overview
`@xuqm/rn-sdk` is the meta-package that re-exports all sub-modules. It provides a unified `XuqmSDK` entry point with coordinated login/logout that wires up IM, Push, and token management in a single call.
You can install the meta-package for full functionality, or pick individual packages as needed.
**Peer Dependencies** (required in host app):
| Package | Version |
|---------|---------|
| `react` | >= 18.0.0 |
| `react-native` | >= 0.76.0 |
| `@react-native-async-storage/async-storage` | >= 1.21.0 |
---
## Package Structure
```
XuqmGroup-RNSDK/ # @xuqm/rn-sdk (meta-package, private)
├── src/ ├── src/
│ ├── index.ts # 对外聚合入口 │ ├── index.ts # Re-exports all sub-packages
│ └── sdk.ts # 统一登录 / 登出封装 │ └── sdk.ts # Unified login / logout
├── packages/ ├── packages/
│ ├── common/ # 初始化、网络、设备、Token、基础组件 │ ├── common/ @xuqm/rn-common # Init, network, device, token, HTTP, auto-init
│ ├── im/ # IM、会话、历史、群组、关系链 │ ├── im/ @xuqm/rn-im # IM messaging (WebSocket/STOMP + WatermelonDB)
│ ├── push/ # 推送设备注册 │ ├── push/ @xuqm/rn-push # Multi-vendor push registration
│ ├── update/ # App 更新 / RN 热更新 │ ├── update/ @xuqm/rn-update # App update check + RN plugin hot-update
│ └── xwebview/ # 内置 WebView 浏览器 ├── xwebview/ @xuqm/rn-xwebview # Enhanced WebView with JSBridge
└── README.md │ └── license/ @xuqm/rn-license # Encrypted license file decryption & verification
``` ```
## 安装 All packages are at version **0.2.2**.
---
## Installation
### 1. Configure the npm registry
```bash
# In your host project root
cat > .npmrc << 'EOF'
registry=https://nexus.xuqinmin.com/repository/npm/
legacy-peer-deps=true
EOF
```
### 2. Install
**Option A -- Meta-package (all modules):**
```bash ```bash
yarn add @xuqm/rn-sdk yarn add @xuqm/rn-sdk
yarn add @react-native-async-storage/async-storage yarn add @react-native-async-storage/async-storage
# 如需按模块拆分接入,也可以直接安装
yarn add @xuqm/rn-common @xuqm/rn-im @xuqm/rn-push @xuqm/rn-update @xuqm/rn-xwebview
``` ```
## 入口 **Option B -- Individual packages:**
- `XuqmSDK.initialize({ appKey, logLevel })` ```bash
- `XuqmSDK.login({ userId, userSig })` yarn add @xuqm/rn-common @xuqm/rn-im @xuqm/rn-push @xuqm/rn-update @xuqm/rn-xwebview @xuqm/rn-license
- `XuqmSDK.logout()` yarn add @react-native-async-storage/async-storage
- `ImSDK` ```
- `PushSDK`
- `UpdateSDK`
- `XWebViewScreen`
- `XWebViewView`
详细用法见 [docs/rn-sdk/README.md](../docs/rn-sdk/README.md)。 ### 3. Install optional peer dependencies
```bash
# IM module (local database)
yarn add @nozbe/watermelondb
# XWebView module
yarn add react-native-webview react-native-blob-util react-native-svg
# License / encrypted config
yarn add react-native-quick-crypto
```
---
## Quick Start
### Zero-config initialization (recommended)
Place an encrypted `.xuqmconfig` file (XUQM-CONFIG-V1 format) in your project, then add a Metro alias so the SDK can auto-discover it:
```js
// metro.config.js
const { getDefaultConfig } = require('metro-config');
module.exports = (async () => {
const config = await getDefaultConfig(__dirname);
config.resolver.extraNodeModules = {
'@xuqm/autoinit-config': './path/to/your.xuqmconfig',
};
return config;
})();
```
That is it. When `@xuqm/rn-common` is imported, it silently calls `XuqmSDK.initWithConfigFile()` using the encrypted file. No explicit init code needed.
### Manual initialization
```ts
import { XuqmSDK } from '@xuqm/rn-sdk';
// Init with appKey (fetches config from server)
await XuqmSDK.initialize({ appKey: 'your-app-key' });
// Login (wires up IM + Push automatically)
await XuqmSDK.login({ userId: 'user123', userSig: '...' });
// Logout
await XuqmSDK.logout();
```
### Using individual modules
```ts
import { ImSDK } from '@xuqm/rn-im';
import { PushSDK } from '@xuqm/rn-push';
import { UpdateSDK } from '@xuqm/rn-update';
import { XWebViewScreen } from '@xuqm/rn-xwebview';
```
---
## Package Details
### @xuqm/rn-common
Core module. Handles SDK initialization, HTTP requests, token persistence, device identification, and auto-initialization from encrypted config files.
| Export | Description |
|--------|-------------|
| `XuqmSDK.initialize(opts)` | Init with `appKey`, fetches remote config |
| `XuqmSDK.initWithConfigFile(encrypted)` | Init from `XUQM-CONFIG-V1` encrypted file |
| `XuqmSDK.initializeFromLicense(licenseFile)` | Init from decrypted license data |
| `XuqmSDK.awaitInitialization()` | Wait for async init to complete |
| `XuqmSDK.setUserId / getUserId` | Manage current user ID |
| `XuqmSDK.setUserInfo / getUserInfo` | Manage current user profile |
| `apiRequest(url, options?)` | HTTP request with Bearer token auth |
| `configureHttp(opts)` | Override HTTP base URL or headers |
| `getDeviceId()` | Stable per-install UUID |
| `getDeviceInfo()` | Device brand/model/OS info |
| `detectPushVendor()` | Detect push vendor from device brand |
| `ScaledImage` | Image component with aspect-ratio scaling |
| `isInitialized()` | Check if SDK is ready |
| `getConfig()` | Get resolved config |
**Auto-init mechanism:** On import, `autoInit.ts` tries `require('@xuqm/autoinit-config')`. If the Metro alias is configured, it decrypts the file and calls `initWithConfigFile` silently. If the alias is not configured, it skips without error.
**Encrypted config format:** `XUQM-CONFIG-V1.{base64url-salt}.{base64url-iv}.{base64url-ciphertext}` -- decrypted via PBKDF2 (120k iterations, SHA-256) + AES-256-GCM using `react-native-quick-crypto`.
---
### @xuqm/rn-im
Full-featured IM module. WebSocket/STOMP transport with WatermelonDB local persistence. Supports single and group chat with 15 message types.
**Message types:** `TEXT`, `IMAGE`, `VIDEO`, `AUDIO`, `FILE`, `CUSTOM`, `LOCATION`, `NOTIFY`, `RICH_TEXT`, `CALL_AUDIO`, `CALL_VIDEO`, `QUOTE`, `MERGE`, `REVOKED`, `FORWARD`
| Export | Description |
|--------|-------------|
| `ImSDK.login(userId, userSig)` | Connect to IM server |
| `ImSDK.disconnect()` | Disconnect |
| `ImSDK.sendMessage(params)` | Send a message |
| `ImSDK.getConversations()` | List conversations |
| `ImSDK.getHistory(query)` | Fetch message history |
| `ImSDK.createGroup(...)` | Create a group |
| `ImSDK.joinGroup(groupId)` | Join a group |
| `ImSDK.getGroupInfo(groupId)` | Get group details |
| `ImSDK.getGroupMembers(groupId)` | List group members |
| `listFriends / addFriend / removeFriend` | Friend management |
| `setFriendGroup / listFriendGroups` | Friend grouping |
| `checkBlacklist` | Blacklist check |
| `searchUsers / searchGroups / searchMessages` | Search |
| `editMessage` | Edit a sent message |
| `setConversationHidden / setConversationGroup` | Conversation management |
| `locateHistoryPage / locateGroupHistoryPage` | Jump to specific history page |
| `syncOfflineMessages / offlineMessageCount` | Offline message handling |
| `ImClient` | Low-level STOMP client |
| `ImDatabase` | WatermelonDB wrapper |
| `uploadFile` | File upload for IM |
**Additional peer dependency:** `@nozbe/watermelondb >= 0.27.0`
---
### @xuqm/rn-push
Multi-vendor push registration. Automatically detects the device vendor (Huawei, Xiaomi, OPPO, vivo, Honor, FCM, APNS) and bridges to the native push module.
| Export | Description |
|--------|-------------|
| `PushSDK.initialize(userId?)` | Initialize push and register device token |
| `PushSDK.registerToken(userId, token, vendor?)` | Register a push token with server |
| `PushSDK.unregisterToken(userId)` | Unregister push token |
| `PushSDK.setDeviceToken(token, vendor?)` | Manually set device token |
| `PushSDK.onPushToken(callback)` | Listen for push token updates |
| `PushSDK.logout(userId?)` | Unregister and clean up |
| `isNativePushAvailable()` | Check if native push module is linked |
| `detectVendorNative()` | Detect vendor via native module |
| `registerPushNative()` | Trigger native vendor registration |
**Native module:** Requires `NativeModules.XuqmPushModule` to be linked in the host app.
---
### @xuqm/rn-update
App update check and RN plugin (bundle) hot-update. Checks for new app versions and downloads/caches RN bundles for OTA delivery.
| Export | Description |
|--------|-------------|
| `UpdateSDK.registerPlugin(meta)` | Register plugin metadata at bundle load time |
| `UpdateSDK.checkAppUpdate(bypassIgnore?)` | Check for app-level update |
| `UpdateSDK.openStore(url?, marketUrl?)` | Open app store page |
| `UpdateSDK.checkPluginUpdate(moduleId)` | Check if a plugin bundle has an update |
| `UpdateSDK.downloadPluginBundle(url)` | Download bundle source text |
| `UpdateSDK.cachePluginBundle(id, ver, md5, src)` | Cache bundle to AsyncStorage |
| `UpdateSDK.getCachedPluginBundle(id)` | Read cached bundle |
| `UpdateSDK.checkAndCachePlugin(id)` | One-step check + download + cache |
| `UpdateSDK.getRegisteredPlugins()` | List all registered plugins |
| `UpdateSDK.getAppVersionCode / getAppVersionName` | Get host app version |
**Types:** `PluginMeta`, `AppUpdateInfo`, `PluginUpdateInfo`, `CachedRnBundle`
---
### @xuqm/rn-xwebview
Enhanced WebView with JSBridge for bidirectional communication between RN and web content. Includes progress bar, inline view, and full-screen screen components.
| Export | Description |
|--------|-------------|
| `XWebViewScreen` | Full-screen WebView (React Navigation screen) |
| `XWebViewView` | Inline WebView component |
| `XWebViewProgress` | Progress bar component |
| `openXWebView(url, options?)` | Open WebView programmatically |
| `setXWebViewController(controller)` | Set global WebView controller |
| `getXWebViewConfig()` | Get current WebView config |
**Additional dependencies:** `react-native-webview`, `react-native-blob-util`, `react-native-svg`
**Additional peer dependency:** `@react-navigation/native >= 7.0.0`
---
### @xuqm/rn-license
Encrypted license file decryption and device license verification with server. Supports `XUQM-CONFIG-V1` and `XUQM-LICENSE-V1` encrypted formats.
| Export | Description |
|--------|-------------|
| `initialize(appKey, options?)` | Configure license with appKey and optional baseUrl |
| `initializeFromFile(encryptedContent)` | Decrypt and init from encrypted file |
| `checkLicense(userInfo?)` | Verify device license with server (10-min cache) |
| `getStatus()` | Returns `'ok'`, `'denied'`, or `'unknown'` |
| `getDeviceId()` | Get device ID for license check |
| `decryptLicenseFile(content)` | Decrypt `XUQM-LICENSE-V1` format |
| `decryptConfigFile(content)` | Decrypt `XUQM-CONFIG-V1` format (alias) |
**Additional peer dependency:** `react-native-quick-crypto >= 0.7.0`
---
## Development
### Prerequisites
- Node.js >= 18
- Yarn (workspaces)
- React Native development environment
### Commands
```bash
# Type-check all packages
yarn typecheck
# Type-check a single package
cd packages/im && yarn typecheck
```
### Publishing
Packages are published to the private npm registry at `https://nexus.xuqinmin.com/repository/npm-hosted/`.
```bash
# Publish a single package
cd packages/common
npm publish
# Publish all packages (from each package directory)
for pkg in common im push update xwebview license; do
cd packages/$pkg && npm publish && cd ../..
done
```
### Project layout
```
packages/<name>/
├── src/
│ └── index.ts # Public API
├── package.json
├── tsconfig.json
└── README.md
```
Each package has its own `package.json`, `tsconfig.json`, and `publishConfig` pointing to the hosted registry. The root `package.json` is a private meta-package that depends on all sub-packages.
---
## Plugin Scaffolding
The `create-plugin.mjs` script generates a new UpdateSDK plugin skeleton and auto-registers it into the host project.
### Usage
```bash
# Interactive mode
node packages/update/scripts/create-plugin.mjs
# CLI mode
node packages/update/scripts/create-plugin.mjs <moduleId> [title] [subtitle] [accentColor]
```
### What it generates
- `bundle.ts` -- Plugin entry point
- `{PascalCase}Screen.tsx` -- Screen component
- `plugin.json` -- Plugin metadata
### What it auto-registers
- `pluginCatalog.ts` -- Adds plugin to catalog
- `debugPlugins.ts` -- Adds to debug plugin list
- `package.json` -- Adds build script for the new bundle
- `metro.split.config.js` -- Adds bundle entry
- `babel.config.js` -- Adds module alias
- `tsconfig.json` -- Adds path mapping
**Module ID rules:** Lowercase alphanumeric, must be unique across all plugins.

846
docs/SDK-API参考.md 普通文件
查看文件

@ -0,0 +1,846 @@
# XuqmGroup RN SDK API 参考
> 版本:基于源码自动生成,最后更新 2026-06-15
>
> 包名:`@xuqm/rn-common` · `@xuqm/rn-update` · `@xuqm/rn-xwebview` · `@xuqm/rn-push` · `@xuqm/rn-im` · `@xuqm/rn-license`
---
## 1. XuqmSDKcommon
> `import { XuqmSDK } from '@xuqm/rn-common'`
SDK 核心初始化模块。所有其他 SDK 模块依赖 XuqmSDK 完成初始化后才能正常工作。
### 1.1 类型定义
```ts
interface XuqmInitOptions {
appKey: string; // 应用标识(从租户平台获取)
debug?: boolean; // 是否开启调试日志
}
interface XuqmConfig {
appKey: string;
apiUrl: string;
imWsUrl: string;
fileServiceUrl: string;
debug: boolean;
}
interface XuqmUserInfo {
userId?: string;
name?: string;
email?: string;
phone?: string;
}
```
### 1.2 API 方法
#### `XuqmSDK.initialize(options): Promise<void>`
异步初始化。从租户平台拉取远程配置IM WebSocket URL、文件服务 URL 等),失败时回退到内置默认地址。
```ts
await XuqmSDK.initialize({ appKey: "your-app-key", debug: __DEV__ });
```
#### `XuqmSDK.init(options): void`
同步初始化(不拉取远程配置,直接使用内置默认 URL。适用于网络不可用或无需远程配置的场景。
```ts
XuqmSDK.init({ appKey: "your-app-key" });
```
#### `XuqmSDK.initializeFromLicense(file, options?): void`
从已解密的 License 文件对象初始化。通常配合 License SDK 使用。
```ts
interface LicenseFile {
appKey: string;
baseUrl?: string;
serverUrl?: string;
}
XuqmSDK.initializeFromLicense(decryptedFile, { debug: false });
```
#### `XuqmSDK.initWithConfigFile(encryptedContent, options?): Promise<void>`
从加密配置文件(`.xuqmconfig`初始化。SDK 自动解密并初始化,支持 `XUQM-CONFIG-V1``XUQM-LICENSE-V1` 两种格式。
```ts
import config from "./assets/app.xuqmconfig";
await XuqmSDK.initWithConfigFile(config);
```
#### `XuqmSDK.awaitInitialization(): Promise<void>`
等待初始化完成。在异步初始化场景下,其他模块可调用此方法确保 SDK 已就绪。
```ts
await XuqmSDK.awaitInitialization();
```
#### `XuqmSDK.setUserId(userId): void`
设置当前用户 ID。登录成功后调用。
```ts
XuqmSDK.setUserId("user-123");
```
#### `XuqmSDK.getUserId(): string | null`
获取当前用户 ID。
#### `XuqmSDK.setUserInfo(info): void`
设置用户信息(用于灰度发布和 License 验证)。登录成功后调用,同时会自动同步 `userId`
```ts
XuqmSDK.setUserInfo({ userId: "user-123", name: "张三", phone: "13800138000" });
```
#### `XuqmSDK.getUserInfo(): XuqmUserInfo | null`
获取当前用户信息。
### 1.3 独立导出
```ts
import { awaitInitialization } from "@xuqm/rn-common";
// 等同于 XuqmSDK.awaitInitialization()
```
---
## 2. UpdateSDKupdate
> `import { UpdateSDK } from '@xuqm/rn-update'`
应用整包更新 + RN 插件Bundle热更新。
### 2.1 类型定义
```ts
interface PluginMeta {
moduleId: string; // 插件唯一标识,如 'buz1'
version: string; // 当前 bundle 版本号
}
interface AppUpdateInfo {
needsUpdate: boolean;
versionName?: string; // 最新版本名,如 '2.1.0'
versionCode?: number; // 最新版本号(整数)
downloadUrl?: string; // APK 直接下载地址
changeLog?: string; // 更新日志
forceUpdate?: boolean; // 是否强制更新
appStoreUrl?: string; // iOS App Store 地址
marketUrl?: string; // Android 应用商店地址
requiresLogin?: boolean; // 服务端要求登录后才能检查
alreadyDownloaded?: boolean; // APK 是否已下载(仅 Android
apkHash?: string | null; // APK SHA-256 校验值
}
interface PluginUpdateInfo {
needsUpdate: boolean;
latestVersion: string;
downloadUrl: string;
md5: string;
minCommonVersion: string; // 要求的最低 common bundle 版本
note: string;
}
interface CachedRnBundle {
moduleId: string;
version: string;
md5: string;
downloadedAt: string;
source: string;
}
```
### 2.2 API 方法
#### `UpdateSDK.registerPlugin(meta): void`
注册插件元数据。在插件 bundle 入口文件顶部调用。
```ts
// src/plugins/buz1/bundle.ts
UpdateSDK.registerPlugin({ moduleId: "buz1", version: "1.0.0" });
```
#### `UpdateSDK.getRegisteredPluginVersion(moduleId): string | undefined`
获取已注册的插件版本号。
#### `UpdateSDK.getRegisteredPlugins(): PluginMeta[]`
获取所有已注册的插件列表。
#### `UpdateSDK.checkAppUpdate(bypassIgnore?): Promise<AppUpdateInfo>`
检查 App 整包更新。自动检测平台iOS/Android并传给服务端。
- `bypassIgnore = false`(默认):静默检查,跳过用户已忽略的版本
- `bypassIgnore = true`:用户主动检查,不跳过
```ts
const info = await UpdateSDK.checkAppUpdate(true);
if (info.needsUpdate && info.forceUpdate) {
// 强制更新
}
```
#### `UpdateSDK.openStore(appStoreUrl?, marketUrl?): Promise<void>`
打开应用商店。iOS 使用 `appStoreUrl`,Android 使用 `marketUrl`
#### `UpdateSDK.checkPluginUpdate(moduleId): Promise<PluginUpdateInfo>`
检查指定插件的更新。插件必须已通过 `registerPlugin()` 注册。
```ts
const info = await UpdateSDK.checkPluginUpdate("buz1");
if (info.needsUpdate) {
// 下载并缓存
}
```
#### `UpdateSDK.downloadPluginBundle(downloadUrl): Promise<string>`
下载插件 bundle 源码文本。
#### `UpdateSDK.cachePluginBundle(moduleId, version, md5, source): Promise<CachedRnBundle>`
缓存插件 bundle 到本地AsyncStorage
#### `UpdateSDK.getCachedPluginBundle(moduleId): Promise<CachedRnBundle | null>`
读取已缓存的插件 bundle。
#### `UpdateSDK.checkAndCachePlugin(moduleId): Promise<CachedRnBundle | null>`
检查并下载插件更新(一步完成)。有更新时下载并缓存,无更新返回 `null`。调用方需在下次启动时通过 `reloadPlugin()` 加载新版本。
```ts
const cached = await UpdateSDK.checkAndCachePlugin("buz1");
if (cached) {
// 下次启动生效
}
```
#### `UpdateSDK.getAppVersionCode(): number`
当前 App versionCode原生读取
#### `UpdateSDK.getAppVersionName(): string`
当前 App versionName原生读取
#### `UpdateSDK._devSetAppVersion(versionCode, versionName?): void`
开发环境手动设置版本号(仅调试用,生产环境不要调用)。
---
## 3. XWebViewxwebview
> `import { XWebViewScreen, XWebViewView, XWebViewConfig, ... } from '@xuqm/rn-xwebview'`
WebView 容器组件,支持导航栏、文件下载、JS Bridge 等。
### 3.1 类型定义
```ts
type XWebViewConfig = {
showTopBar?: boolean; // 是否显示顶部导航栏
showStatusBar?: boolean; // 是否显示状态栏
doubleBackExit?: boolean; // 双击返回退出
title?: string; // 自定义标题
showTitle?: boolean; // 是否显示标题
autoTitle?: boolean; // 自动从网页获取标题
showMenu?: boolean; // 是否显示菜单按钮
openForBrowser?: boolean; // 是否用外部浏览器打开
clickMenu?: XWebViewClickMenu; // 自定义菜单
url?: string; // 加载的 URL
content?: string; // 加载的 HTML 内容
onMessage?: (event: XWebViewMessageEvent) => void; // 接收 Web 消息
injectedJavaScript?: string; // 注入的 JS
onPermissionRequest?: (request: XWebViewPermissionRequest) => void;
autoDownload?: boolean; // 自动下载
onDownloadStart?: (request: XWebViewDownloadRequest) => void;
onDownloadProgress?: (progress: XWebViewDownloadProgress) => void;
onDownloadComplete?: (result: XWebViewDownloadResult) => void;
onDownloadError?: (url: string, error: string) => void;
onDownloadDecide?: (
request: XWebViewDownloadRequest,
) => XWebViewDownloadDecision | Promise<XWebViewDownloadDecision>;
downloadConflict?: "rename" | "overwrite";
onClose?: () => void;
};
type XWebViewControllerAPI = {
refresh: () => void;
close: () => void;
goBack: () => void;
goForward: () => void;
copyUrl: () => void;
postMessageToWeb: (jsString: string) => void;
getTitle: () => string;
};
type XWebViewClickMenu = { view?: React.ReactNode; onClick: () => void };
type XWebViewMessageEvent = { nativeEvent: { data: string } };
type XWebViewPermissionRequest = {
origin: string;
resources: string[];
grant: (resources?: string[]) => void;
deny: () => void;
};
type XWebViewDownloadRequest = {
url: string;
suggestedFilename: string;
mimeType?: string;
fileSize?: number;
};
type XWebViewDownloadDecision = {
allowed: boolean;
filename?: string;
savePath?: string;
};
type XWebViewDownloadProgress = {
url: string;
filename: string;
received: number;
total: number;
percentage: number;
};
type XWebViewDownloadResult = {
url: string;
filename: string;
filePath: string;
fileSize: number;
};
```
### 3.2 组件
#### `<XWebViewScreen />`
全屏 WebView 页面组件,带顶部导航栏(返回、关闭、菜单)。作为独立屏幕使用,通过导航跳转进入。
#### `<XWebViewView />`
嵌入式 WebView 组件,无顶部导航栏。适用于嵌入到其他页面中。
### 3.3 桥接函数
#### `openXWebView(navigate, config): void`
打开 XWebView 页面。调用后会设置配置并执行导航。
```ts
import { openXWebView } from "@xuqm/rn-xwebview";
openXWebView(navigation.navigate, {
url: "https://example.com",
title: "详情",
showTopBar: true,
});
```
#### `getXWebViewConfig(): XWebViewConfig`
获取当前 XWebView 配置(由 `openXWebView` 设置)。
#### `setXWebViewController(controller: XWebViewControllerAPI | null): void`
设置 XWebView 控制器实例(由组件内部调用)。
#### `XWebViewControl: XWebViewControllerAPI`
全局 WebView 控制代理。可调用 `refresh()`、`close()`、`goBack()`、`goForward()`、`copyUrl()`、`postMessageToWeb(js)`、`getTitle()` 等方法控制当前 WebView。
```ts
import { XWebViewControl } from "@xuqm/rn-xwebview";
XWebViewControl.postMessageToWeb('window.dispatchEvent(new Event("refresh"))');
XWebViewControl.close();
```
### 3.4 子组件
#### `XWebViewProgress`
WebView 加载进度条组件。
---
## 4. PushSDKpush
> `import { PushSDK } from '@xuqm/rn-push'`
推送服务 SDK,支持华为、小米、OPPO、vivo、荣耀、FCMAndroid和 APNsiOS
### 4.1 类型定义
```ts
type PushVendor =
| "HUAWEI"
| "XIAOMI"
| "OPPO"
| "VIVO"
| "HONOR"
| "FCM"
| "APNS";
```
### 4.2 API 方法
#### `PushSDK.initialize(userId?): Promise<void>`
初始化推送服务。如果已有缓存的 token 且用户匹配,自动注册到服务端。
```ts
await PushSDK.initialize("user-123");
```
#### `PushSDK.setPendingToken(token, vendor?): void`
缓存设备 token不立即注册。适用于原生层在用户登录前就收到 token 的场景。
```ts
PushSDK.setPendingToken(token, "HUAWEI");
```
#### `PushSDK.getPendingToken(): { token: string; vendor?: PushVendor } | null`
获取当前缓存的 pending token。
#### `PushSDK.setDeviceToken(token, vendor?): Promise<void>`
设置设备 token 并立即尝试注册到服务端。
#### `PushSDK.requestNativeRegistration(): Promise<void>`
触发原生推送注册。Android 尝试注册厂商 SDK,iOS 请求 APNs 注册。通过 `onPushToken()` 监听 token 回调。
#### `PushSDK.onPushToken(callback): () => void`
监听原生层推送 token。返回取消监听函数。
```ts
const unsubscribe = PushSDK.onPushToken((token, vendor) => {
PushSDK.setDeviceToken(token, vendor as PushVendor);
});
```
#### `PushSDK.registerToken(userId, token, vendor?): Promise<void>`
注册推送设备 token 到服务端。vendor 缺省时自动检测设备品牌。
```ts
await PushSDK.registerToken("user-123", deviceToken, "HUAWEI");
```
#### `PushSDK.unregisterToken(userId): Promise<void>`
注销用户的推送 token。
#### `PushSDK.logout(userId?): Promise<void>`
登出推送服务(内部调用 `unregisterToken`。userId 缺省时使用当前用户。
---
## 5. ImSDKim
> `import { ImSDK } from '@xuqm/rn-im'`
即时通讯 SDK,提供登录、消息收发、会话管理、群组、好友等功能。
### 5.1 核心类型
```ts
type ChatType = "SINGLE" | "GROUP";
type MsgType =
| "TEXT"
| "IMAGE"
| "VIDEO"
| "AUDIO"
| "FILE"
| "CUSTOM"
| "LOCATION"
| "NOTIFY"
| "RICH_TEXT"
| "CALL_AUDIO"
| "CALL_VIDEO"
| "QUOTE"
| "MERGE"
| "REVOKED"
| "FORWARD";
type MsgStatus =
| "SENDING"
| "SENT"
| "DELIVERED"
| "READ"
| "FAILED"
| "REVOKED";
interface ImMessage {
id: string;
appKey: string;
fromUserId: string;
fromId?: string;
toId: string;
chatType: ChatType;
msgType: MsgType;
content: string;
status: MsgStatus;
mentionedUserIds?: string;
groupReadCount?: number;
revoked?: boolean;
createdAt: number;
editedAt?: number | null;
}
interface ImGroup {
id: string;
appKey: string;
name: string;
groupType?: string;
creatorId: string;
memberIds: string;
adminIds: string;
announcement?: string | null;
memberInfo?: string | null;
extAttributes?: string | null;
createdAt: number;
}
interface ConversationData {
targetId: string;
chatType: ChatType;
lastMsgContent?: string | null;
lastMsgType?: string | null;
lastMsgTime: number;
unreadCount: number;
isMuted: boolean;
isPinned: boolean;
conversationGroup?: string | null;
}
interface UserProfile {
id?: string;
appKey?: string;
userId: string;
nickname?: string | null;
avatar?: string | null;
gender?: string | null;
status?: string | null;
createdAt?: number | null;
}
interface FriendRequest {
id: string;
appKey: string;
fromUserId: string;
toUserId: string;
remark?: string | null;
status: "PENDING" | "ACCEPTED" | "REJECTED";
createdAt: number;
reviewedAt?: number | null;
}
interface GroupJoinRequest {
id: string;
appKey: string;
groupId: string;
requesterId: string;
remark?: string | null;
status: "PENDING" | "ACCEPTED" | "REJECTED";
createdAt: number;
reviewedAt?: number | null;
}
interface ImEventListener {
onConnected?: () => void;
onDisconnected?: (reason?: string) => void;
onMessage?: (msg: ImMessage) => void;
onGroupMessage?: (msg: ImMessage) => void;
onSystemMessage?: (msg: ImMessage) => void;
onRead?: (msg: ImMessage) => void;
onRevoke?: (data: { msgId: string; operatorId: string }) => void;
onError?: (error: string) => void;
}
```
### 5.2 连接与认证
| 方法 | 签名 | 说明 |
| ------------- | ---------------------------------------------------- | --------------------------------- |
| `login` | `(userId: string, userSig: string) => Promise<void>` | 登录 IM 服务,建立 WebSocket 连接 |
| `reconnect` | `() => Promise<void>` | 重新连接(使用已保存的 token |
| `disconnect` | `() => void` | 断开连接并清理状态 |
| `isConnected` | `() => boolean` | 是否已连接 |
### 5.3 消息发送
| 方法 | 签名 | 说明 |
| ---------------------- | ------------------------------------------------------------------ | ------------ |
| `sendMessage` | `(params: SendMessageParams) => Promise<ImMessage>` | 通用发送 |
| `sendTextMessage` | `(toId, chatType, text) => Promise<ImMessage>` | 文本消息 |
| `sendImageMessage` | `(toId, chatType, imageUri) => Promise<ImMessage>` | 图片消息 |
| `sendVideoMessage` | `(toId, chatType, videoUri) => Promise<ImMessage>` | 视频消息 |
| `sendAudioMessage` | `(toId, chatType, audioUri) => Promise<ImMessage>` | 语音消息 |
| `sendFileMessage` | `(toId, chatType, fileUri) => Promise<ImMessage>` | 文件消息 |
| `sendNotifyMessage` | `(toId, chatType, content) => Promise<ImMessage>` | 通知消息 |
| `sendQuoteMessage` | `(toId, chatType, content, quotedMsgId) => Promise<ImMessage>` | 引用消息 |
| `sendMergeMessage` | `(toId, chatType, title, summary, messages) => Promise<ImMessage>` | 合并转发 |
| `sendCallAudioMessage` | `(toId, chatType, content) => Promise<ImMessage>` | 语音通话消息 |
| `sendCallVideoMessage` | `(toId, chatType, content) => Promise<ImMessage>` | 视频通话消息 |
| `sendCustomMessage` | `(toId, chatType, content) => Promise<ImMessage>` | 自定义消息 |
| `sendLocationMessage` | `(toId, chatType, ...) => Promise<ImMessage>` | 位置消息 |
| `sendRichTextMessage` | `(toId, chatType, content) => Promise<ImMessage>` | 富文本消息 |
| `sendForwardMessage` | `(toId, chatType, messageId) => Promise<ImMessage>` | 转发消息 |
### 5.4 消息操作
| 方法 | 签名 | 说明 |
| ------------------------------ | --------------------------------------------------------------- | --------------------- |
| `revokeMessage` | `(messageId: string) => Promise<ImMessage>` | 撤回消息 |
| `editMessage` | `(messageId: string, content: string) => Promise<ImMessage>` | 编辑消息 |
| `fetchHistory` | `(toId, page?, size?) => Promise<ImMessage[]>` | 获取单聊历史消息 |
| `fetchHistoryWithFilters` | `(toId, params) => Promise<PageResult<ImMessage>>` | 带筛选的历史消息 |
| `fetchGroupHistory` | `(groupId, page?, size?) => Promise<ImMessage[]>` | 获取群聊历史消息 |
| `fetchGroupHistoryWithFilters` | `(groupId, params) => Promise<PageResult<ImMessage>>` | 带筛选的群聊历史 |
| `locateHistoryPage` | `(toId, messageId, size?) => Promise<PageResult<ImMessage>>` | 定位到指定消息所在页 |
| `locateGroupHistoryPage` | `(groupId, messageId, size?) => Promise<PageResult<ImMessage>>` | 群聊定位到指定消息 |
| `syncOfflineMessages` | `(maxCount?) => Promise<ImMessage[]>` | 同步离线消息 |
| `offlineMessageCount` | `() => Promise<number>` | 离线消息数量 |
| `searchMessages` | `(params) => Promise<...>` | 搜索本地消息(需 DB |
### 5.5 会话管理
| 方法 | 签名 | 说明 |
| -------------------------------- | --------------------------------------------------- | ---------------------- |
| `listConversations` | `() => Promise<ConversationData[]>` | 获取会话列表 |
| `subscribeConversations` | `(callback) => () => void` | 订阅会话列表变化 |
| `markRead` | `(targetId, chatType?) => Promise<void>` | 标记已读 |
| `setConversationMuted` | `(targetId, chatType, muted) => Promise<void>` | 设置免打扰 |
| `setConversationPinned` | `(targetId, chatType, pinned) => Promise<void>` | 设置置顶 |
| `setDraft` | `(targetId, chatType, draft) => Promise<void>` | 保存草稿 |
| `getDraft` | `(targetId, chatType) => Promise<string>` | 获取草稿 |
| `setConversationHidden` | `(targetId, chatType, hidden) => Promise<void>` | 隐藏会话 |
| `setConversationGroup` | `(targetId, chatType, groupName?) => Promise<void>` | 设置会话分组 |
| `listConversationGroups` | `() => Promise<string[]>` | 获取会话分组列表 |
| `listConversationGroupItems` | `(groupName) => Promise<ConversationGroupItem[]>` | 获取分组下的会话 |
| `deleteConversation` | `(targetId, chatType) => Promise<void>` | 删除会话 |
| `getTotalUnreadCount` | `() => Promise<number>` | 获取总未读数 |
| `syncHistoryForAllConversations` | `() => Promise<void>` | 同步所有会话的历史消息 |
### 5.6 群组管理
| 方法 | 签名 | 说明 |
| ------------------------- | ------------------------------------------------------------- | -------------------- |
| `createGroup` | `(name, memberIds, groupType?) => Promise<ImGroup>` | 创建群组 |
| `listGroups` | `() => Promise<ImGroup[]>` | 获取已加入的群组列表 |
| `listPublicGroups` | `(keyword?) => Promise<ImGroup[]>` | 搜索公开群组 |
| `getGroupInfo` | `(groupId) => Promise<ImGroup>` | 获取群组详情 |
| `listGroupMembers` | `(groupId) => Promise<UserProfile[]>` | 获取群成员列表 |
| `searchGroupMembers` | `(groupId, keyword, size?) => Promise<UserProfile[]>` | 搜索群成员 |
| `updateGroupInfo` | `(groupId, name?, announcement?) => Promise<ImGroup>` | 更新群信息 |
| `addGroupMember` | `(groupId, userId) => Promise<ImGroup>` | 添加群成员 |
| `removeGroupMember` | `(groupId, targetUserId) => Promise<ImGroup>` | 移除群成员 |
| `batchAddGroupMembers` | `(groupId, userIds) => Promise<void>` | 批量添加群成员 |
| `batchRemoveGroupMembers` | `(groupId, userIds) => Promise<void>` | 批量移除群成员 |
| `leaveGroup` | `(groupId) => Promise<ImGroup>` | 退出群组 |
| `setGroupRole` | `(groupId, userId, role) => Promise<ImGroup>` | 设置群成员角色 |
| `muteGroupMember` | `(groupId, userId, minutes) => Promise<ImGroup>` | 禁言群成员 |
| `transferGroupOwner` | `(groupId, newOwnerId) => Promise<ImGroup>` | 转让群主 |
| `updateGroupAttributes` | `(groupId, attributes) => Promise<ImGroup>` | 更新群属性 |
| `removeGroupAttributes` | `(groupId, keys) => Promise<ImGroup>` | 删除群属性 |
| `dismissGroup` | `(groupId) => Promise<void>` | 解散群组 |
| `modifyGroupMemberInfo` | `(groupId, userId, nickname?, role?) => Promise<void>` | 修改群成员信息 |
| `adminGroupReadReceipts` | `(groupId, messageIds) => Promise<GroupReadReceiptSummary[]>` | 群消息已读回执 |
### 5.7 群组加入请求
| 方法 | 签名 | 说明 |
| ------------------------------ | --------------------------------------------------- | ---------------- |
| `sendGroupJoinRequest` | `(groupId, remark?) => Promise<GroupJoinRequest>` | 发送入群申请 |
| `listGroupJoinRequests` | `(groupId) => Promise<GroupJoinRequest[]>` | 获取入群申请列表 |
| `acceptGroupJoinRequest` | `(groupId, requestId) => Promise<GroupJoinRequest>` | 同意入群申请 |
| `rejectGroupJoinRequest` | `(groupId, requestId) => Promise<GroupJoinRequest>` | 拒绝入群申请 |
| `batchAcceptGroupJoinRequests` | `(groupId, requestIds) => Promise<void>` | 批量同意 |
| `batchRejectGroupJoinRequests` | `(groupId, requestIds) => Promise<void>` | 批量拒绝 |
### 5.8 好友管理
| 方法 | 签名 | 说明 |
| --------------------------- | ----------------------------------------------- | ---------------- |
| `listFriends` | `() => Promise<string[]>` | 获取好友列表 |
| `addFriend` | `(friendId) => Promise<void>` | 添加好友 |
| `removeFriend` | `(friendId) => Promise<void>` | 删除好友 |
| `removeAllFriends` | `() => Promise<void>` | 删除所有好友 |
| `batchAddFriends` | `(friendIds) => Promise<void>` | 批量添加好友 |
| `batchRemoveFriends` | `(friendIds) => Promise<void>` | 批量删除好友 |
| `setFriendGroup` | `(friendId, groupName?) => Promise<void>` | 设置好友分组 |
| `listFriendGroups` | `() => Promise<string[]>` | 获取好友分组列表 |
| `listFriendsByGroup` | `(groupName) => Promise<string[]>` | 按分组获取好友 |
| `listFriendRequests` | `(direction?) => Promise<FriendRequest[]>` | 获取好友请求列表 |
| `sendFriendRequest` | `(toUserId, remark?) => Promise<FriendRequest>` | 发送好友请求 |
| `acceptFriendRequest` | `(requestId) => Promise<FriendRequest>` | 同意好友请求 |
| `rejectFriendRequest` | `(requestId) => Promise<FriendRequest>` | 拒绝好友请求 |
| `batchAcceptFriendRequests` | `(requestIds) => Promise<void>` | 批量同意 |
| `batchRejectFriendRequests` | `(requestIds) => Promise<void>` | 批量拒绝 |
### 5.9 黑名单
| 方法 | 签名 | 说明 |
| --------------------- | ------------------------------------------------- | -------------- |
| `listBlacklist` | `() => Promise<BlacklistEntry[]>` | 获取黑名单 |
| `addToBlacklist` | `(blockedUserId) => Promise<BlacklistEntry>` | 加入黑名单 |
| `removeFromBlacklist` | `(blockedUserId) => Promise<void>` | 移出黑名单 |
| `checkBlacklist` | `(targetUserId) => Promise<BlacklistCheckResult>` | 检查黑名单状态 |
### 5.10 用户资料
| 方法 | 签名 | 说明 |
| --------------- | --------------------------------------------------------------- | ------------ |
| `getProfile` | `(userId) => Promise<UserProfile>` | 获取用户资料 |
| `updateProfile` | `(userId, nickname?, avatar?, gender?) => Promise<UserProfile>` | 更新用户资料 |
| `searchUsers` | `(keyword, size?) => Promise<UserProfile[]>` | 搜索用户 |
### 5.11 事件监听
| 方法 | 签名 | 说明 |
| ------------------ | ------------------------------------- | ---------------- |
| `addListener` | `(listener: ImEventListener) => void` | 添加事件监听器 |
| `removeListener` | `(listener: ImEventListener) => void` | 移除事件监听器 |
| `subscribeGroup` | `(groupId) => void` | 订阅群组消息 |
| `unsubscribeGroup` | `(groupId) => void` | 取消订阅群组消息 |
---
## 6. Licenselicense
> `import * as License from '@xuqm/rn-license'`
设备 License 验证模块,支持离线缓存和自动注册。
### 6.1 类型定义
```ts
interface LicenseFile {
appKey: string;
appName?: string;
companyName?: string;
baseUrl?: string;
issuedAt?: string;
expiresAt?: string;
}
interface LicenseUserInfo {
userId?: string;
name?: string;
email?: string;
phone?: string;
}
type LicenseStatus = "ok" | "denied" | "unknown";
type LicenseResult =
| { type: "success"; reason: string }
| { type: "error"; message: string };
```
### 6.2 API 方法
#### `initialize(appKey, options?): void`
初始化 License SDK。
```ts
License.initialize("your-app-key", {
baseUrl: "https://auth.xuqinmin.com",
deviceName: "My Device",
});
```
#### `initializeFromFile(encryptedContent): Promise<void>`
从加密 License 文件初始化。自动解密并提取 `appKey``baseUrl`
```ts
import licenseFile from "./assets/license.xuqmconfig";
await License.initializeFromFile(licenseFile);
```
#### `checkLicense(userInfo?): Promise<LicenseResult>`
检查 License 状态。优先使用内存缓存10 分钟有效期),其次持久化缓存,最后请求服务端验证。服务端流程:先尝试 verify已有 token,失败则 register注册新设备
```ts
const result = await License.checkLicense({ userId: "user-123", name: "张三" });
if (result.type === "error") {
console.error(result.message);
}
```
#### `getStatus(): Promise<LicenseStatus>`
获取当前 License 状态(`'ok'` / `'denied'` / `'unknown'`)。
#### `getDeviceId(): Promise<string>`
获取设备 ID首次生成后持久化存储
#### `clear(): Promise<void>`
清除所有 License 缓存状态、token、设备 ID
---
## 快速参考
### 初始化顺序
```ts
// 1. 初始化核心 SDK
await XuqmSDK.initialize({ appKey: "xxx" });
// 或从加密文件初始化
await XuqmSDK.initWithConfigFile(encryptedConfig);
// 2. 登录后设置用户信息
XuqmSDK.setUserInfo({ userId, name, phone });
// 3. 初始化推送
PushSDK.onPushToken((token, vendor) => {
PushSDK.setDeviceToken(token, vendor);
});
await PushSDK.initialize(userId);
// 4. 登录 IM
await ImSDK.login(userId, userSig);
// 5. 检查更新
const appUpdate = await UpdateSDK.checkAppUpdate();
const pluginUpdate = await UpdateSDK.checkAndCachePlugin("buz1");
```
### 包依赖关系
```
@xuqm/rn-common ← XuqmSDK, 配置, HTTP, 工具函数(其他包的底层依赖)
@xuqm/rn-im ← ImSDK依赖 common
@xuqm/rn-push ← PushSDK依赖 common
@xuqm/rn-update ← UpdateSDK依赖 common
@xuqm/rn-xwebview ← XWebView 组件(依赖 common
@xuqm/rn-license ← License依赖 common
```

164
docs/插件脚手架.md 普通文件
查看文件

@ -0,0 +1,164 @@
# 插件脚手架工具
> 脚本位置:`packages/update/scripts/create-plugin.mjs`
> 最后更新2026-06-15
---
## 概述
`create-plugin.mjs` 是 UpdateSDK 提供的插件脚手架工具,用于一键创建完整的插件骨架项目。
**功能**
- 交互式或命令行输入插件参数
- 校验 moduleId 唯一性
- 自动生成插件文件bundle.ts / Screen / plugin.json
- 自动注册到宿主pluginCatalog / debugPlugins / build scripts / metro config / babel alias / tsconfig
---
## 用法
### 命令行模式(推荐)
```bash
node scripts/create-plugin.mjs <moduleId> [title] [subtitle] [accentColor]
```
示例:
```bash
# 完整参数
node scripts/create-plugin.mjs buz4 "IM 消息" "即时通讯业务组件" "#E74C3C"
# 最简title/subtitle/accentColor 使用默认值)
node scripts/create-plugin.mjs buz5
```
### 交互模式
```bash
node scripts/create-plugin.mjs
```
按提示依次输入 moduleId、title、subtitle、accentColor。
---
## 参数说明
| 参数 | 必填 | 格式 | 默认值 | 说明 |
| ----------- | ---- | ------------------ | ----------------- | ----------------------- |
| moduleId | ✅ | `^[a-z][a-z0-9]*$` | — | 插件唯一标识,如 `buz4` |
| title | ❌ | 任意文本 | = moduleId | 插件标题 |
| subtitle | ❌ | 任意文本 | `{title}业务组件` | 插件副标题 |
| accentColor | ❌ | CSS 颜色值 | `#0E84FA` | 主题色 |
---
## 自动生成的文件
### 插件目录 `src/plugins/{moduleId}/`
#### `bundle.ts` — 入口文件
```typescript
import { UpdateSDK } from "@xuqm/rn-update";
import { registerPluginFromBridge } from "@plugins/runtimeBridge";
import { Buz4Screen } from "@buz4/Buz4Screen";
// 自动注册插件版本bundle 加载时执行)
UpdateSDK.registerPlugin({ moduleId: "buz4", version: "1.0.0" });
// 注册 UI 组件到宿主运行时
registerPluginFromBridge({
id: "buz4",
title: "IM 消息",
subtitle: "即时通讯业务组件",
accentColor: "#E74C3C",
Component: Buz4Screen,
});
```
#### `{ModuleId}Screen.tsx` — Screen 骨架
```typescript
import { StyleSheet, Text, View } from 'react-native';
export function Buz4Screen() {
return (
<View style={styles.container}>
<Text style={styles.title}>buz4</Text>
<Text style={styles.subtitle}>TODO: 实现buz4功能</Text>
</View>
);
}
```
#### `plugin.json` — 插件元数据
```json
{ "moduleId": "buz4", "version": "1.0.0" }
```
---
## 自动更新的宿主文件
| 文件 | 变更内容 |
| ------------------------------- | --------------------------------------------------------------- |
| `src/app/pluginCatalog.ts` | 添加插件目录条目id/title/summary/accentColor |
| `src/bootstrap/debugPlugins.ts` | 注册 debug loader`require('../plugins/{id}/bundle')` |
| `package.json` | 添加 `build:android:{id}` / `build:ios:{id}` 脚本,更新聚合脚本 |
| `metro.split.config.js` | 添加 moduleId 的 offset 分支 |
| `babel.config.js` | 添加 `@{id}` alias |
| `tsconfig.json` | 添加 `@{id}/*` path mapping |
---
## 唯一性校验
脚本会检查两处来确保 moduleId 唯一:
1. `src/app/pluginCatalog.ts` 中已注册的 id
2. `src/plugins/` 目录下已存在的目录名
如果 moduleId 已存在,脚本会报错并退出:
```
❌ moduleId "buz4" 已存在,请使用其他 ID
```
---
## moduleId 命名规范
- 必须以**小写字母**开头
- 只能包含**小写字母和数字**
- 推荐格式:`buz{N}`(如 `buz4`、`buz5`
Metro module ID offset 自动计算:`buz1` → 11M,`buz2` → 12M,`buzN` → (10+N)M。
---
## 创建后的开发流程
```bash
# 1. 生成插件骨架
node scripts/create-plugin.mjs buz4 "IM 消息" "即时通讯业务组件" "#E74C3C"
# 2. 实现业务 UI
# 编辑 src/plugins/buz4/Buz4Screen.tsx
# 3. 创建子目录
mkdir -p src/plugins/buz4/services
mkdir -p src/plugins/buz4/pages
# 4. 验证
yarn validate
# 5. 构建
yarn build:android:buz4
yarn build:ios:buz4
```

182
docs/配置文件规范.md 普通文件
查看文件

@ -0,0 +1,182 @@
# 配置文件规范
> 最后更新2026-06-15
---
## 概述
XuqmGroup SDK 使用加密配置文件完成自动初始化,对齐 Android SDK 的 ContentProvider 模式。
**核心流程**
1. 宿主把加密配置文件放到标准位置
2. SDK 在 common bundle 加载时自动读取、解密、初始化
3. 宿主代码零初始化调用
---
## 配置文件格式
### 加密格式
```
XUQM-CONFIG-V1.{base64urlSalt}.{base64urlIV}.{base64urlCiphertext}
```
| 部分 | 说明 |
| --------------------- | ------------------------------------------- |
| `XUQM-CONFIG-V1` | 固定 magic header |
| `base64urlSalt` | PBKDF2 盐值16 字节,base64url 编码) |
| `base64urlIV` | AES-GCM 初始向量12 字节,base64url 编码) |
| `base64urlCiphertext` | AES-256-GCM 密文 + 16 字节 auth tag |
### 加密参数
| 参数 | 值 |
| -------- | ------------------ |
| 算法 | AES-256-GCM |
| 密钥派生 | PBKDF2-HMAC-SHA256 |
| 迭代次数 | 120,000 |
| 密钥长度 | 256 位 |
| Auth Tag | 128 位16 字节) |
### 解密后 JSON 结构
```json
{
"appKey": "yiwangxin",
"appName": "医网信",
"companyName": "BJCA",
"baseUrl": "https://www.51trust.com",
"serverUrl": "https://www.51trust.com",
"packageName": "cn.org.bjca.wcert.ywq",
"iosBundleId": "com.bjca.ywq",
"issuedAt": "2026-01-01T00:00:00Z",
"expiresAt": "2028-01-01T00:00:00Z"
}
```
| 字段 | 必填 | 说明 |
| ------------- | ---- | -------------------------------- |
| `appKey` | ✅ | 应用唯一标识 |
| `appName` | ❌ | 应用名称 |
| `companyName` | ❌ | 公司名称 |
| `baseUrl` | ❌ | API 服务地址(覆盖默认值) |
| `serverUrl` | ❌ | 私有部署地址(覆盖所有服务端点) |
| `packageName` | ❌ | Android 包名(用于校验) |
| `iosBundleId` | ❌ | iOS Bundle ID |
| `issuedAt` | ❌ | 签发时间 |
| `expiresAt` | ❌ | 过期时间 |
---
## 宿主集成方式
### React NativeMetro bundler
由于 Metro 只能 bundle JS/TS 模块,配置文件使用 `.ts` 扩展名。
**文件位置**`src/assets/xuqm/config.xuqmconfig.ts`
```typescript
export const ENCRYPTED_CONFIG = "XUQM-CONFIG-V1.{salt}.{iv}.{ciphertext}";
```
**Metro alias**`metro.config.js`
```javascript
const alias = {
"@xuqm/autoinit-config": path.resolve(
projectRoot,
"src/assets/xuqm/config.xuqmconfig",
),
};
```
**Babel alias**`babel.config.js`
```javascript
'@xuqm/autoinit-config': './src/assets/xuqm/config.xuqmconfig'
```
**TypeScript path**`tsconfig.json`
```json
"@xuqm/autoinit-config": ["src/assets/xuqm/config.xuqmconfig"]
```
### Android原生
配置文件位置:`src/main/assets/xuqm/config.xuqm`
Android SDK 通过 ContentProvider 自动读取,无需额外配置。
### iOS原生
配置文件位置:`{app}/xuqm/config.xuqm`
---
## 自动初始化流程
```
common bundle 加载
→ import '@xuqm/rn-common'
→ packages/common/src/index.ts
→ import './autoInit' (副作用)
→ tryAutoInit()
→ require('@xuqm/autoinit-config')
→ Metro 解析 alias → 宿主 config.xuqmconfig.ts
→ XuqmSDK.initWithConfigFile(encrypted)
→ configCrypto.decryptConfigFile(encrypted)
→ PBKDF2 派生密钥
→ AES-256-GCM 解密
→ initializeFromLicense(decrypted)
→ configureHttp(baseUrl || serverUrl)
→ markInitialized()
```
### 失败处理
对齐 Android SDK 的 `runCatching + Log.w` 行为:
- 配置文件不存在 → 静默跳过
- 解密失败 → 静默跳过
- SDK 保持未初始化状态
- `awaitInitialization()` 会超时
- 不崩溃、不阻塞 app 启动
---
## 降级机制
如果自动初始化失败,宿主可以手动调用:
```typescript
import { XuqmSDK } from "@xuqm/rn-common";
// 使用默认 URL 初始化
XuqmSDK.init({ appKey: "yiwangxin", debug: __DEV__ });
```
---
## 安全注意事项
1. **不要提交解密后的明文配置**到版本控制
2. **不要在日志中打印**配置文件内容或解密后的数据
3. 配置文件的 `appKey` 用于 API 调用,**不要硬编码**在业务代码中
4. `serverUrl` 字段会覆盖所有服务端点,确保指向正确的环境
---
## 与 Android SDK 的对齐
| 环节 | Android SDK | RNSDK |
| ------------ | -------------------------- | --------------------------------------------- |
| 配置文件位置 | `assets/xuqm/config.xuqm` | `src/assets/xuqm/config.xuqmconfig.ts` |
| 自动初始化 | ContentProvider.onCreate() | common 包 import 时 autoInit |
| 解密 | ConfigFileCrypto (Java) | configCrypto (JS + react-native-quick-crypto) |
| 失败处理 | runCatching + Log.w | try/catch + 静默跳过 |
| 服务端点覆盖 | configurePrivateServer() | initializeFromLicense(serverUrl) |

38
package-lock.json 自动生成的
查看文件

@ -1,12 +1,12 @@
{ {
"name": "@xuqm/rn-sdk", "name": "@xuqm/rn-sdk",
"version": "0.2.0", "version": "0.3.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@xuqm/rn-sdk", "name": "@xuqm/rn-sdk",
"version": "0.2.0", "version": "0.3.0",
"license": "UNLICENSED", "license": "UNLICENSED",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@ -14,6 +14,7 @@
"dependencies": { "dependencies": {
"@xuqm/rn-common": ">=0.2.0", "@xuqm/rn-common": ">=0.2.0",
"@xuqm/rn-im": ">=0.2.0", "@xuqm/rn-im": ">=0.2.0",
"@xuqm/rn-license": ">=0.2.0",
"@xuqm/rn-push": ">=0.2.0", "@xuqm/rn-push": ">=0.2.0",
"@xuqm/rn-update": ">=0.2.0", "@xuqm/rn-update": ">=0.2.0",
"@xuqm/rn-xwebview": ">=0.2.0" "@xuqm/rn-xwebview": ">=0.2.0"
@ -722,6 +723,10 @@
"resolved": "packages/im", "resolved": "packages/im",
"link": true "link": true
}, },
"node_modules/@xuqm/rn-license": {
"resolved": "packages/license",
"link": true
},
"node_modules/@xuqm/rn-push": { "node_modules/@xuqm/rn-push": {
"resolved": "packages/push", "resolved": "packages/push",
"link": true "link": true
@ -3479,7 +3484,7 @@
}, },
"packages/common": { "packages/common": {
"name": "@xuqm/rn-common", "name": "@xuqm/rn-common",
"version": "0.2.3", "version": "0.3.2",
"license": "UNLICENSED", "license": "UNLICENSED",
"devDependencies": { "devDependencies": {
"@react-native-async-storage/async-storage": "^2.1.2", "@react-native-async-storage/async-storage": "^2.1.2",
@ -3493,7 +3498,7 @@
}, },
"packages/im": { "packages/im": {
"name": "@xuqm/rn-im", "name": "@xuqm/rn-im",
"version": "0.2.1", "version": "0.2.2",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@xuqm/rn-common": ">=0.2.2" "@xuqm/rn-common": ">=0.2.2"
@ -3507,9 +3512,26 @@
"react-native": ">=0.76.0" "react-native": ">=0.76.0"
} }
}, },
"packages/license": {
"name": "@xuqm/rn-license",
"version": "0.3.0",
"license": "UNLICENSED",
"dependencies": {
"@xuqm/rn-common": ">=0.2.2"
},
"devDependencies": {
"@types/react-native": "^0.73.0",
"typescript": "^5.9.3"
},
"peerDependencies": {
"@react-native-async-storage/async-storage": ">=1.21.0",
"react-native": ">=0.76.0",
"react-native-quick-crypto": ">=0.7.0"
}
},
"packages/push": { "packages/push": {
"name": "@xuqm/rn-push", "name": "@xuqm/rn-push",
"version": "0.2.1", "version": "0.2.2",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@xuqm/rn-common": ">=0.2.2" "@xuqm/rn-common": ">=0.2.2"
@ -3524,10 +3546,10 @@
}, },
"packages/update": { "packages/update": {
"name": "@xuqm/rn-update", "name": "@xuqm/rn-update",
"version": "0.2.3", "version": "0.3.0",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@xuqm/rn-common": ">=0.2.1" "@xuqm/rn-common": ">=0.2.2"
}, },
"devDependencies": { "devDependencies": {
"@types/react-native": "^0.73.0", "@types/react-native": "^0.73.0",
@ -3540,7 +3562,7 @@
}, },
"packages/xwebview": { "packages/xwebview": {
"name": "@xuqm/rn-xwebview", "name": "@xuqm/rn-xwebview",
"version": "0.2.1", "version": "0.2.2",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@xuqm/rn-common": ">=0.2.2", "@xuqm/rn-common": ">=0.2.2",

查看文件

@ -1,14 +1,18 @@
{ {
"name": "@xuqm/rn-sdk", "name": "@xuqm/rn-sdk",
"version": "0.2.2", "version": "0.3.0",
"description": "XuqmGroup React Native SDK — meta-package (IM, Push, Update, Common)", "description": "XuqmGroup React Native SDK — meta-package (IM, Push, Update, Common)",
"license": "UNLICENSED", "license": "UNLICENSED",
"main": "src/index.ts", "main": "src/index.ts",
"react-native": "src/index.ts", "react-native": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",
"private": true, "private": true,
"files": ["src"], "files": [
"workspaces": ["packages/*"], "src"
],
"workspaces": [
"packages/*"
],
"publishConfig": { "publishConfig": {
"registry": "https://nexus.xuqinmin.com/repository/npm-hosted/" "registry": "https://nexus.xuqinmin.com/repository/npm-hosted/"
}, },

查看文件

@ -1,6 +1,6 @@
{ {
"name": "@xuqm/rn-common", "name": "@xuqm/rn-common",
"version": "0.2.2", "version": "0.3.2",
"description": "XuqmGroup RN SDK — core: init, network, token management", "description": "XuqmGroup RN SDK — core: init, network, token management",
"license": "UNLICENSED", "license": "UNLICENSED",
"main": "src/index.ts", "main": "src/index.ts",
@ -10,7 +10,9 @@
"publishConfig": { "publishConfig": {
"registry": "https://nexus.xuqinmin.com/repository/npm-hosted/" "registry": "https://nexus.xuqinmin.com/repository/npm-hosted/"
}, },
"scripts": { "typecheck": "tsc --noEmit" }, "scripts": {
"typecheck": "tsc --noEmit"
},
"peerDependencies": { "peerDependencies": {
"react-native": ">=0.76.0", "react-native": ">=0.76.0",
"@react-native-async-storage/async-storage": ">=1.21.0" "@react-native-async-storage/async-storage": ">=1.21.0"

查看文件

@ -0,0 +1,46 @@
/**
* SDK
*
* Android SDK ContentProvider
*
* React Native ContentProvider Metro moduleNameMapper
* 1. 宿 src/assets/xuqm/config.xuqmconfig.ts
* 2. 宿 babel.config.js alias: '@xuqm/autoinit-config'
* 3. tryRequire('@xuqm/autoinit-config')
* 4. 宿
*
* alias require
*/
import {isInitialized} from './config'
import {XuqmSDK} from './sdk'
let _autoInitAttempted = false
function tryRequireConfig(): string | null {
try {
// Metro moduleNameMapper 将此路径映射到宿主的配置文件
// eslint-disable-next-line @typescript-eslint/no-require-imports
const mod = require('@xuqm/autoinit-config') as {ENCRYPTED_CONFIG?: string; default?: string}
return mod?.ENCRYPTED_CONFIG ?? mod?.default ?? null
} catch {
return null
}
}
/**
*
* Android SDK runCatching + Log.w
*/
export function tryAutoInit(): void {
if (_autoInitAttempted) return
_autoInitAttempted = true
if (isInitialized()) return
const encrypted = tryRequireConfig()
if (!encrypted) return
XuqmSDK.initWithConfigFile(encrypted, {debug: __DEV__}).catch(() => {
// 静默降级
})
}

查看文件

@ -14,6 +14,15 @@ export interface XuqmConfig {
let _config: XuqmConfig | null = null let _config: XuqmConfig | null = null
let _userId: string | null = null let _userId: string | null = null
export interface XuqmUserInfo {
userId?: string
name?: string
email?: string
phone?: string
}
let _userInfo: XuqmUserInfo | null = null
export function initConfigFromRemote( export function initConfigFromRemote(
options: XuqmInitOptions, options: XuqmInitOptions,
remote: { imWsUrl: string; fileServiceUrl: string; apiUrl: string }, remote: { imWsUrl: string; fileServiceUrl: string; apiUrl: string },
@ -43,3 +52,15 @@ export function getUserId(): string | null {
export function isInitialized(): boolean { export function isInitialized(): boolean {
return _config !== null return _config !== null
} }
export function setUserInfo(info: XuqmUserInfo | null): void {
_userInfo = info
// Sync userId for backward compatibility
if (info?.userId) {
_userId = info.userId
}
}
export function getUserInfo(): XuqmUserInfo | null {
return _userInfo
}

查看文件

@ -0,0 +1,83 @@
/**
*
*
*
* XUQM-LICENSE-V1.{salt}.{iv}.{ciphertext}
* XUQM-CONFIG-V1.{salt}.{iv}.{ciphertext}
*
* 使 react-native-quick-crypto SubtleCryptopeer dependency
*/
const MAGIC_LICENSE = 'XUQM-LICENSE-V1'
const MAGIC_CONFIG = 'XUQM-CONFIG-V1'
const PASSPHRASE_CONFIG = 'xuqm-config-file-v1.2026.internal'
const PASSPHRASE_LICENSE = 'xuqm-license-file-v1.2026.internal'
const PBKDF2_ITERATIONS = 120_000
function getPassphrase(magic: string): string {
return magic === MAGIC_CONFIG ? PASSPHRASE_CONFIG : PASSPHRASE_LICENSE
}
export interface DecryptedConfig {
appKey: string
appName?: string
companyName?: string
baseUrl?: string
serverUrl?: string
issuedAt?: string
expiresAt?: string
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getSubtle(): any {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const qc = require('react-native-quick-crypto') as any
const subtle = qc.subtle ?? qc.default?.subtle
if (!subtle) throw new Error('[XuqmSDK] react-native-quick-crypto not available')
return subtle
}
function base64UrlDecode(s: string): Uint8Array {
const padded = s.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (s.length % 4)) % 4)
const binary = atob(padded)
return Uint8Array.from({ length: binary.length }, (_, i) => binary.charCodeAt(i))
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function deriveKey(salt: Uint8Array, passphrase: string): Promise<any> {
const subtle = getSubtle()
const passphraseKey = await subtle.importKey(
'raw',
new TextEncoder().encode(passphrase),
{ name: 'PBKDF2' },
false,
['deriveKey'],
)
return subtle.deriveKey(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{ name: 'PBKDF2', salt: salt as any, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' },
passphraseKey,
{ name: 'AES-GCM', length: 256 },
false,
['decrypt'],
)
}
export async function decryptConfigFile(content: string): Promise<DecryptedConfig> {
const parts = content.trim().split('.')
const magic = parts[0]
if (parts.length !== 4 || (magic !== MAGIC_CONFIG && magic !== MAGIC_LICENSE)) {
throw new Error('[XuqmSDK] Invalid config/license file format')
}
const salt = base64UrlDecode(parts[1])
const iv = base64UrlDecode(parts[2])
const ciphertext = base64UrlDecode(parts[3])
const passphrase = getPassphrase(magic)
const key = await deriveKey(salt, passphrase)
const subtle = getSubtle()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const plainBuffer = await subtle.decrypt({ name: 'AES-GCM', iv } as any, key, ciphertext as any)
const json = new TextDecoder().decode(plainBuffer)
return JSON.parse(json) as DecryptedConfig
}

42
packages/common/src/crypto-types.d.ts vendored 普通文件
查看文件

@ -0,0 +1,42 @@
/**
* Web Crypto API Hermes
*
* TypeScript lib
* Hermes (React Native 0.71+) crypto.subtle
*/
interface SubtleCrypto {
importKey(
format: string,
keyData: BufferSource,
algorithm: AlgorithmIdentifier,
extractable: boolean,
keyUsages: string[],
): Promise<CryptoKey>
deriveKey(
algorithm: AlgorithmIdentifier,
baseKey: CryptoKey,
derivedKeyType: AlgorithmIdentifier,
extractable: boolean,
keyUsages: string[],
): Promise<CryptoKey>
decrypt(
algorithm: AlgorithmIdentifier,
key: CryptoKey,
data: BufferSource,
): Promise<ArrayBuffer>
}
interface CryptoKey {
readonly type: string
readonly extractable: boolean
readonly algorithm: AlgorithmIdentifier
readonly usages: string[]
}
interface Crypto {
readonly subtle: SubtleCrypto
}
declare var crypto: Crypto
declare function atob(data: string): string

查看文件

@ -1,5 +1,8 @@
// 自动初始化(对齐 Android ContentProvider 模式)
import './autoInit'
export { XuqmSDK } from './sdk' export { XuqmSDK } from './sdk'
export type { XuqmInitOptions, XuqmConfig } from './config' export type { XuqmInitOptions, XuqmConfig, XuqmUserInfo } from './config'
export { getConfig, isInitialized, setUserId, getUserId } from './config' export { getConfig, isInitialized, setUserId, getUserId } from './config'
export { awaitInitialization } from './sdk' export { awaitInitialization } from './sdk'
export { apiRequest, configureHttp, _getToken, _saveToken, _clearToken } from './http' export { apiRequest, configureHttp, _getToken, _saveToken, _clearToken } from './http'

查看文件

@ -1,6 +1,7 @@
import { initConfigFromRemote, isInitialized, type XuqmInitOptions, setUserId as setCommonUserId, getUserId as getCommonUserId } from './config' 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 { DEFAULT_IM_WS_URL, DEFAULT_TENANT_PLATFORM_URL } from './constants'
import { configureHttp } from './http' import { configureHttp } from './http'
import { decryptConfigFile } from './configCrypto'
let _initPromise: Promise<void> | null = null let _initPromise: Promise<void> | null = null
let _initResolve: (() => void) | null = null let _initResolve: (() => void) | null = null
@ -76,7 +77,6 @@ export const XuqmSDK = {
/** /**
* Initialize from a decrypted license file object. * Initialize from a decrypted license file object.
* Use @xuqm/rn-license's decryptLicenseFile() to decrypt the raw file content first.
*/ */
initializeFromLicense(file: { appKey: string; baseUrl?: string; serverUrl?: string }, options?: { debug?: boolean }): void { initializeFromLicense(file: { appKey: string; baseUrl?: string; serverUrl?: string }, options?: { debug?: boolean }): void {
if (isInitialized()) return if (isInitialized()) return
@ -90,6 +90,26 @@ export const XuqmSDK = {
markInitialized() markInitialized()
}, },
/**
* SDK
*
* 宿 .xuqmconfig SDK
* XUQM-CONFIG-V1 XUQM-LICENSE-V1
*
* @param encryptedContent
* @param options.debug
*
* @example
* // common bundle 入口
* import config from './assets/app.xuqmconfig'
* await XuqmSDK.initWithConfigFile(config)
*/
async initWithConfigFile(encryptedContent: string, options?: { debug?: boolean }): Promise<void> {
if (isInitialized()) return
const file = await decryptConfigFile(encryptedContent)
this.initializeFromLicense(file, options)
},
/** /**
* Wait for initialization to complete. * Wait for initialization to complete.
*/ */
@ -105,6 +125,18 @@ export const XuqmSDK = {
getUserId(): string | null { getUserId(): string | null {
return getCommonUserId() return getCommonUserId()
}, },
/**
* Set user info for gray release targeting and license verification.
* Call this after user login.
*/
setUserInfo(info: XuqmUserInfo | null): void {
setCommonUserInfo(info)
},
getUserInfo(): XuqmUserInfo | null {
return getCommonUserInfo()
},
} }
export async function awaitInitialization(): Promise<void> { export async function awaitInitialization(): Promise<void> {

查看文件

@ -1,6 +1,6 @@
{ {
"name": "@xuqm/rn-license", "name": "@xuqm/rn-license",
"version": "0.2.2", "version": "0.3.0",
"description": "XuqmGroup RN SDK — License module (device registration & verification)", "description": "XuqmGroup RN SDK — License module (device registration & verification)",
"license": "UNLICENSED", "license": "UNLICENSED",
"main": "src/index.ts", "main": "src/index.ts",
@ -10,7 +10,9 @@
"publishConfig": { "publishConfig": {
"registry": "https://nexus.xuqinmin.com/repository/npm-hosted/" "registry": "https://nexus.xuqinmin.com/repository/npm-hosted/"
}, },
"scripts": { "typecheck": "tsc --noEmit" }, "scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": { "dependencies": {
"@xuqm/rn-common": ">=0.2.2" "@xuqm/rn-common": ">=0.2.2"
}, },

查看文件

@ -1,6 +1,8 @@
import type { LicenseFile } from './models' import type { LicenseFile } from './models'
const MAGIC = 'XUQM-LICENSE-V1' const MAGIC_LICENSE = 'XUQM-LICENSE-V1'
const MAGIC_CONFIG = 'XUQM-CONFIG-V1'
const SUPPORTED_MAGICS = [MAGIC_LICENSE, MAGIC_CONFIG]
const PASSPHRASE = 'xuqm-license-file-v1.2026.internal' const PASSPHRASE = 'xuqm-license-file-v1.2026.internal'
const PBKDF2_ITERATIONS = 120_000 const PBKDF2_ITERATIONS = 120_000
@ -31,7 +33,8 @@ async function deriveKey(salt: Uint8Array): Promise<CryptoKey> {
['deriveKey'], ['deriveKey'],
) )
return subtle.deriveKey( return subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' }, // eslint-disable-next-line @typescript-eslint/no-explicit-any
{ name: 'PBKDF2', salt: salt as any, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' },
passphraseKey, passphraseKey,
{ name: 'AES-GCM', length: 256 }, { name: 'AES-GCM', length: 256 },
false, false,
@ -39,12 +42,15 @@ async function deriveKey(salt: Uint8Array): Promise<CryptoKey> {
) )
} }
// Decrypts license file content. Format: MAGIC.base64UrlSalt.base64UrlIV.base64UrlCiphertext // Decrypts license/config file content.
// Supported formats:
// XUQM-LICENSE-V1.{salt}.{iv}.{ciphertext}
// XUQM-CONFIG-V1.{salt}.{iv}.{ciphertext}
// Ciphertext includes the 16-byte GCM tag appended (same as Android JCE output). // Ciphertext includes the 16-byte GCM tag appended (same as Android JCE output).
export async function decryptLicenseFile(content: string): Promise<LicenseFile> { export async function decryptLicenseFile(content: string): Promise<LicenseFile> {
const parts = content.trim().split('.') const parts = content.trim().split('.')
if (parts.length !== 4 || parts[0] !== MAGIC) { if (parts.length !== 4 || !SUPPORTED_MAGICS.includes(parts[0])) {
throw new Error('[XuqmLicense] Invalid license file format') throw new Error('[XuqmLicense] Invalid license/config file format')
} }
const salt = base64UrlDecode(parts[1]) const salt = base64UrlDecode(parts[1])
const iv = base64UrlDecode(parts[2]) const iv = base64UrlDecode(parts[2])
@ -53,7 +59,11 @@ export async function decryptLicenseFile(content: string): Promise<LicenseFile>
const key = await deriveKey(salt) const key = await deriveKey(salt)
const subtle = getSubtle() const subtle = getSubtle()
// Web Crypto AES-GCM decrypt expects ciphertext with tag appended — matches Android JCE output // Web Crypto AES-GCM decrypt expects ciphertext with tag appended — matches Android JCE output
const plainBuffer = await subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext) // eslint-disable-next-line @typescript-eslint/no-explicit-any
const plainBuffer = await subtle.decrypt({ name: 'AES-GCM', iv } as any, key, ciphertext as any)
const json = new TextDecoder().decode(plainBuffer) const json = new TextDecoder().decode(plainBuffer)
return JSON.parse(json) as LicenseFile return JSON.parse(json) as LicenseFile
} }
/** Alias for decryptLicenseFile — makes intent clearer when reading .xuqmconfig files. */
export const decryptConfigFile = decryptLicenseFile

查看文件

@ -1,2 +1,3 @@
export { initialize, initializeFromFile, checkLicense, getStatus, getDeviceId, clear } from './license' export { initialize, initializeFromFile, checkLicense, getStatus, getDeviceId, clear } from './license'
export { decryptLicenseFile, decryptConfigFile } from './crypto'
export type { LicenseFile, LicenseUserInfo, LicenseStatus, LicenseResult } from './models' export type { LicenseFile, LicenseUserInfo, LicenseStatus, LicenseResult } from './models'

查看文件

@ -1,6 +1,6 @@
{ {
"name": "@xuqm/rn-update", "name": "@xuqm/rn-update",
"version": "0.2.2", "version": "0.3.0",
"description": "XuqmGroup RN SDK — Update module (App update, RN plugin hot-update)", "description": "XuqmGroup RN SDK — Update module (App update, RN plugin hot-update)",
"license": "UNLICENSED", "license": "UNLICENSED",
"main": "src/index.ts", "main": "src/index.ts",
@ -10,7 +10,9 @@
"publishConfig": { "publishConfig": {
"registry": "https://nexus.xuqinmin.com/repository/npm-hosted/" "registry": "https://nexus.xuqinmin.com/repository/npm-hosted/"
}, },
"scripts": { "typecheck": "tsc --noEmit" }, "scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": { "dependencies": {
"@xuqm/rn-common": ">=0.2.2" "@xuqm/rn-common": ">=0.2.2"
}, },

查看文件

@ -0,0 +1,427 @@
#!/usr/bin/env node
/**
* create-plugin.mjs UpdateSDK 插件脚手架工具
*
* 用法
* node scripts/create-plugin.mjs
* npx @xuqm/rn-update create-plugin
*
* 功能
* 1. 交互式输入插件参数moduleId / title / subtitle / accentColor
* 2. 校验 moduleId 唯一性
* 3. 自动生成完整插件骨架bundle.ts / Screen / plugin.json
* 4. 自动注册到宿主pluginCatalog / debugPlugins / build scripts / metro config
*/
import fs from 'fs';
import path from 'path';
import readline from 'readline';
// ─── 交互工具 ────────────────────────────────────────────────────────────────
function createRL() {
return readline.createInterface({
input: process.stdin,
output: process.stdout,
});
}
function ask(rl, question) {
return new Promise(resolve => {
rl.question(question, answer => resolve(answer.trim()));
});
}
// ─── 项目路径检测 ────────────────────────────────────────────────────────────
function findProjectRoot() {
let dir = process.cwd();
while (dir !== path.dirname(dir)) {
if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
dir = path.dirname(dir);
}
console.error('❌ 未找到 package.json,请在 RN 项目根目录下运行此脚本。');
process.exit(1);
}
// ─── 唯一性校验 ──────────────────────────────────────────────────────────────
function getExistingPluginIds(root) {
const ids = new Set();
// 从 pluginCatalog.ts 读取
const catalogPath = path.join(root, 'src/app/pluginCatalog.ts');
if (fs.existsSync(catalogPath)) {
const content = fs.readFileSync(catalogPath, 'utf-8');
const matches = content.matchAll(/id:\s*'([^']+)'/g);
for (const m of matches) ids.add(m[1]);
}
// 从 plugins 目录读取
const pluginsDir = path.join(root, 'src/plugins');
if (fs.existsSync(pluginsDir)) {
for (const entry of fs.readdirSync(pluginsDir)) {
const full = path.join(pluginsDir, entry);
if (fs.statSync(full).isDirectory()) ids.add(entry);
}
}
return ids;
}
// ─── 文件生成 ────────────────────────────────────────────────────────────────
function pascalCase(str) {
return str
.replace(/[-_]+(\w)/g, (_, c) => c.toUpperCase())
.replace(/^(\w)/, (_, c) => c.toUpperCase());
}
function generateBundleTs(moduleId, title, subtitle, accentColor) {
const screenName = `${pascalCase(moduleId)}Screen`;
return `import { UpdateSDK } from '@xuqm/rn-update';
import { registerPluginFromBridge } from '@plugins/runtimeBridge';
import { ${screenName} } from '@${moduleId}/${screenName}';
// 自动注册插件版本bundle 加载时执行)
UpdateSDK.registerPlugin({ moduleId: '${moduleId}', version: '1.0.0' });
// 注册 UI 组件到宿主运行时
registerPluginFromBridge({
id: '${moduleId}',
title: '${title}',
subtitle: '${subtitle}',
accentColor: '${accentColor}',
Component: ${screenName},
});
`;
}
function generateScreenTs(moduleId) {
const screenName = `${pascalCase(moduleId)}Screen`;
return `import { StyleSheet, Text, View } from 'react-native';
export function ${screenName}() {
return (
<View style={styles.container}>
<Text style={styles.title}>${moduleId}</Text>
<Text style={styles.subtitle}>TODO: 实现${moduleId}功能</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5F7FA' },
title: { fontSize: 24, fontWeight: '700', color: '#17171A' },
subtitle: { fontSize: 14, color: '#666666', marginTop: 8 },
});
`;
}
function generatePluginJson(moduleId) {
return JSON.stringify({ moduleId, version: '1.0.0' }, null, 2) + '\n';
}
// ─── 宿主文件更新 ────────────────────────────────────────────────────────────
function updatePluginCatalog(root, moduleId, title, accentColor) {
const filePath = path.join(root, 'src/app/pluginCatalog.ts');
if (!fs.existsSync(filePath)) {
console.warn('⚠️ src/app/pluginCatalog.ts 不存在,跳过');
return;
}
let content = fs.readFileSync(filePath, 'utf-8');
// 检查是否已存在
if (content.includes(`id: '${moduleId}'`)) {
console.log(` pluginCatalog 已包含 ${moduleId},跳过`);
return;
}
const newEntry = ` {
id: '${moduleId}',
title: '${title}',
summary: '${title}业务模块',
description: '点击后异步加载 ${moduleId} bundle。',
accentColor: '${accentColor}',
},`;
// 在最后一个 ] 之前插入
const lastBracket = content.lastIndexOf('];');
if (lastBracket === -1) {
console.warn('⚠️ pluginCatalog.ts 格式异常,跳过');
return;
}
content =
content.slice(0, lastBracket) + newEntry + '\n' + content.slice(lastBracket);
fs.writeFileSync(filePath, content, 'utf-8');
}
function updateDebugPlugins(root, moduleId) {
const filePath = path.join(root, 'src/bootstrap/debugPlugins.ts');
if (!fs.existsSync(filePath)) {
console.warn('⚠️ src/bootstrap/debugPlugins.ts 不存在,跳过');
return;
}
let content = fs.readFileSync(filePath, 'utf-8');
if (content.includes(`'${moduleId}'`)) {
console.log(` debugPlugins 已包含 ${moduleId},跳过`);
return;
}
const newLoader = `
registerDebugPluginLoader('${moduleId}', async () => {
require('../plugins/${moduleId}/bundle');
});`;
content = content.trimEnd() + newLoader + '\n';
fs.writeFileSync(filePath, content, 'utf-8');
}
function updatePackageJson(root, moduleId) {
const filePath = path.join(root, 'package.json');
const pkg = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
const androidKey = `build:android:${moduleId}`;
const iosKey = `build:ios:${moduleId}`;
if (pkg.scripts[androidKey]) {
console.log(` package.json 已包含 ${androidKey},跳过`);
return;
}
// 添加构建脚本
pkg.scripts[androidKey] =
`mkdir -p bundle/android/${moduleId} && react-native bundle --platform android --dev false --entry-file ./src/plugins/${moduleId}/bundle.ts --bundle-output ./bundle/android/${moduleId}/${moduleId}.android.bundle --assets-dest ./bundle/android/${moduleId} --config metro.split.config.js --reset-cache`;
pkg.scripts[iosKey] =
`mkdir -p bundle/ios/${moduleId} && react-native bundle --platform ios --dev false --entry-file ./src/plugins/${moduleId}/bundle.ts --bundle-output ./bundle/ios/${moduleId}/${moduleId}.ios.bundle --assets-dest ./bundle/ios/${moduleId} --config metro.split.config.js --reset-cache`;
// 更新 plugins 聚合脚本
for (const platform of ['android', 'ios']) {
const key = `build:${platform}:plugins`;
if (pkg.scripts[key] && !pkg.scripts[key].includes(`${platform}:${moduleId}`)) {
pkg.scripts[key] = pkg.scripts[key] + ` && yarn build:${platform}:${moduleId}`;
}
}
// 更新 embedded 脚本
for (const platform of ['android', 'ios']) {
const key = `build:${platform}:embedded`;
if (pkg.scripts[key] && !pkg.scripts[key].includes(`${platform}:${moduleId}`)) {
// 在 prepare-embedded-bundles 之前插入
pkg.scripts[key] = pkg.scripts[key].replace(
/ && node \.\/scripts\/prepare/,
` && yarn build:${platform}:${moduleId} && node ./scripts/prepare`,
);
}
}
fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
}
function updateMetroConfig(root, moduleId) {
const filePath = path.join(root, 'metro.split.config.js');
if (!fs.existsSync(filePath)) {
console.warn('⚠️ metro.split.config.js 不存在,跳过');
return;
}
let content = fs.readFileSync(filePath, 'utf-8');
if (content.includes(`'${moduleId}'`)) {
console.log(` metro.split.config 已包含 ${moduleId},跳过`);
return;
}
// 计算 offsetbuz1=11M, buz2=12M, buzN=(10+N)M
const num = parseInt(moduleId.replace(/\D/g, ''), 10);
const offset = Number.isFinite(num) ? (10 + num) * 1_000_000 : 30_000_000;
// 在 inferOffset 函数中添加新插件的分支
const insertPoint = content.lastIndexOf('return MODULE_OFFSETS');
if (insertPoint === -1) {
console.warn('⚠️ metro.split.config.js 格式异常,跳过');
return;
}
const newBranch = ` if (modulePath.includes(\`\${path.sep}${moduleId}\${path.sep}\`)) {
return ${offset};
}
`;
content = content.slice(0, insertPoint) + newBranch + content.slice(insertPoint);
fs.writeFileSync(filePath, content, 'utf-8');
}
function updateBabelAlias(root, moduleId) {
const filePath = path.join(root, 'babel.config.js');
if (!fs.existsSync(filePath)) return;
let content = fs.readFileSync(filePath, 'utf-8');
if (content.includes(`'@${moduleId}'`)) {
console.log(` babel alias 已包含 @${moduleId},跳过`);
return;
}
// 在 alias 对象最后一个条目后添加
content = content.replace(
/('@utils':\s*'[^']+',?\s*)}/,
`$1'@${moduleId}': './src/plugins/${moduleId}',\n }}`,
);
fs.writeFileSync(filePath, content, 'utf-8');
}
function updateTsconfig(root, moduleId) {
const filePath = path.join(root, 'tsconfig.json');
if (!fs.existsSync(filePath)) return;
let content = fs.readFileSync(filePath, 'utf-8');
if (content.includes(`"@${moduleId}/*"`)) {
console.log(` tsconfig 已包含 @${moduleId},跳过`);
return;
}
content = content.replace(
/("@utils\/\*":\s*\["src\/utils\/\*"\],?)/,
`$1\n "@${moduleId}/*": ["src/plugins/${moduleId}/*"]`,
);
fs.writeFileSync(filePath, content, 'utf-8');
}
// ─── 主流程 ──────────────────────────────────────────────────────────────────
function printUsage() {
console.log('');
console.log('用法:');
console.log(' 交互模式: node scripts/create-plugin.mjs');
console.log(' 命令行: node scripts/create-plugin.mjs <moduleId> [title] [subtitle] [accentColor]');
console.log('');
console.log('示例:');
console.log(' node scripts/create-plugin.mjs buz4 "IM 消息" "即时通讯业务组件" "#E74C3C"');
console.log(' node scripts/create-plugin.mjs buz5');
console.log('');
}
async function main() {
console.log('');
console.log('╔══════════════════════════════════════════╗');
console.log('║ XuqmGroup UpdateSDK — 插件脚手架工具 ║');
console.log('╚══════════════════════════════════════════╝');
const root = findProjectRoot();
const args = process.argv.slice(2);
// ── 解析参数(支持 CLI 和交互两种模式)──
let moduleId, title, subtitle, accentColor;
if (args.length > 0) {
// CLI 模式
[moduleId, title, subtitle, accentColor] = args;
} else {
// 交互模式
const rl = createRL();
try {
moduleId = await ask(rl, '\n插件 ID如 buz4: ');
title = await ask(rl, '插件标题(如 IM 消息): ');
subtitle = await ask(rl, '插件副标题(如 即时通讯业务组件): ');
accentColor = await ask(rl, '主题色(如 #0E84FA: ');
} finally {
rl.close();
}
}
// ── 校验 ──
if (!moduleId || !/^[a-z][a-z0-9]*$/.test(moduleId)) {
console.error('\n❌ moduleId 必须以小写字母开头,只包含小写字母和数字');
printUsage();
process.exit(1);
}
title = title || moduleId;
subtitle = subtitle || `${title}业务组件`;
accentColor = accentColor || '#0E84FA';
// 唯一性校验
const existing = getExistingPluginIds(root);
if (existing.has(moduleId)) {
console.error(`\n❌ moduleId "${moduleId}" 已存在,请使用其他 ID`);
process.exit(1);
}
console.log('');
console.log(`📦 创建插件: ${moduleId}`);
console.log(` 标题: ${title}`);
console.log(` 副标题: ${subtitle}`);
console.log(` 主题色: ${accentColor}`);
console.log('');
// ── 创建目录 ──
const pluginDir = path.join(root, 'src/plugins', moduleId);
fs.mkdirSync(pluginDir, { recursive: true });
// ── 生成文件 ──
const files = [
{
path: path.join(pluginDir, 'bundle.ts'),
content: generateBundleTs(moduleId, title, subtitle, accentColor),
desc: 'bundle 入口(自动注册 UpdateSDK + pluginRuntime',
},
{
path: path.join(pluginDir, `${pascalCase(moduleId)}Screen.tsx`),
content: generateScreenTs(moduleId),
desc: 'Screen 骨架',
},
{
path: path.join(pluginDir, 'plugin.json'),
content: generatePluginJson(moduleId),
desc: '插件元数据',
},
];
for (const file of files) {
fs.writeFileSync(file.path, file.content, 'utf-8');
console.log(`${path.relative(root, file.path)}${file.desc}`);
}
// ── 更新宿主配置 ──
console.log('');
console.log('🔗 更新宿主配置:');
updatePluginCatalog(root, moduleId, title, accentColor);
console.log(' ✅ pluginCatalog.ts');
updateDebugPlugins(root, moduleId);
console.log(' ✅ debugPlugins.ts');
updatePackageJson(root, moduleId);
console.log(' ✅ package.json构建脚本');
updateMetroConfig(root, moduleId);
console.log(' ✅ metro.split.config.js');
updateBabelAlias(root, moduleId);
console.log(' ✅ babel.config.js');
updateTsconfig(root, moduleId);
console.log(' ✅ tsconfig.json');
// ── 完成 ──
console.log('');
console.log('═══════════════════════════════════════════');
console.log(`✅ 插件 ${moduleId} 创建完成!`);
console.log('');
console.log('下一步:');
console.log(` 1. 编辑 src/plugins/${moduleId}/${pascalCase(moduleId)}Screen.tsx 实现业务 UI`);
console.log(` 2. 在 src/plugins/${moduleId}/ 下创建 services/、pages/ 等目录`);
console.log(` 3. yarn validate 确认无错误`);
console.log(` 4. yarn build:android:${moduleId} 构建 Android bundle`);
console.log('═══════════════════════════════════════════');
console.log('');
}
main();

查看文件

@ -4,31 +4,71 @@ import { apiRequest, getConfig, getUserId } from '@xuqm/rn-common'
import { getAppVersionCode, getAppVersionName, _devSetAppVersion } from './NativeVersion' import { getAppVersionCode, getAppVersionName, _devSetAppVersion } from './NativeVersion'
import { awaitInitialization } from '@xuqm/rn-common' import { awaitInitialization } from '@xuqm/rn-common'
// ─── Types ────────────────────────────────────────────────────────────────────
/** 插件注册元数据 */
export interface PluginMeta { export interface PluginMeta {
/** 插件唯一标识,如 'buz1'、'buz2' */
moduleId: string moduleId: string
/** 当前 bundle 版本号 */
version: string version: string
} }
/**
* App
*
* Android SDK UpdateInfo
* SDK UI app
*/
export interface AppUpdateInfo { export interface AppUpdateInfo {
/** 是否需要更新 */
needsUpdate: boolean needsUpdate: boolean
/** 最新版本名,如 '2.1.0' */
versionName?: string versionName?: string
/** 最新版本号(整数) */
versionCode?: number versionCode?: number
/** APK/安装包直接下载地址Android 有此字段时优先下载) */
downloadUrl?: string downloadUrl?: string
/** 更新日志 */
changeLog?: string changeLog?: string
/** 是否强制更新true 时不允许跳过) */
forceUpdate?: boolean forceUpdate?: boolean
/** iOS App Store 地址 */
appStoreUrl?: string appStoreUrl?: string
/** Android 应用商店地址(华为/小米/OPPO 等) */
marketUrl?: string marketUrl?: string
/** 服务端要求登录后才能检查true 时 needsUpdate 通常为 false */
requiresLogin?: boolean
/** SDK 内部标记APK 是否已下载到本地(仅 Android */
alreadyDownloaded?: boolean
/** APK 文件 SHA-256 校验值 */
apkHash?: string | null
} }
export interface RnUpdateInfo { /**
* RN Bundle
*
* = appKey + platform + moduleId
* - appKey
* - platformANDROID / IOS
* - moduleId ID 'buz1'
*/
export interface PluginUpdateInfo {
/** 是否需要更新 */
needsUpdate: boolean needsUpdate: boolean
/** 最新版本号 */
latestVersion: string latestVersion: string
/** bundle 下载地址 */
downloadUrl: string downloadUrl: string
/** bundle 文件 MD5 */
md5: string md5: string
/** 要求的最低 common bundle 版本 */
minCommonVersion: string minCommonVersion: string
/** 更新说明 */
note: string note: string
} }
/** 已缓存的 bundle 元数据 */
export interface CachedRnBundle { export interface CachedRnBundle {
moduleId: string moduleId: string
version: string version: string
@ -37,6 +77,8 @@ export interface CachedRnBundle {
source: string source: string
} }
// ─── Internal ─────────────────────────────────────────────────────────────────
const _pluginRegistry = new Map<string, PluginMeta>() const _pluginRegistry = new Map<string, PluginMeta>()
function bundleCacheKey(moduleId: string) { function bundleCacheKey(moduleId: string) {
@ -59,32 +101,57 @@ function normalizeDownloadUrl(rawUrl?: string): string | undefined {
return rawUrl return rawUrl
} }
// ─── UpdateSDK ────────────────────────────────────────────────────────────────
export const UpdateSDK = { export const UpdateSDK = {
// ── 插件注册 ──────────────────────────────────────────────────────────────
/** /**
* Register a plugin's metadata. Call this at the top of the plugin's bundle entry file. * bundle
*
* = moduleId 'buz1'
* = appKey+ platform+ moduleId
* *
* @example * @example
* // In your plugin's index.ts: * // src/plugins/buz1/bundle.ts
* import meta from './plugin.json' * UpdateSDK.registerPlugin({ moduleId: 'buz1', version: '1.0.0' })
* UpdateSDK.registerPlugin(meta)
*/ */
registerPlugin(meta: PluginMeta): void { registerPlugin(meta: PluginMeta): void {
_pluginRegistry.set(meta.moduleId, meta) _pluginRegistry.set(meta.moduleId, meta)
}, },
/** /**
* For dev/simulator environments where the native XuqmVersionModule is not linked. *
* Do NOT call this in production the native module provides the value automatically.
*/ */
_devSetAppVersion(versionCode: number, versionName?: string): void { getRegisteredPluginVersion(moduleId: string): string | undefined {
_devSetAppVersion(versionCode, versionName) return _pluginRegistry.get(moduleId)?.version
}, },
/** /**
* Check if there is a newer App version available. *
* App version is read automatically from native code (XuqmVersionModule).
*/ */
async checkAppUpdate(): Promise<AppUpdateInfo> { getRegisteredPlugins(): PluginMeta[] {
return Array.from(_pluginRegistry.values())
},
// ── 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<AppUpdateInfo> {
await awaitInitialization() await awaitInitialization()
const config = getConfig() const config = getConfig()
const currentVersionCode = getAppVersionCode() const currentVersionCode = getAppVersionCode()
@ -94,9 +161,9 @@ export const UpdateSDK = {
platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS', platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS',
currentVersionCode: String(currentVersionCode), currentVersionCode: String(currentVersionCode),
} }
if (userId) { if (userId) params.userId = userId
params.userId = userId if (bypassIgnore) params.bypassIgnore = 'true'
}
const result = await apiRequest<AppUpdateInfo>('/api/v1/updates/app/check', { const result = await apiRequest<AppUpdateInfo>('/api/v1/updates/app/check', {
skipAuth: true, skipAuth: true,
params, params,
@ -104,16 +171,29 @@ export const UpdateSDK = {
return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) } return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) }
}, },
/**
*
* iOS 使 appStoreUrlAndroid 使 marketUrl
*/
async openStore(appStoreUrl?: string, marketUrl?: string): Promise<void> { async openStore(appStoreUrl?: string, marketUrl?: string): Promise<void> {
const url = Platform.OS === 'ios' ? appStoreUrl : marketUrl const url = Platform.OS === 'ios' ? appStoreUrl : marketUrl
if (url) await Linking.openURL(url) if (url) await Linking.openURL(url)
}, },
// ── 插件更新 ──────────────────────────────────────────────────────────────
/** /**
* Check if a newer RN bundle exists for the given plugin. *
* The plugin must have been registered via registerPlugin() first. *
* = appKey + platform + moduleId
* registerPlugin()
*
* @param moduleId ID 'buz1'
* @returns latestVersion / downloadUrl / md5
* @throws
*/ */
async checkRnUpdate(moduleId: string): Promise<RnUpdateInfo> { async checkPluginUpdate(moduleId: string): Promise<PluginUpdateInfo> {
await awaitInitialization()
const config = getConfig() const config = getConfig()
const meta = _pluginRegistry.get(moduleId) const meta = _pluginRegistry.get(moduleId)
if (!meta) { if (!meta) {
@ -122,7 +202,7 @@ export const UpdateSDK = {
'Call UpdateSDK.registerPlugin({ moduleId, version }) at bundle load time.', 'Call UpdateSDK.registerPlugin({ moduleId, version }) at bundle load time.',
) )
} }
const result = await apiRequest<RnUpdateInfo>('/api/v1/rn/update/check', { const result = await apiRequest<PluginUpdateInfo>('/api/v1/rn/update/check', {
skipAuth: true, skipAuth: true,
params: { params: {
appKey: config.appKey, appKey: config.appKey,
@ -134,13 +214,19 @@ export const UpdateSDK = {
return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) ?? result.downloadUrl } return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) ?? result.downloadUrl }
}, },
async downloadRnBundle(downloadUrl: string): Promise<string> { /**
* bundle
*/
async downloadPluginBundle(downloadUrl: string): Promise<string> {
const response = await fetch(downloadUrl) const response = await fetch(downloadUrl)
if (!response.ok) throw new Error(`[UpdateSDK] Bundle download failed: ${response.status}`) if (!response.ok) throw new Error(`[UpdateSDK] Bundle download failed: ${response.status}`)
return response.text() return response.text()
}, },
async cacheRnBundle(moduleId: string, version: string, md5: string, source: string): Promise<CachedRnBundle> { /**
* bundle AsyncStorage
*/
async cachePluginBundle(moduleId: string, version: string, md5: string, source: string): Promise<CachedRnBundle> {
const payload: CachedRnBundle = { const payload: CachedRnBundle = {
moduleId, version, md5, source, moduleId, version, md5, source,
downloadedAt: new Date().toISOString(), downloadedAt: new Date().toISOString(),
@ -149,17 +235,41 @@ export const UpdateSDK = {
return payload return payload
}, },
async getCachedRnBundle(moduleId: string): Promise<CachedRnBundle | null> { /**
* bundle
*/
async getCachedPluginBundle(moduleId: string): Promise<CachedRnBundle | null> {
const raw = await AsyncStorage.getItem(bundleCacheKey(moduleId)) const raw = await AsyncStorage.getItem(bundleCacheKey(moduleId))
return raw ? (JSON.parse(raw) as CachedRnBundle) : null return raw ? (JSON.parse(raw) as CachedRnBundle) : null
}, },
/** Returns the currently running version of a registered plugin. */ /**
getRegisteredPluginVersion(moduleId: string): string | undefined { *
return _pluginRegistry.get(moduleId)?.version *
* bundle
* reloadPlugin()
*
* @returns bundle null
*/
async checkAndCachePlugin(moduleId: string): Promise<CachedRnBundle | null> {
const info = await this.checkPluginUpdate(moduleId)
if (!info.needsUpdate) return null
const source = await this.downloadPluginBundle(info.downloadUrl)
return this.cachePluginBundle(moduleId, info.latestVersion, info.md5, source)
}, },
/** Returns the current app versionCode (read from native). */ // ── 版本信息 ──────────────────────────────────────────────────────────────
/** 当前 App versionCode原生读取 */
getAppVersionCode, getAppVersionCode,
/** 当前 App versionName原生读取 */
getAppVersionName, getAppVersionName,
/**
*
*/
_devSetAppVersion(versionCode: number, versionName?: string): void {
_devSetAppVersion(versionCode, versionName)
},
} }

查看文件

@ -1,2 +1,2 @@
export { UpdateSDK } from './UpdateSDK' export { UpdateSDK } from './UpdateSDK'
export type { PluginMeta, AppUpdateInfo, RnUpdateInfo, CachedRnBundle } from './UpdateSDK' export type { PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle } from './UpdateSDK'

查看文件

@ -1,6 +1,6 @@
export { XuqmSDK } from './sdk' export { XuqmSDK } from './sdk'
export type { UnifiedLoginOptions } from './sdk' export type { UnifiedLoginOptions } from './sdk'
export type { XuqmInitOptions, DeviceInfo } from '@xuqm/rn-common' export type { XuqmInitOptions, XuqmUserInfo, DeviceInfo } from '@xuqm/rn-common'
export { getDeviceId, getDeviceInfo, detectPushVendor, setUserId, getUserId } from '@xuqm/rn-common' export { getDeviceId, getDeviceInfo, detectPushVendor, setUserId, getUserId } from '@xuqm/rn-common'
export { ScaledImage } from '@xuqm/rn-common' export { ScaledImage } from '@xuqm/rn-common'
export { apiRequest } from '@xuqm/rn-common' export { apiRequest } from '@xuqm/rn-common'
@ -24,7 +24,9 @@ export type {
export { PushSDK } from '@xuqm/rn-push' export { PushSDK } from '@xuqm/rn-push'
export type { PushVendor } from '@xuqm/rn-push' export type { PushVendor } from '@xuqm/rn-push'
export { UpdateSDK } from '@xuqm/rn-update' export { UpdateSDK } from '@xuqm/rn-update'
export type { PluginMeta, AppUpdateInfo, RnUpdateInfo, CachedRnBundle } from '@xuqm/rn-update' export type { PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle } from '@xuqm/rn-update'
export { decryptLicenseFile, decryptConfigFile, initialize as initializeLicense, initializeFromFile as initializeLicenseFromFile, checkLicense } from '@xuqm/rn-license'
export type { LicenseFile, LicenseUserInfo, LicenseStatus, LicenseResult } from '@xuqm/rn-license'
export { export {
XWebViewControl, XWebViewControl,
XWebViewScreen, XWebViewScreen,

1928
yarn.lock 普通文件

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