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
这个提交包含在:
父节点
af3aa0cd43
当前提交
ab30b28f3d
370
README.md
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
普通文件
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. XuqmSDK(common)
|
||||||
|
|
||||||
|
> `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. UpdateSDK(update)
|
||||||
|
|
||||||
|
> `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. XWebView(xwebview)
|
||||||
|
|
||||||
|
> `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. PushSDK(push)
|
||||||
|
|
||||||
|
> `import { PushSDK } from '@xuqm/rn-push'`
|
||||||
|
|
||||||
|
推送服务 SDK,支持华为、小米、OPPO、vivo、荣耀、FCM(Android)和 APNs(iOS)。
|
||||||
|
|
||||||
|
### 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. ImSDK(im)
|
||||||
|
|
||||||
|
> `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. License(license)
|
||||||
|
|
||||||
|
> `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
普通文件
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
普通文件
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 Native(Metro 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
自动生成的
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",
|
||||||
|
|||||||
10
package.json
10
package.json
@ -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"
|
||||||
|
|||||||
46
packages/common/src/autoInit.ts
普通文件
46
packages/common/src/autoInit.ts
普通文件
@ -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 的 SubtleCrypto(peer 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
普通文件
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算 offset:buz1=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:应用标识(来自配置文件)
|
||||||
|
* - platform:ANDROID / 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 使用 appStoreUrl,Android 使用 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,
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户