diff --git a/docs-site/docs/.vitepress/config.ts b/docs-site/docs/.vitepress/config.ts
index 5696739..2917070 100644
--- a/docs-site/docs/.vitepress/config.ts
+++ b/docs-site/docs/.vitepress/config.ts
@@ -58,11 +58,15 @@ export default defineConfig({
],
'/rn/': [
{ text: '概览', link: '/rn/' },
- { text: '安装配置', link: '/rn/setup' },
+ { text: '安装与自动配置', link: '/rn/setup' },
+ { text: 'Common 基础能力', link: '/rn/common' },
+ { text: '会话与隐私授权', link: '/rn/session' },
+ { text: '版本与插件更新', link: '/rn/update' },
+ { text: 'BugCollect', link: '/rn/bugcollect' },
+ { text: 'XWebView', link: '/rn/xwebview' },
{ text: 'IM 接入', link: '/rn/im' },
- { text: '群聊', link: '/rn/group' },
{ text: '推送接入', link: '/rn/push' },
- { text: '版本管理', link: '/rn/update' },
+ { text: '错误处理与排障', link: '/rn/troubleshooting' },
],
'/vue3/': [
{ text: '概览', link: '/vue3/' },
diff --git a/docs-site/docs/rn/bugcollect.md b/docs-site/docs/rn/bugcollect.md
new file mode 100644
index 0000000..21e8910
--- /dev/null
+++ b/docs-site/docs/rn/bugcollect.md
@@ -0,0 +1,128 @@
+# BugCollect
+
+`@xuqm/rn-bugcollect` 用于采集 JS 异常、原生崩溃、业务面包屑和自定义事件。它共享
+Common 的自动配置与会话,不提供第二套初始化或登录入口。
+
+## 安装
+
+```bash
+pnpm add @xuqm/rn-bugcollect @xuqm/rn-common
+```
+
+完成 [自动配置](./setup) 后无需在 App 入口调用 BugCollect 初始化方法。
+
+## 隐私前置条件
+
+只有宿主明确同步用户授权后才允许采集:
+
+```ts
+import { XuqmSDK } from '@xuqm/rn-common'
+
+await XuqmSDK.setPrivacyConsent(true)
+```
+
+撤回授权:
+
+```ts
+await XuqmSDK.setPrivacyConsent(false)
+```
+
+SDK 不负责展示隐私声明,也不会把一次历史授权永久推断为当前授权。
+
+## 自动采集
+
+满足以下条件时,Release 应用自动启用异常采集:
+
+- 租户平台已开启 BugCollect。
+- 当前配置有效。
+- 用户已同意隐私声明。
+- App 构建没有使用应急关闭选项。
+
+服务关闭、网络失败、限流或上传失败不会阻断宿主业务。服务端明确返回关闭状态后,SDK 会
+停止后续采集,直到获取到新的有效平台配置。
+
+## 手动记录
+
+```ts
+import { BugCollect } from '@xuqm/rn-bugcollect'
+
+BugCollect.addBreadcrumb({
+ type: 'navigation',
+ category: 'order',
+ message: '进入订单详情',
+ data: { orderType: 'outpatient' },
+})
+
+BugCollect.info('开始提交订单')
+BugCollect.warn('接口响应较慢', { elapsedMs: 2200 })
+
+try {
+ await submitOrder()
+} catch (error) {
+ BugCollect.captureError(error, {
+ scene: 'submit-order',
+ })
+}
+```
+
+不要把密码、Token、身份证号、手机号、病历正文或完整请求响应放入 metadata。
+
+## 自定义事件与漏斗
+
+```ts
+BugCollect.event('checkout_started', {
+ source: 'cart',
+})
+
+BugCollect.defineFunnel({
+ id: 'checkout',
+ steps: ['checkout_started', 'payment_started', 'payment_completed'],
+})
+
+const progress = BugCollect.getFunnelProgress('checkout')
+```
+
+事件名应保持稳定,属性只记录分析所需的非敏感枚举和数值。
+
+## 日志级别与环境
+
+```ts
+BugCollect.setLogLevel(__DEV__ ? 'debug' : 'warn')
+BugCollect.setEnvironment(__DEV__ ? 'development' : 'production')
+BugCollect.setSampleRate(0.2)
+```
+
+采样率只影响非致命事件,致命异常不参与客户端采样。线上采样与服务端限流共同生效,宿主
+不需要自行实现重试队列。
+
+## Debug 验证
+
+Debug 默认不自动上传。开发者工具页可以在用户已同意隐私声明后显式执行:
+
+```ts
+await BugCollect.sendTestErrorAndFlush('BugCollect integration test')
+```
+
+该方法用于验证配置和上传链路,不应放在正常业务流程中。
+
+## 主动刷新
+
+```ts
+await BugCollect.flush()
+```
+
+通常不需要主动调用。应用即将退出且确实需要等待当前队列时才使用;上传失败仍应当允许
+宿主继续退出。
+
+## 应急关闭
+
+当 BugCollect 服务不可用且影响生产打包时,宿主可以通过自己的 Release 构建参数关闭本次
+构建的 BugCollect。该设置只用于应急,不写入平台下发配置,也不允许业务页面在运行时
+反向开启平台已关闭的服务。
+
+## 数据与错误边界
+
+- SDK 会对常见敏感字段进行脱敏,但调用方仍须避免主动传入敏感正文。
+- 网络和上传错误不会抛到全局导致 App 崩溃。
+- 未授权、服务关闭和配置不可用时,自定义记录调用安全跳过。
+- `sendTestErrorAndFlush` 是诊断方法,会把错误明确返回给开发者页面。
diff --git a/docs-site/docs/rn/common.md b/docs-site/docs/rn/common.md
new file mode 100644
index 0000000..bf393d0
--- /dev/null
+++ b/docs-site/docs/rn/common.md
@@ -0,0 +1,157 @@
+# Common 基础能力
+
+`@xuqm/rn-common` 是所有 RN 扩展包共享的基础依赖,也可以不接入平台配置、不开启登录而
+独立使用。
+
+## 网络请求
+
+Common-only 工程可以设置自己的业务地址:
+
+```ts
+import {
+ apiRequest,
+ configureHttp,
+ setGlobalApiErrorHandler,
+} from '@xuqm/rn-common'
+
+configureHttp({
+ baseUrl: 'https://api.example.com',
+})
+
+setGlobalApiErrorHandler((report) => {
+ // report 只包含经过脱敏的请求诊断信息
+ console.warn(report)
+})
+
+const profile = await apiRequest<{ name: string }>('/profile', {
+ timeoutMs: 15_000,
+})
+```
+
+安装扩展包并完成自动配置后,扩展模块共享平台地址和当前会话。页面不要重复创建平台
+Axios 实例,也不要逐层传递用户 ID 或 Token。
+
+React 页面需要请求状态和运行时响应校验时,可以使用 `useRequest`、`useApi` 和
+`usePageApi`。这些 Hooks 接收 URL、请求方法、参数和可选的 Zod Schema,具体类型由
+TypeScript 自动提示。
+
+## 文件与下载
+
+```ts
+import {
+ downloadFileToPath,
+ fileExists,
+ openLocalFile,
+ resolveAvailableFilePath,
+} from '@xuqm/rn-common'
+
+const target = await resolveAvailableFilePath('/downloads', 'report.pdf', 'rename')
+const task = downloadFileToPath('https://example.com/report.pdf', target, {
+ onProgress(progress) {
+ console.log(progress.percent)
+ },
+})
+
+await task.result
+if (await fileExists(target)) await openLocalFile(target)
+```
+
+常用能力还包括:
+
+- `ensureDirectory`
+- `readFileAsBase64`
+- `writeBase64File`
+- `deleteFile`
+- `registerAndroidDownloadedFile`
+- `fileNameFromUrl`
+- `parseContentDispositionFileName`
+- `sanitizeFileName`
+- `inferMimeType`
+
+下载任务支持取消,页面卸载时可以调用 `task.cancel()`。
+
+## 日期与过期状态
+
+```ts
+import {
+ expirationStatus,
+ formatDateTime,
+ millisecondsUntil,
+ toTimestamp,
+} from '@xuqm/rn-common'
+
+const status = expirationStatus(endTime)
+if (status === 'expired') {
+ // 已过期
+}
+
+const remaining = millisecondsUntil(endTime)
+const display = formatDateTime(endTime)
+const timestamp = toTimestamp(endTime)
+```
+
+协议要求固定数字格式时使用 `formatNumericDateTime`,不要依赖设备 Locale 的标点顺序。
+
+## 版本判断
+
+```ts
+import {
+ compareVersions,
+ isVersionUpgrade,
+ satisfiesVersion,
+} from '@xuqm/rn-common'
+
+compareVersions('1.2.0', '1.10.0')
+isVersionUpgrade('1.0.0', '1.1.0')
+satisfiesVersion('1.2.3', '>=1.0.0 <2.0.0')
+```
+
+## 设备信息
+
+```ts
+import { getDeviceId, getDeviceInfo, detectPushVendor } from '@xuqm/rn-common'
+
+const deviceId = await getDeviceId()
+const device = await getDeviceInfo()
+const vendor = detectPushVendor(device.brand)
+```
+
+## 公共提示
+
+```ts
+import { showAlert, showConfirm, showToast } from '@xuqm/rn-common'
+
+showToast('保存成功')
+await showAlert({ title: '提示', message: '操作已完成' })
+showConfirm({
+ title: '确认删除',
+ message: '删除后无法恢复',
+ onConfirm() {
+ // 执行删除
+ },
+})
+```
+
+宿主可在应用入口统一调用 `configureToast` 对接自己的提示组件,业务页面不应复制多套
+Alert/Toast 样式。
+
+## 错误处理
+
+```ts
+import { XuqmError } from '@xuqm/rn-common'
+
+try {
+ await someOperation()
+} catch (error) {
+ if (error instanceof XuqmError) {
+ switch (error.code) {
+ case 'XUQM_NOT_READY':
+ case 'XUQM_SERVICE_DISABLED':
+ // 非阻断提示或跳过功能
+ break
+ }
+ }
+}
+```
+
+不要根据英文错误文案判断业务分支,统一使用结构化错误码。
diff --git a/docs-site/docs/rn/group.md b/docs-site/docs/rn/group.md
deleted file mode 100644
index 3af8e46..0000000
--- a/docs-site/docs/rn/group.md
+++ /dev/null
@@ -1,109 +0,0 @@
-# React Native 群聊
-
-基于 `@xuqm/rn-im` 模块实现群组相关功能。
-
----
-
-## 创建群聊
-
-```ts
-import { ImSDK } from '@xuqm/rn-im'
-
-const group = await ImSDK.createGroup('项目讨论', ['user_002', 'user_003'])
-// group.id — 群 ID
-// group.name — 群名称
-// group.creatorId — 创建者
-```
-
-> `createGroup` 第二个参数为初始成员列表,创建者自动加入。
-
----
-
-## 邀请成员
-
-```ts
-// 添加单个成员
-await ImSDK.addGroupMember('group_xxx', 'user_004')
-
-// 批量添加成员
-await ImSDK.batchAddGroupMembers('group_xxx', ['user_004', 'user_005'])
-```
-
----
-
-## 发送群消息
-
-```ts
-const msg = await ImSDK.sendMessage(
- 'group_xxx', // toId
- 'GROUP', // chatType
- 'TEXT', // msgType
- '大家好!' // content
-)
-```
-
-发送多媒体群消息:
-
-```ts
-// 图片
-await ImSDK.sendImageMessage('group_xxx', 'GROUP', '/path/to/image.jpg', 800, 600)
-```
-
----
-
-## 群成员管理
-
-```ts
-// 移除成员
-await ImSDK.removeGroupMember('group_xxx', 'user_004')
-
-// 批量移除
-await ImSDK.batchRemoveGroupMembers('group_xxx', ['user_004', 'user_005'])
-
-// 退出群聊
-await ImSDK.leaveGroup('group_xxx')
-
-// 设置管理员角色(示例)
-await ImSDK.setGroupRole('group_xxx', 'user_004', 'ADMIN')
-
-// 禁言成员(示例)
-await ImSDK.muteGroupMember('group_xxx', 'user_004', 60)
-
-// 转让群主(示例)
-await ImSDK.transferGroupOwner('group_xxx', 'user_002')
-
-// 解散群聊(示例)
-await ImSDK.dismissGroup('group_xxx')
-```
-
----
-
-## 群信息查询
-
-```ts
-// 群列表(仅返回当前用户所在的群)
-const groups = await ImSDK.listGroups()
-
-// 群详情
-const group = await ImSDK.getGroupInfo('group_xxx')
-
-// 群成员
-const members = await ImSDK.listGroupMembers('group_xxx')
-
-// 群历史消息
-const history = await ImSDK.fetchGroupHistory('group_xxx', page, size)
-```
-
----
-
-## 群类型
-
-创建群时可指定 `groupType`:
-
-| 类型 | 说明 |
-|------|------|
-| `WORK` | 工作群(默认)|
-| `PUBLIC` | 公开群 |
-| `PRIVATE` | 私有群 |
-
-[→ 返回 RN IM 接入文档](./im)
diff --git a/docs-site/docs/rn/im.md b/docs-site/docs/rn/im.md
index dd7db74..0866d75 100644
--- a/docs-site/docs/rn/im.md
+++ b/docs-site/docs/rn/im.md
@@ -1,199 +1,232 @@
-# React Native IM 接入
+# IM 接入
-基于 `@xuqm/rn-im` 模块实现即时通讯功能,使用 WatermelonDB 进行本地消息存储。
+`@xuqm/rn-im` 提供单聊、群聊、会话、好友关系、黑名单和离线消息能力。宿主仍然只通过
+Common 同步一次登录状态。
----
+## 安装
-## 初始化与登录
-
-### 初始化
-
-```ts
-import { XuqmSDK } from '@xuqm/rn-common'
-
-await XuqmSDK.initialize({
- appKey: 'your_app_key',
- logLevel: __DEV__ ? 'debug' : 'warn',
-})
+```bash
+pnpm add @xuqm/rn-common @xuqm/rn-im @nozbe/watermelondb
```
-### 登录
+## 登录与连接
```ts
import { XuqmSDK } from '@xuqm/rn-common'
await XuqmSDK.login({
- userId: 'user_001',
- userSig: 'your_user_sig_jwt',
+ userId: 'user-001',
+ accessToken: 'host-access-token',
+ userSig: 'im-user-signature',
})
```
-> 如果集成了 `rn-im`,`XuqmSDK.login` 会自动触发 `ImSDK` 连接,业务侧无需单独调用 `ImSDK.login`。
+安装 IM 后,公共会话会自动建立连接。不要再调用第二套 IM 登录方法。
----
-
-## 消息收发
-
-### 监听实时消息
+连接状态:
```ts
import { ImSDK } from '@xuqm/rn-im'
-ImSDK.addEventListener('message', (msg) => {
- console.log('收到消息:', msg.msgType, msg.content)
-})
-
-ImSDK.addEventListener('read', (msg) => {
- console.log('已读回执:', msg.id)
-})
-
-ImSDK.addEventListener('revoke', (data) => {
- console.log('消息被撤回:', data.msgId)
-})
-```
-
-### 发送文本消息
-
-```ts
-const msg = await ImSDK.sendMessage(
- 'user_002', // toId
- 'SINGLE', // chatType: 'SINGLE' | 'GROUP'
- 'TEXT', // msgType
- 'Hello!' // content
-)
-```
-
-### 发送图片
-
-```ts
-const msg = await ImSDK.sendImageMessage(
- 'user_002',
- 'SINGLE',
- '/path/to/image.jpg', // 本地 URI
- 800, // 宽
- 600 // 高
-)
-```
-
-### 获取历史消息
-
-```ts
-// 单聊历史
-const history = await ImSDK.fetchHistory('user_002', page, size)
-
-// 群历史
-const groupHistory = await ImSDK.fetchGroupHistory('group_xxx', page, size)
-```
-
----
-
-## WatermelonDB 本地存储
-
-`rn-im` 使用 WatermelonDB(SQLite)存储本地消息,按 `appKey + userId` 自动隔离。
-
-```ts
-import { ImDatabase } from '@xuqm/rn-im'
-
-// 消息搜索(本地)
-const params: MessageSearchParams = {
- keyword: '会议',
- toId: 'user_002',
- chatType: 'SINGLE',
- msgTypes: ['TEXT'],
- limit: 20,
+if (!ImSDK.isConnected()) {
+ await ImSDK.reconnect()
}
-const results = await ImSDK.searchMessages(params)
```
----
-
-## 群聊
+## 事件监听
```ts
-// 创建群
-const group = await ImSDK.createGroup('项目讨论', ['user_002', 'user_003'])
+const listener = {
+ onConnected() {
+ console.log('IM 已连接')
+ },
+ onDisconnected(reason?: string) {
+ console.log('IM 已断开', reason)
+ },
+ onMessage(message) {
+ appendMessage(message)
+ },
+ onGroupMessage(message) {
+ appendGroupMessage(message)
+ },
+ onRead(message) {
+ markMessageRead(message.id)
+ },
+ onRevoke({ msgId }) {
+ markMessageRevoked(msgId)
+ },
+ onError(error) {
+ console.warn(error)
+ },
+}
-// 群列表
-const groups = await ImSDK.listGroups()
+ImSDK.addListener(listener)
-// 添加成员
-await ImSDK.addGroupMember('group_xxx', 'user_004')
-
-// 移除成员
-await ImSDK.removeGroupMember('group_xxx', 'user_004')
-
-// 退出群聊
-await ImSDK.leaveGroup('group_xxx')
+// 页面或服务销毁时
+ImSDK.removeListener(listener)
```
-详见 [RN 群聊文档 →](./group)
+同一个 listener 对象用于添加和移除。
----
+## 发送消息
+
+文本:
+
+```ts
+const message = await ImSDK.sendTextMessage(
+ 'user-002',
+ 'SINGLE',
+ '你好',
+)
+```
+
+群聊:
+
+```ts
+await ImSDK.sendTextMessage('group-001', 'GROUP', '大家好')
+```
+
+图片:
+
+```ts
+await ImSDK.sendImageMessage(
+ 'user-002',
+ 'SINGLE',
+ imageUri,
+ imageWidth,
+ imageHeight,
+)
+```
+
+文件、音频和视频使用对应方法:
+
+```ts
+await ImSDK.sendFileMessage(
+ 'user-002',
+ 'SINGLE',
+ fileUri,
+ 'report.pdf',
+ fileSize,
+)
+await ImSDK.sendAudioMessage('user-002', 'SINGLE', audioUri, durationSeconds)
+await ImSDK.sendVideoMessage(
+ 'user-002',
+ 'SINGLE',
+ videoUri,
+ thumbnailUri,
+ durationSeconds,
+)
+```
+
+自定义、位置、富文本、通知、引用、合并转发和音视频通话信令也有对应的类型化方法。消息
+业务内容由双方约定,不要在普通文本字段中拼接不可验证的脚本。
+
+撤回与编辑:
+
+```ts
+await ImSDK.revokeMessage(message.id)
+await ImSDK.editMessage(message.id, '修改后的内容')
+```
+
+## 历史和离线消息
+
+```ts
+const history = await ImSDK.fetchHistory(
+ 'user-002',
+ 0,
+ 20,
+ 'SINGLE',
+)
+
+const filtered = await ImSDK.fetchHistoryWithFilters('user-002', {
+ keyword: '会议',
+ msgType: 'TEXT',
+ startTime: '2026-07-01 00:00:00',
+ endTime: '2026-07-31 23:59:59',
+ page: 0,
+ size: 20,
+})
+
+const offlineCount = await ImSDK.offlineMessageCount()
+const offlineMessages = await ImSDK.syncOfflineMessages(100)
+```
+
+SDK 负责当前登录用户的本地消息缓存与同步,宿主不需要依赖其内部存储结构。
## 会话列表
```ts
-// 订阅会话变化
-const unsub = ImSDK.subscribeConversations((conversations) => {
- console.log(conversations)
+const conversations = await ImSDK.listConversations()
+
+const unsubscribe = ImSDK.subscribeConversations((next) => {
+ setConversations(next)
})
-// 置顶
-await ImSDK.setConversationPinned('user_002', 'SINGLE', true)
+await ImSDK.markRead('user-002', 'SINGLE')
+await ImSDK.setConversationPinned('user-002', 'SINGLE', true)
+await ImSDK.setConversationMuted('user-002', 'SINGLE', true)
+await ImSDK.setDraft('user-002', 'SINGLE', '稍后继续')
-// 免打扰
-await ImSDK.setConversationMuted('group_xxx', 'GROUP', true)
-
-// 标记已读
-await ImSDK.markRead('user_002')
+unsubscribe()
```
----
+还可以隐藏、删除会话或将会话加入自定义分组。
-## 离线消息同步
+## 群聊
```ts
-// 查询离线消息数量(示例)
-const count = await ImSDK.offlineMessageCount()
+const group = await ImSDK.createGroup(
+ '项目讨论',
+ ['user-002', 'user-003'],
+ 'WORK',
+)
-// 同步离线消息(示例)
-const messages = await ImSDK.syncOfflineMessages()
+const groups = await ImSDK.listGroups()
+const detail = await ImSDK.getGroupInfo(group.id)
+const members = await ImSDK.listGroupMembers(group.id)
+
+await ImSDK.addGroupMember(group.id, 'user-004')
+await ImSDK.removeGroupMember(group.id, 'user-004')
+await ImSDK.leaveGroup(group.id)
```
----
+管理员能力包括群资料、角色、禁言、群主转让、入群申请和解散群聊。调用失败时使用服务端
+返回的权限错误,不要仅靠前端角色隐藏来替代服务端校验。
-## 断开连接
+## 好友和黑名单
```ts
-ImSDK.disconnect()
+const friends = await ImSDK.listFriends()
+await ImSDK.addFriend('user-002')
+await ImSDK.removeFriend('user-002')
+
+const incoming = await ImSDK.listFriendRequests('incoming')
+const request = await ImSDK.sendFriendRequest('user-003', '我是张医生')
+await ImSDK.acceptFriendRequest(request.id)
+
+await ImSDK.addToBlacklist('user-004')
+const blocked = await ImSDK.checkBlacklist('user-004')
+await ImSDK.removeFromBlacklist('user-004')
```
----
-
-## 类型参考
+## 用户与消息搜索
```ts
-interface ImMessage {
- id: string
- fromId: string
- toId: string
- chatType: 'SINGLE' | 'GROUP'
- msgType: 'TEXT' | 'IMAGE' | 'AUDIO' | 'VIDEO' | 'FILE' |
- 'LOCATION' | 'NOTIFY' | 'CUSTOM' | 'RICH_TEXT' |
- 'CALL_AUDIO' | 'CALL_VIDEO' | 'FORWARD' | 'REVOKED'
- content: string
- status: 'SENDING' | 'SENT' | 'DELIVERED' | 'READ' | 'FAILED' | 'REVOKED'
- createdAt: number
-}
-
-interface ConversationData {
- targetId: string
- chatType: 'SINGLE' | 'GROUP'
- lastMsgContent: string
- lastMsgType: string
- lastMsgTime: number
- unreadCount: number
- isMuted: boolean
- isPinned: boolean
-}
+const users = await ImSDK.searchUsers('张医生')
+const groups = await ImSDK.searchGroups('项目')
+const messages = await ImSDK.searchMessages({
+ keyword: '会议',
+ chatType: 'SINGLE',
+ toId: 'user-002',
+ msgTypes: ['TEXT'],
+ limit: 20,
+})
```
+
+## 登出
+
+```ts
+await XuqmSDK.logout()
+```
+
+公共登出会自动断开 IM。`ImSDK.disconnect()` 仅用于 IM 专用诊断或明确的临时断开场景。
diff --git a/docs-site/docs/rn/index.md b/docs-site/docs/rn/index.md
index 9222159..1d0a8e8 100644
--- a/docs-site/docs/rn/index.md
+++ b/docs-site/docs/rn/index.md
@@ -1,288 +1,88 @@
-# React Native SDK 接入指南
+# React Native SDK
-**包名**:`@xuqm/rn-sdk` · **版本**:0.2.x(内部基础包,业务方不直接引用)
+XuqmGroup React Native SDK 由一个可独立使用的基础包和多个按需安装的扩展包组成。宿主只需
+维护一份平台下发的配置文件,并在登录成功后同步一次会话;已安装的扩展包会共享这份配置
+和会话状态。
-> **注意**:`rn-sdk` 作为内部基础包存在,业务方正常接入时使用 `@xuqm/rn-common` 和各业务模块即可。
+## 选择需要的模块
----
+| 包 | 用途 | 是否需要平台配置 |
+| --- | --- | --- |
+| `@xuqm/rn-common` | 网络、文件、时间、设备、通用 UI 与 API Hooks | 独立使用时不需要 |
+| `@xuqm/rn-update` | Android 整包更新与 RN 插件更新 | 需要 |
+| `@xuqm/rn-bugcollect` | JS 异常、原生崩溃、面包屑和自定义事件 | 需要 |
+| `@xuqm/rn-xwebview` | 独立 WebView 页面、导航栏、权限、下载和宿主 Bridge | 使用页面能力时需要 |
+| `@xuqm/rn-im` | 单聊、群聊、会话、好友和离线消息 | 需要,并需要登录 |
+| `@xuqm/rn-push` | 厂商推送绑定、离线推送和免打扰 | 需要,并需要登录 |
-## 功能模块
+只安装实际使用的模块。扩展包会声明对 `rn-common` 的依赖范围,不需要宿主再创建第二套
+初始化器、网络层或用户 Store。
-| 包 | 功能 |
-|----|------|
-| `@xuqm/rn-common` | 初始化、网络、设备信息,可独立使用 |
-| `@xuqm/rn-im` | 单聊、群聊、消息收发、本地 DB(WatermelonDB)|
-| `@xuqm/rn-push` | 推送设备 Token 上报 |
-| `@xuqm/rn-update` | App 版本检查、RN Bundle 热更新 |
-| `@xuqm/rn-sdk` | 内部基础包,随 IM / Push / Update 自动安装,不建议业务方直接引用 |
+## 两种接入方式
----
+### 只使用 Common
-## 安装
+不放置 `.xuqmconfig`,也不包装 Metro:
-在项目根目录创建 `.npmrc`:
+```ts
+import { configureHttp, apiRequest, expirationStatus } from '@xuqm/rn-common'
-```
-@xuqm:registry=https://nexus.xuqinmin.com/repository/npm-hosted/
+configureHttp({ baseUrl: 'https://api.example.com' })
+const health = await apiRequest('/health', { skipAuth: true })
+const certState = expirationStatus('2027-12-31T23:59:59+08:00')
```
-只使用基础能力时,直接安装 `rn-common`,不会带入 IM / Push / Update:
+这种模式不需要初始化和登录。
-```bash
-yarn add @xuqm/rn-common
+### 使用任意扩展包
+
+1. 从租户平台下载当前应用的 `.xuqmconfig`。
+2. 将唯一一份配置放到 `src/assets/config/`。
+3. 使用 SDK 提供的 Metro 包装器。
+4. 宿主隐私声明获得用户同意后调用一次 `setPrivacyConsent(true)`。
+5. 宿主登录成功后调用一次 `XuqmSDK.login(...)`。
+
+SDK 不要求页面逐层传递 AppKey、平台地址、用户 ID 或登录凭证。
+
+## 最小示例
+
+```js
+// metro.config.js
+const { getDefaultConfig } = require('@react-native/metro-config')
+const { withXuqmConfig } = require('@xuqm/rn-common/metro')
+
+module.exports = withXuqmConfig(getDefaultConfig(__dirname))
```
-按需安装模块时,`rn-im` / `rn-push` / `rn-update` 都会自动带上 `rn-common` 和 `rn-sdk`:
-
-```bash
-yarn add @xuqm/rn-common @xuqm/rn-im
-```
-
-`rn-sdk` 不作为业务方直接安装入口;它会随着 `rn-im` / `rn-push` / `rn-update` 自动进入依赖树。
-
----
-
-## 快速接入(当前 v0.2.x)
-
-### 1. 初始化
-
-初始化只需传入 `appKey`,平台地址由 SDK 内置,开发者无需额外配置。
-
```ts
import { XuqmSDK } from '@xuqm/rn-common'
-// App 入口(如 App.tsx 的顶层)
-await XuqmSDK.initialize({
- appKey: 'your_app_key',
- logLevel: __DEV__ ? 'debug' : 'warn',
-})
-```
+await XuqmSDK.setPrivacyConsent(true)
-> SDK 内部自动处理服务器地址、IM 实时连接、文件服务等配置,开发者无需关心。
-
-### 2. 统一登录
-
-```ts
-import { XuqmSDK } from '@xuqm/rn-common'
-import { ImSDK } from '@xuqm/rn-im'
-
-// 登录只需要 userId + userSig
-// nickname / avatar 不再通过登录接口传入
await XuqmSDK.login({
- userId: 'user_001',
- userSig: 'your_user_sig_jwt',
+ userId: 'user-001',
+ accessToken: 'host-access-token',
+ // 仅使用 IM 时传入:
+ userSig: 'im-user-signature',
})
-
-// 如果集成了 rn-im,XuqmSDK.login 会自动触发 ImSDK 连接
-// 业务侧无需单独调用 ImSDK.login
```
-### 3. 消息收发
+## 默认行为
-```ts
-// 监听实时消息
-ImSDK.addEventListener('message', (msg) => {
- console.log('收到消息:', msg.msgType, msg.content)
-})
+- 配置由构建工具自动发现,运行时没有手动初始化 API。
+- 配置错误会阻断构建,避免生成安装后才无法初始化的包。
+- 扩展服务初始化失败不会阻断宿主应用自身启动;具体操作会返回结构化状态或错误。
+- Update、BugCollect 等服务是否启用由租户平台控制。
+- Debug 不会自动检查更新,也不会自动上报 BugCollect。
+- Common 独立模式不会访问 XuqmGroup 服务。
-// 发送文本消息
-const msg = await ImSDK.sendMessage(
- 'user_002', // toId
- 'SINGLE', // chatType: 'SINGLE' | 'GROUP'
- 'TEXT', // msgType
- 'Hello!' // content
-)
+## 下一步
-// 发送图片(自动上传到 file-service)
-const msg = await ImSDK.sendImageMessage(
- 'user_002', 'SINGLE',
- '/path/to/image.jpg', // 本地 URI
- 800, 600 // 宽高(可选)
-)
-
-// 获取历史消息(单聊)
-const history = await ImSDK.fetchHistory('user_002', page, size)
-
-// 获取群历史消息
-const groupHistory = await ImSDK.fetchGroupHistory('group_xxx', page, size)
-```
-
-### 4. 会话列表
-
-```ts
-// 订阅会话变化
-const unsub = ImSDK.subscribeConversations((conversations) => {
- // conversations: ConversationData[]
- console.log(conversations)
-})
-
-// 置顶会话
-await ImSDK.setConversationPinned('user_002', 'SINGLE', true)
-
-// 免打扰
-await ImSDK.setConversationMuted('group_xxx', 'GROUP', true)
-
-// 标记已读
-await ImSDK.markRead('user_002')
-```
-
-### 5. 群组管理
-
-```ts
-// 创建群组
-const group = await ImSDK.createGroup('项目讨论', ['user_002', 'user_003'])
-
-// 群组列表(仅返回当前用户所在的群)
-const groups = await ImSDK.listGroups()
-
-// 添加成员
-await ImSDK.addGroupMember('group_xxx', 'user_004')
-
-// 移除成员(需要管理员权限)
-await ImSDK.removeGroupMember('group_xxx', 'user_004')
-
-// 退出群聊
-await ImSDK.leaveGroup('group_xxx')
-```
-
-### 6. 好友关系
-
-```ts
-// 好友列表
-const friends = await ImSDK.listFriends()
-
-// 添加好友(双向)
-await ImSDK.addFriend('user_002')
-
-// 删除好友(双向)
-await ImSDK.removeFriend('user_002')
-```
-
-### 7. 消息搜索(本地)
-
-```ts
-import { ImSDK } from '@xuqm/rn-im'
-import type { MessageSearchParams } from '@xuqm/rn-im'
-
-const params: MessageSearchParams = {
- keyword: '会议', // 关键词搜索
- toId: 'user_002', // 限定会话(可选)
- chatType: 'SINGLE', // 限定类型(可选)
- msgTypes: ['TEXT'], // 限定消息类型(可选)
- limit: 20,
-}
-const results = await ImSDK.searchMessages(params)
-```
-
-### 8. 推送 SDK
-
-登录成功后 SDK **自动**注册设备 Token,业务侧无需手动调用。
-
-如需自定义 Push 行为,可引入 `@xuqm/rn-push`:
-
-```ts
-import { PushSDK } from '@xuqm/rn-push'
-
-// 手动设置接收推送开关
-await PushSDK.setReceivePush(false)
-```
-
-### 9. 版本更新 SDK
-
-```ts
-import { UpdateSDK } from '@xuqm/rn-update'
-
-// 检查 App 原生更新
-const appUpdate = await UpdateSDK.checkAppUpdate()
-if (appUpdate?.needsUpdate) {
- console.log('新版本:', appUpdate.versionName)
- // Linking.openURL(appUpdate.downloadUrl)
-}
-
-// 检查 RN Bundle 热更新
-const rnUpdate = await UpdateSDK.checkRnUpdate('your_module_id')
-if (rnUpdate?.needsUpdate) {
- await UpdateSDK.downloadAndApplyBundle(rnUpdate)
-}
-```
-
-### 10. 断开连接
-
-```ts
-ImSDK.disconnect()
-```
-
----
-
-## TypeScript 类型参考
-
-```ts
-// 消息
-interface ImMessage {
- id: string
- fromId: string
- toId: string
- chatType: 'SINGLE' | 'GROUP'
- msgType: 'TEXT' | 'IMAGE' | 'AUDIO' | 'VIDEO' | 'FILE' |
- 'LOCATION' | 'NOTIFY' | 'CUSTOM' | 'RICH_TEXT' |
- 'CALL_AUDIO' | 'CALL_VIDEO' | 'FORWARD' | 'REVOKED'
- content: string // JSON 字符串,结构按 msgType 不同
- status: 'SENDING' | 'SENT' | 'DELIVERED' | 'READ' | 'FAILED' | 'REVOKED'
- createdAt: number // Unix 毫秒
-}
-
-// 会话
-interface ConversationData {
- targetId: string
- chatType: 'SINGLE' | 'GROUP'
- lastMsgContent: string
- lastMsgType: string
- lastMsgTime: number
- unreadCount: number
- isMuted: boolean
- isPinned: boolean
-}
-
-// 群组
-interface ImGroup {
- id: string
- name: string
- creatorId: string
- memberIds: string // JSON 数组字符串
- adminIds: string
- createdAt: number
-}
-```
-
----
-
-## 当前接入
-
-当前接入方式如下:
-
-```ts
-// 初始化
-await XuqmSDK.initialize({ appKey: 'xxx' }) // 平台地址内置,无需传入
-
-// IM 登录(UserSig 由业务服务端签发)
-const userSig = await yourServer.getUserSig(userId)
-await ImSDK.login(userId, userSig)
-
-// dbName 自动由 appKey + userId 派生,无需传入
-```
-
----
-
-## 常见问题
-
-**Q: 如何获取 appKey?**
-在 [平台控制台](https://dev.xuqinmin.com) 注册账号后,创建应用即可获得 AppKey。
-
-**Q: 为什么不需要传平台地址参数?**
-XuqmGroup 是托管平台,服务地址统一管理,与腾讯云 IM 等平台的设计一致,开发者只需关心业务逻辑。
-
-**Q: UserSig 是什么?**
-UserSig 是您的业务服务端用 AppSecret 为用户签发的安全凭证。当前项目的 IM 登录不做过期功能,只校验 `userId + UserSig` 是否匹配。AppSecret 绝不下发到客户端。
-
-**Q: 本地消息存储在哪里?**
-使用 WatermelonDB(SQLite),按 `appKey + userId` 自动隔离,多账号切换安全。
+- [安装与自动配置](./setup)
+- [Common 基础能力](./common)
+- [会话与隐私授权](./session)
+- [版本与插件更新](./update)
+- [BugCollect](./bugcollect)
+- [XWebView](./xwebview)
+- [IM 接入](./im)
+- [推送接入](./push)
diff --git a/docs-site/docs/rn/push.md b/docs-site/docs/rn/push.md
index 70ac0d0..549a441 100644
--- a/docs-site/docs/rn/push.md
+++ b/docs-site/docs/rn/push.md
@@ -1,138 +1,80 @@
-# React Native 推送接入指南
+# 推送接入
-**包名**:`@xuqm/rn-push` · **支持**:华为、小米、OPPO、vivo、荣耀、APNs(iOS)
+`@xuqm/rn-push` 负责把宿主登录用户同步给原生推送模块,并提供离线推送开关与免打扰
+设置。厂商识别、Token 获取和绑定由原生实现完成,RN 页面不接收或上传厂商 Token。
----
-
-## 1. 安装
+## 安装
```bash
-yarn add @xuqm/rn-push
+pnpm add @xuqm/rn-common @xuqm/rn-push
```
-iOS 需要执行 `pod install`:
+Android 原生推送依赖按租户平台为当前应用生成的配置接入。宿主只启用实际使用的厂商
+渠道,不要把其他项目的厂商 App ID 或密钥复制过来。
-```bash
-cd ios && pod install
-```
+## 登录后自动绑定
----
-
-## 2. Android 厂商推送集成
-
-各厂商推送 SDK 需在原生 Android 层集成。在 `android/app/build.gradle` 中按需添加:
-
-```gradle
-dependencies {
- // 华为 HMS Push
- implementation 'com.huawei.hms:push:6.9.0.300'
- // 小米 Push
- implementation 'com.xiaomi.mipush:sdk:5.0.6'
- // OPPO Push
- implementation 'com.heytap.msp:push:3.5.0'
- // vivo Push
- implementation 'com.vivo.push:sdk:3.0.0.4_484'
- // 荣耀 Push
- implementation 'com.hihonor.mcs:push:7.0.41.301'
-}
-```
-
----
-
-## 3. 请求原生推送权限并注册
-
-```ts
-import { PushSDK } from '@xuqm/rn-push'
-
-// 触发原生推送注册(Android 请求厂商 token;iOS 请求 APNs 权限)
-await PushSDK.requestNativeRegistration()
-```
-
----
-
-## 4. 监听推送 Token
-
-```ts
-import { PushSDK } from '@xuqm/rn-push'
-
-// 在 App 启动时监听 token 回调,并向服务端注册
-const unsubscribe = PushSDK.onPushToken(async (token, vendor) => {
- console.log('获取到 Token:', token, '厂商:', vendor)
- // 登录后调用注册接口
- await PushSDK.setDeviceToken(token, vendor as PushVendor)
-})
-
-// 在组件卸载时取消监听
-unsubscribe()
-```
-
----
-
-## 5. 手动注册 Token
-
-```ts
-import { PushSDK } from '@xuqm/rn-push'
-import type { PushVendor } from '@xuqm/rn-push'
-
-// 登录成功后,将 token 注册到服务端
-await PushSDK.registerToken('user_001', 'device_token_here', 'HUAWEI')
-```
-
----
-
-## 6. 登出时注销 Token
-
-```ts
-await PushSDK.unregisterToken('user_001')
-// 或简写
-await PushSDK.logout('user_001')
-```
-
----
-
-## 7. 多模块统一登录
-
-Push 模块与 IM、Update 模块共享同一套登录态:
+宿主仍然只调用公共登录入口:
```ts
import { XuqmSDK } from '@xuqm/rn-common'
+
+await XuqmSDK.login({
+ userId: 'user-001',
+ accessToken: 'host-access-token',
+})
+```
+
+已安装 Push 模块时,会话变化自动同步到原生层。不要调用不存在的
+Token 注册方法,也不要建立第二套 Push 登录 API。
+
+登出:
+
+```ts
+await XuqmSDK.logout()
+```
+
+## 离线推送开关
+
+```ts
import { PushSDK } from '@xuqm/rn-push'
-await XuqmSDK.initialize({ appKey: 'your_app_key' })
-await XuqmSDK.login({ userId: 'user_001', userSig: 'your_user_sig' })
-// ↓ 登录后调用 PushSDK.initialize() 完成 token 注册
-await PushSDK.initialize('user_001')
+await PushSDK.setOfflinePushEnabled(false)
+await PushSDK.setOfflinePushEnabled(true)
```
----
+该开关同步到当前登录用户。未登录时不要在业务页面展示为已经保存成功。
-## 8. iOS APNs 配置
+## 免打扰
-在 `AppDelegate.m` 或 `AppDelegate.swift` 中:
-
-```objc
-// AppDelegate.m
-- (void)application:(UIApplication *)application
- didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
- // 转发 token,由 rn-push 原生模块处理
- [RCTEventEmitter ...] // 通过 Bridge 传至 JS
-}
+```ts
+await PushSDK.setQuietHours('22:00', '08:00')
```
-> 使用 `PushSDK.requestNativeRegistration()` 会自动触发 iOS APNs 注册流程,无需额外原生代码(前提是 RN 0.76+ 自动链接)。
+时间使用 24 小时制的 `HH:mm`。清除设置:
----
+```ts
+await PushSDK.clearQuietHours()
+```
-## 9. 厂商渠道自动检测
+## 权限和厂商配置
-`@xuqm/rn-common` 的 `detectPushVendor` 会根据 `device.brand` 自动识别厂商:
+- Android 13 及以上由宿主在合适的业务时机申请通知权限。
+- 厂商推送应用标识和服务端密钥在租户平台维护。
+- 客户端只包含厂商要求的公开配置,不得包含服务端密钥。
+- Push 服务关闭或注册失败不能阻断宿主登录。
+- 重新登录或切换用户后,SDK 会使用新的公共会话重新绑定。
-| 品牌关键字 | 识别厂商 |
-|-----------|---------|
-| xiaomi / redmi | XIAOMI |
-| huawei | HUAWEI |
-| honor | HONOR |
-| oppo / realme | OPPO |
-| vivo / iqoo | VIVO |
-| iOS | APNS |
-| 其他 | FCM |
+## 常见错误
+
+### 登录成功但没有离线通知
+
+依次检查:
+
+1. 系统通知权限是否开启。
+2. 当前构建是否包含目标设备厂商通道。
+3. 租户平台是否启用 Push。
+4. App 包名、签名和厂商控制台配置是否一致。
+5. 当前用户是否关闭了离线推送或设置了免打扰。
+
+不要在 RN 页面手工缓存 Token 作为“绑定成功”的判断依据。
diff --git a/docs-site/docs/rn/session.md b/docs-site/docs/rn/session.md
new file mode 100644
index 0000000..1315c15
--- /dev/null
+++ b/docs-site/docs/rn/session.md
@@ -0,0 +1,83 @@
+# 会话与隐私授权
+
+使用扩展模块的应用只有一个公共会话入口。Common 独立模式不需要登录。
+
+## 隐私授权
+
+SDK 不展示隐私弹窗,也不会自行推断用户是否同意。宿主在自己的隐私声明获得授权后调用:
+
+```ts
+import { XuqmSDK } from '@xuqm/rn-common'
+
+await XuqmSDK.setPrivacyConsent(true)
+```
+
+用户撤回授权时:
+
+```ts
+await XuqmSDK.setPrivacyConsent(false)
+```
+
+撤回后 BugCollect 会停止采集并清理尚未发送的数据。宿主业务是否继续运行由宿主决定。
+
+## 登录
+
+宿主业务登录成功后调用一次:
+
+```ts
+await XuqmSDK.login({
+ userId: 'user-001',
+ accessToken: 'host-access-token',
+ name: '张医生',
+ phone: '13800000000',
+ avatar: 'https://example.com/avatar.png',
+ // 只有使用 IM 时才需要:
+ userSig: 'im-user-signature',
+})
+```
+
+字段说明:
+
+| 字段 | 必填 | 用途 |
+| --- | --- | --- |
+| `userId` | 是 | XuqmGroup 扩展服务共享的用户标识 |
+| `accessToken` | 按业务 | 公共 HTTP Bearer Token |
+| `userSig` | 仅 IM | IM 登录凭证 |
+| `name/email/phone/avatar` | 否 | 扩展模块需要时使用的用户资料 |
+
+`accessToken` 和 `userSig` 是两种不同凭证,不要互相替代。
+
+## 登出
+
+```ts
+await XuqmSDK.logout()
+```
+
+登出会通知所有已安装扩展包清理当前会话。业务页面不需要分别调用 Update、Push 或 IM 的
+登录/登出方法。
+
+## 获取当前状态
+
+```ts
+const userId = XuqmSDK.getUserId()
+const userInfo = XuqmSDK.getUserInfo()
+const initState = XuqmSDK.getInitializationState()
+```
+
+这些方法适合应用入口、调试页或基础设施层读取。业务组件应从宿主自己的用户领域 Store
+展示资料,不要把 SDK 当作宿主用户资料数据库。
+
+## 登录门禁与宿主行为
+
+租户平台可以把 Update 等服务配置为“登录后检查”。未登录时 SDK 会返回
+`LOGIN_REQUIRED`,宿主应当跳过更新提示并继续正常使用应用:
+
+```ts
+const result = await UpdateSDK.checkAppUpdate()
+
+if (result.status === 'LOGIN_REQUIRED') {
+ return // 不弹错误、不阻断启动
+}
+```
+
+平台临时不可用、服务关闭或登录要求发生变化,都不能替代宿主自身的登录和启动流程。
diff --git a/docs-site/docs/rn/setup.md b/docs-site/docs/rn/setup.md
index 2239b00..aeef694 100644
--- a/docs-site/docs/rn/setup.md
+++ b/docs-site/docs/rn/setup.md
@@ -1,147 +1,156 @@
-# React Native 安装配置
+# 安装与自动配置
-**包名**:`@xuqm/rn-sdk` · **版本**:0.2.x · **RN 版本**:≥ 0.76.0
+## 环境要求
-> `rn-sdk` 作为内部基础包存在,业务方正常接入时使用 `@xuqm/rn-common` 和各业务模块即可。
+- React Native `0.76` 或更高版本
+- Node.js 当前 LTS
+- Android `minSdk 24` 或更高版本
+- 使用原生模块时,需要标准 React Native 原生工程或完成 Prebuild 的 Expo 工程
----
+SDK 当前以 Android 为完整验证平台。页面中明确标记为 Android 的能力不能据此推断为
+iOS 已支持。
-## npm / yarn 安装
+## 配置 npm 仓库
-在项目根目录创建 `.npmrc`:
+宿主从只读聚合仓库安装依赖:
-```
-@xuqm:registry=https://nexus.xuqinmin.com/repository/npm-hosted/
+```ini
+# .npmrc
+@xuqm:registry=https://nexus.xuqinmin.com/repository/npm/
```
-只使用基础能力时,直接安装 `rn-common`:
+请使用租户平台为当前应用推荐的精确版本。开发和测试阶段可能使用带 `alpha`、`beta`、
+`rc` 或 `SNAPSHOT` 标识的预发布版本,不要自行改成无预发布标识的版本。
+
+## 按需安装
+
+只使用通用基础能力:
```bash
-yarn add @xuqm/rn-common
+pnpm add @xuqm/rn-common axios \
+ @react-native-async-storage/async-storage
```
-按需安装模块时,`rn-im` / `rn-push` / `rn-update` 都会自动带上 `rn-common` 和 `rn-sdk`:
+使用版本更新:
```bash
-yarn add @xuqm/rn-common @xuqm/rn-im
-# 或全量安装
-yarn add @xuqm/rn-common @xuqm/rn-im @xuqm/rn-push @xuqm/rn-update @xuqm/rn-bugcollect
+pnpm add @xuqm/rn-common @xuqm/rn-update \
+ @react-native-async-storage/async-storage
```
----
-
-## 自动 / 手动链接
-
-React Native 0.60+ 支持**自动链接**,安装后执行:
+使用 WebView:
```bash
-cd ios && pod install
+pnpm add @xuqm/rn-common @xuqm/rn-xwebview \
+ react-native-webview react-native-safe-area-context react-native-svg
```
-> 若使用 Expo,需先执行 `expo prebuild` 生成原生项目后再执行 `pod install`。
-
----
-
-## iOS 配置
-
-### Pod 安装
+其他模块同样按需安装:
```bash
-cd ios
-pod install
+pnpm add @xuqm/rn-bugcollect
+pnpm add @xuqm/rn-im @nozbe/watermelondb
+pnpm add @xuqm/rn-push
```
-### 权限声明
+项目使用 npm 或 Yarn 时,可以使用等价安装命令;不要同时维护多份锁文件。
-在 `Info.plist` 中添加:
+## 获取配置文件
-```xml
-NSCameraUsageDescription
-需要访问相机
-NSPhotoLibraryUsageDescription
-需要访问相册
-NSMicrophoneUsageDescription
-需要访问麦克风
+使用扩展包时,在租户平台进入目标应用并下载配置文件。将下载文件原名放到:
+
+```text
+src/assets/config/<平台生成的文件名>.xuqmconfig
```
----
+约束:
-## Android 配置
+- 目录中只能存在一个 `.xuqmconfig`。
+- 不要重命名内容、手工编辑或提交到公共仓库。
+- 公有化和私有化平台生成的配置不能混用。
+- 重新生成配置后,下一次构建必须使用新文件。
+- Common 独立模式不要放置配置文件。
-### Gradle 仓库
+建议加入忽略规则:
-在 `android/build.gradle` 中确保包含:
-
-```gradle
-allprojects {
- repositories {
- maven { url "https://nexus.xuqinmin.com/repository/android/" }
- google()
- mavenCentral()
- }
-}
+```gitignore
+src/assets/config/*.xuqmconfig
```
-### 最低版本
+## 配置 Metro
-- `minSdkVersion = 24`
-- `compileSdkVersion = 34`
-
----
-
-## 依赖关系
-
-```
-@xuqm/rn-sdk(meta-package,不建议业务方直接引用)
- ├── @xuqm/rn-common ← 初始化、网络、设备信息
- ├── @xuqm/rn-im ← IM 模块(依赖 WatermelonDB)
- ├── @xuqm/rn-push ← Push 模块
- ├── @xuqm/rn-update ← 更新模块
- └── @xuqm/rn-bugcollect ← 异常采集模块(崩溃、ANR 上报)
-```
-
----
-
-## Metro 配置
-
-使用 `withXuqmConfig` Metro 插件替代手动 alias 配置。该插件会自动处理 RN SDK 的模块解析,并注入 `.xuqmconfig` 配置文件:
+没有启用插件化时,只包装一次 Common:
```js
// metro.config.js
+const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config')
const { withXuqmConfig } = require('@xuqm/rn-common/metro')
-module.exports = withXuqmConfig({
- // 你的原始 Metro 配置
+const config = mergeConfig(getDefaultConfig(__dirname), {
+ // 宿主自己的 Metro 配置
})
+
+module.exports = withXuqmConfig(config)
```
-> 不再需要手动配置 `resolver.extraNodeModules` 或 alias。
+启用 RN 插件化时使用 Update 提供的组合器,不要再叠加 `withXuqmConfig`:
----
+```js
+const { getDefaultConfig } = require('@react-native/metro-config')
+const { withXuqmModuleConfig } = require('@xuqm/rn-update/metro')
-## 配置文件
-
-从平台控制台下载应用的 `.xuqmconfig` 配置文件(例如 `识校宝.xuqmconfig`),放置到 `src/assets/config/` 目录。`withXuqmConfig` 插件会自动发现并注入该配置,SDK 运行时自动读取,无需手动指定路径。
-
-文件名不限,只要扩展名为 `.xuqmconfig` 即可。
-
----
-
-## BugCollect
-
-异常采集模块可自动收集 JS 异常、原生崩溃日志并上报到服务端。
-
-```bash
-yarn add @xuqm/rn-bugcollect
+module.exports = withXuqmModuleConfig(getDefaultConfig(__dirname))
```
-集成了 `withXuqmConfig` 插件后,BugCollect 会随 SDK 自动初始化,无需额外代码。采集服务地址从 `.xuqmconfig` 配置中自动读取。
+两种配置二选一。项目只能存在一个 Metro 配置出口。
----
+## Android 仓库
-## 下一步
+原生依赖从 XuqmGroup Android 聚合仓库读取:
-- [RN IM 接入 →](./im)
-- [RN 群聊 →](./group)
-- [RN 推送接入 →](./push)
-- [RN 版本更新 →](./update)
+```kotlin
+// settings.gradle.kts
+dependencyResolutionManagement {
+ repositories {
+ maven("https://nexus.xuqinmin.com/repository/android/")
+ google()
+ mavenCentral()
+ }
+}
+```
+
+React Native 自动链接会处理已安装模块。只有 Update 插件化宿主需要按其页面说明应用
+一次 Bundle 构建脚本。
+
+## 启动前检查
+
+```ts
+import { XuqmSDK } from '@xuqm/rn-common'
+
+const state = XuqmSDK.getInitializationState()
+// idle | initializing | ready | degraded | failed
+```
+
+一般业务无需等待初始化。确实需要在首屏执行扩展操作时:
+
+```ts
+await XuqmSDK.awaitInitialization()
+```
+
+持续失败时按错误码展示非阻断提示,不要让 SDK 服务异常替代宿主首页。
+
+## Debug 策略
+
+- `pnpm start` 和 `pnpm android` 使用标准 Metro 与 Fast Refresh。
+- Debug 默认不自动检查整包或插件更新。
+- Debug 默认不自动上传 BugCollect。
+- 需要验证服务时,通过对应模块的显式测试入口触发。
+
+## 验收清单
+
+- 工程中只有一份 `.xuqmconfig`。
+- Metro 只使用一个 Xuqm 包装器。
+- Common-only 工程不要求初始化。
+- App 启动时没有手写 AppKey、平台地址或内部服务地址。
+- 登录只调用一次公共会话入口。
+- Release 构建使用租户平台当前有效配置。
diff --git a/docs-site/docs/rn/troubleshooting.md b/docs-site/docs/rn/troubleshooting.md
new file mode 100644
index 0000000..9dd1c0f
--- /dev/null
+++ b/docs-site/docs/rn/troubleshooting.md
@@ -0,0 +1,142 @@
+# 错误处理与排障
+
+## 公共错误码
+
+需要初始化的扩展能力可能抛出 `XuqmError`:
+
+| 错误码 | 含义 | 宿主处理 |
+| --- | --- | --- |
+| `XUQM_NOT_READY` | 自动初始化尚未完成或没有启动 | 检查配置和 Metro;不要阻断宿主首页 |
+| `XUQM_NOT_LOGGED_IN` | 当前操作要求登录 | 完成宿主登录后重试 |
+| `XUQM_CONFIG_INVALID` | 配置格式或签名无效 | 重新从租户平台下载配置并构建 |
+| `XUQM_CONFIG_EXPIRED` | 构建使用的配置已过期 | 重新生成并构建 |
+| `XUQM_CONFIG_REVOKED` | 配置已被租户平台撤销 | 使用平台当前有效配置重新构建 |
+| `XUQM_SERVICE_DISABLED` | 对应服务未开启 | 隐藏入口或跳过功能 |
+
+示例:
+
+```ts
+import { XuqmError } from '@xuqm/rn-common'
+
+try {
+ await operation()
+} catch (error) {
+ if (error instanceof XuqmError) {
+ reportNonBlockingError(error.code)
+ return
+ }
+ throw error
+}
+```
+
+## App 启动正常,但扩展功能不可用
+
+1. 确认 `src/assets/config/` 只有一个 `.xuqmconfig`。
+2. 确认 Metro 只使用一个 Xuqm 包装器。
+3. 确认配置来自当前公有化或私有化租户平台,不能混用。
+4. 查看 `XuqmSDK.getInitializationState()`。
+5. 检查租户平台是否已开启对应服务。
+6. 如果服务要求登录,确认已经调用公共 `login`。
+
+扩展服务失败不应导致宿主白屏。不要在根组件渲染前无限等待网络初始化。
+
+## Common-only 工程提示未初始化
+
+Common 的网络、文件、时间和 UI 能力不要求初始化。出现未初始化错误通常表示调用了
+需要平台配置的扩展 API,或直接调用了 `getConfig()`。
+
+Common-only 工程应当:
+
+- 不放 `.xuqmconfig`。
+- 不使用 Metro 自动配置包装器。
+- 不调用 `XuqmSDK.awaitInitialization()`。
+- 业务网络使用绝对 URL 或 `configureHttp`。
+
+## 配置构建失败
+
+构建期失败是预期的安全门禁,常见原因:
+
+- 配置文件超过一个。
+- 文件内容被编辑、损坏或不属于当前应用。
+- 配置过期或已撤销。
+- 公有化配置用于私有化应用,或反向混用。
+
+不要通过跳过校验生成安装包,应从租户平台重新下载配置。
+
+## Update 不弹更新
+
+先检查 `checkAppUpdate()` 的结构化状态:
+
+- `LOGIN_REQUIRED`:未登录时正常跳过。
+- `SERVICE_DISABLED`:租户平台未启用 Update。
+- `NO_UPDATE`:没有适用于当前版本、设备或灰度身份的更新。
+- `FAILED`:查看 `errorCode` 和 `message`,但不要阻断应用。
+
+网络状态随检查结果返回,SDK 不替宿主决定是否允许移动网络下载。
+
+## 插件打不开
+
+1. Debug 确认 Metro 属于当前工程并能正常 Fast Refresh。
+2. 运行 `pnpm run xuqm:doctor`。
+3. 确认模块 ID 与 `xuqm.modules.json` 一致。
+4. 检查插件入口是否调用 `definePlugin`。
+5. 更新检查失败时,确认宿主仍会尝试打开本地版本。
+6. 只有插件实际启动失败才记录启动失败,网络检查失败不是插件启动失败。
+
+## BugCollect 没有数据
+
+确认:
+
+- 用户已经同意隐私声明。
+- 租户平台已开启 BugCollect。
+- 当前不是默认禁止自动上报的 Debug 行为。
+- Release 构建没有启用应急关闭。
+- 测试页调用 `sendTestErrorAndFlush` 时能看到明确错误。
+
+BugCollect 失败不能影响宿主功能,不要通过抛出全局异常验证生产采集。
+
+## XWebView 摄像头或麦克风失败
+
+同时满足以下条件才会授权:
+
+- 宿主声明并获得系统权限。
+- `permissions.camera` 或 `permissions.microphone` 已开启。
+- 当前网页 Origin 精确命中 `allowedOrigins`。
+- 自定义 `onRequest` 没有拒绝。
+
+协议、端口或子域名变化都会形成不同 Origin。
+
+## Push 没有通知
+
+检查系统通知权限、设备厂商通道、包名与签名、租户平台服务状态、用户离线推送开关和免
+打扰时间。RN 页面不应以是否缓存到厂商 Token 判断最终绑定状态。
+
+## 从历史接入方式迁移
+
+当前版本不再使用以下方式:
+
+- 手动传 AppKey 初始化。
+- 页面或子模块分别初始化、登录。
+- 宿主手工传入应用商店地址。
+- Update 的旧 Bundle 下载与应用方法。
+- RN 页面手工注册推送 Token。
+- XWebView 的扁平参数和全局控制器。
+
+迁移步骤:
+
+1. 下载并放置唯一配置文件。
+2. 使用一个 Metro 包装器。
+3. 隐私授权同步一次。
+4. 登录和登出只调用 Common。
+5. 按本文各模块页面替换旧 API。
+6. 删除宿主重复的 SDK 状态、地址和 Token 缓存。
+
+## 提交问题时提供
+
+- SDK 包名与精确版本。
+- React Native、Android/iOS 和设备系统版本。
+- 公有化或私有化类型,不提供服务端秘密。
+- 初始化状态、结构化错误码和最小复现路径。
+- 已脱敏日志。
+
+不要上传 `.xuqmconfig`、Token、用户隐私数据或完整业务响应。
diff --git a/docs-site/docs/rn/update.md b/docs-site/docs/rn/update.md
index 1adbb93..e0bb58e 100644
--- a/docs-site/docs/rn/update.md
+++ b/docs-site/docs/rn/update.md
@@ -1,161 +1,233 @@
-# React Native 版本更新接入指南
+# 版本与插件更新
-**包名**:`@xuqm/rn-update` · **功能**:App 版本检查、RN Bundle 热更新、打开商店
+`@xuqm/rn-update` 提供两类能力:
----
+- Android 整包检查、下载、安装和应用商店跳转。
+- 可选的 RN 插件检查、安装和启动。
-## 1. 安装
+SDK 返回更新信息和当前网络状态,是否弹窗、是否允许使用移动网络以及何时开始下载由宿主
+决定。
-```bash
-yarn add @xuqm/rn-update
-```
+## 整包更新
-`rn-update` 会自动依赖 `@xuqm/rn-common` 和 `@xuqm/rn-sdk`。
-
----
-
-## 2. App 版本检查
+### 检查
```ts
import { UpdateSDK } from '@xuqm/rn-update'
-const appUpdate = await UpdateSDK.checkAppUpdate()
-if (appUpdate.needsUpdate) {
- console.log('新版本:', appUpdate.versionName)
- console.log('更新日志:', appUpdate.changeLog)
- console.log('下载地址:', appUpdate.downloadUrl)
+const result = await UpdateSDK.checkAppUpdate()
- if (appUpdate.forceUpdate) {
- // 强制更新
- showForceUpdateModal(appUpdate)
- } else {
- // 可选更新
- showOptionalUpdateModal(appUpdate)
- }
+switch (result.status) {
+ case 'UPDATE_AVAILABLE':
+ showUpdateDialog(result.update, result.network)
+ break
+ case 'LOGIN_REQUIRED':
+ case 'SERVICE_DISABLED':
+ case 'NO_UPDATE':
+ // 正常跳过,不阻断应用
+ break
+ case 'FAILED':
+ // 记录一次非阻断诊断
+ console.warn(result.errorCode, result.message)
+ break
}
```
-App 版本号自动从原生模块读取,无需手动传入。开发/模拟器环境可调用 `_devSetAppVersion` 覆盖:
+检查状态:
+
+| 状态 | 宿主建议 |
+| --- | --- |
+| `UPDATE_AVAILABLE` | 根据 `forceUpdate` 展示宿主自己的更新弹窗 |
+| `NO_UPDATE` | 继续启动 |
+| `LOGIN_REQUIRED` | 未登录时跳过,登录后可再次检查 |
+| `SERVICE_DISABLED` | 服务未启用,继续运行 |
+| `FAILED` | 记录诊断,不阻断宿主 |
+
+`result.network` 包含连接类型、是否已连接和是否按流量计费。租户平台不替宿主配置网络
+策略。
+
+### 下载并安装
+
+用户确认后调用:
```ts
-// 仅限开发环境使用
-UpdateSDK._devSetAppVersion(100, '1.0.0')
-```
-
----
-
-## 3. 打开商店
-
-```ts
-await UpdateSDK.openStore(appUpdate.appStoreUrl, appUpdate.marketUrl)
-```
-
-- iOS:打开 `appStoreUrl`
-- Android:打开 `marketUrl`
-
----
-
-## 4. RN 插件更新
-
-插件更新以服务端返回的 release-set 为最小事务单位。一个 release-set 只包含目标
-app/buz 及其 Common 依赖;SDK 会统一完成签名、宿主身份、整包版本、原生基线、
-SHA-256 和最终依赖闭包校验。宿主不能分别下载后自行拼接版本。
-
-进入 buz 前自动检查并安装:
-
-```ts
-await UpdateSDK.checkAndInstall('home', {
- onProgress(moduleId, progress) {
- console.log(moduleId, progress.percent)
+await UpdateSDK.downloadAndInstallApp(result.update!, {
+ destination: 'SDK_PRIVATE',
+ onProgress(progress) {
+ setProgress(progress.percent)
},
})
```
-需要由宿主展示确认弹窗时,检查与安装分开调用:
+需要把安装包保留在系统 Downloads:
```ts
-const plan = await UpdateSDK.check('home')
-if (plan && (await showPluginUpdateDialog(plan))) {
- await UpdateSDK.install(plan)
-}
+await UpdateSDK.downloadAndInstallApp(result.update!, {
+ destination: 'PUBLIC_DOWNLOADS',
+})
```
-`check` 只检查,不下载、不修改本地状态。安装阶段会再次验证签名和检查时
-记录的本地基线;计划已过期时必须重新检查。插件版本、路径和事务状态由原生层统一
-管理,宿主不得另建一套缓存版本表。
+下载失败或应用退出后不会在下次启动时自动继续。
----
-
-## 5. 强制更新处理
-
-当 `appUpdate.forceUpdate` 为 `true` 时,建议业务层:
-
-1. 弹出不可关闭的 Modal
-2. 只允许用户点击「立即更新」
-3. 调用 `UpdateSDK.openStore()` 跳转商店
+### 只下载、稍后安装
```ts
-function showForceUpdateModal(update: AppUpdateInfo) {
- // 使用 RN Modal 组件,设置不可取消
- Alert.alert(
- '发现重要更新',
- `当前版本已不可用,请升级至 ${update.versionName}`,
- [
- {
- text: '立即更新',
- onPress: () => UpdateSDK.openStore(update.appStoreUrl, update.marketUrl),
- },
- ],
- { cancelable: false }
- )
-}
+import { NativeAppUpdate, UpdateSDK } from '@xuqm/rn-update'
+
+const downloaded = await UpdateSDK.downloadApk(result.update!, {
+ destination: 'SDK_PRIVATE',
+})
+
+console.log(downloaded.localPath)
+
+await NativeAppUpdate.installDownloaded({
+ versionCode: result.update!.versionCode!,
+ sha256: result.update!.sha256,
+})
```
----
-
-## 6. 多模块统一登录
-
-Update 模块与 IM、Push 模块共享同一套登录态:
+取消当前下载:
```ts
-await XuqmSDK.initialize({ appKey: 'your_app_key' })
-await XuqmSDK.login({ userId: 'user_001', userSig: 'your_user_sig_jwt' })
-// UpdateSDK 在 checkAppUpdate 时自动携带 appKey,无需额外登录操作
+await NativeAppUpdate.cancel()
```
----
+如果系统禁止安装未知来源应用:
-## 7. 一键打包上传
+```ts
+await NativeAppUpdate.openInstallPermissionSettings()
+```
-SDK 提供 `xuqm_release.mjs` 脚本,可一键完成 RN Bundle 构建并上传至 XuqmGroup 版本管理服务:
+### 跳转应用商店
+
+宿主不传商店地址,只传检查接口返回的更新对象:
+
+```ts
+const opened = await UpdateSDK.openStore(result.update!)
+```
+
+Android 会优先打开当前设备对应的应用商店,不可用时使用平台提供的通用下载页。iOS 或
+HarmonyOS 没有配置商店地址时返回 `false`,宿主应提示当前无法提供下载服务。
+
+### 忽略可选版本
+
+```ts
+await UpdateSDK.ignoreAppVersion(result.update!.versionCode!)
+const ignoredVersion = await UpdateSDK.getIgnoredAppVersion()
+await UpdateSDK.clearIgnoredAppVersion()
+```
+
+强制更新不能被忽略。
+
+## 不启用插件化
+
+普通项目可以只使用上述整包更新 API,不创建 `common/app/buz` 模块,也不需要接入插件
+运行时。后续需要插件化时再执行初始化命令即可。
+
+## 启用插件化
+
+在项目根目录执行交互式初始化:
```bash
-XUQM_API_TOKEN=*** node scripts/xuqm_release.mjs --platform android --publish
+pnpm exec xuqm-rn init
+pnpm run xuqm:doctor
```
-脚本执行流程:
-1. 按模块构建 JS/Hermes Bundle 和 Source Map
-2. 生成根目录唯一的 `rn-manifest.json`
-3. 生成 `..xuqm.zip`
-4. 上传归档并由服务端校验、签名
-5. 按用户选择发布,返回不可变发布记录
+初始化工具会创建 `xuqm.modules.json`、补充必要脚本并提示宿主完成最少原生配置。不要
+手工复制其他项目的模块表。
-常用参数:
+Metro 使用:
-| 参数 | 说明 |
-|------|------|
-| `--platform` | 平台:`android` / `ios` |
-| `--module` | 可重复传入,只构建或发布指定模块 |
-| `--note` | 本次发布说明 |
-| `--publish` | 上传后立即发布 |
-| `--bugcollect` | `platform` 上传 Source Map;`disabled` 仅保留本地产物 |
+```js
+const { getDefaultConfig } = require('@react-native/metro-config')
+const { withXuqmModuleConfig } = require('@xuqm/rn-update/metro')
-在 `package.json` 中添加快捷命令:
+module.exports = withXuqmModuleConfig(getDefaultConfig(__dirname))
+```
-```json
-{
- "scripts": {
- "xuqm:release": "node scripts/xuqm_release.mjs --platform android --publish"
- }
+Android 宿主应用一次构建脚本:
+
+```groovy
+apply from: file("../../node_modules/@xuqm/rn-update/android/xuqm-bundles.gradle")
+```
+
+### 注册宿主运行时
+
+```ts
+import { XuqmRuntime } from '@xuqm/rn-update'
+
+XuqmRuntime.configure({
+ plugins: pluginRegistrations,
+ context: {
+ navigate(route, params) {
+ navigationRef.navigate(route, params)
+ },
+ emit(event, payload) {
+ hostEvents.emit(event, payload)
+ },
+ getHostState() {
+ return appStore.getState()
+ },
+ },
+ async loadBundle(_moduleId, bundlePath) {
+ await loadNativeBundle(bundlePath)
+ },
+})
+```
+
+模块入口注册:
+
+```ts
+import { definePlugin } from '@xuqm/rn-update'
+
+definePlugin({
+ moduleId: 'orders',
+ activate(context) {
+ context.navigate('OrdersHome')
+ },
+})
+```
+
+### 检查与安装插件
+
+自动完成检查和安装:
+
+```ts
+const plan = await UpdateSDK.checkAndInstallPlugin('orders', {
+ onProgress(moduleId, progress) {
+ console.log(moduleId, progress.percent)
+ },
+})
+
+await XuqmRuntime.activate('orders', {
+ reloadBundle: plan !== null,
+})
+```
+
+宿主需要先展示确认弹窗时:
+
+```ts
+const plan = await UpdateSDK.checkPluginRelease('orders')
+
+if (plan && (await showPluginUpdateDialog(plan))) {
+ await UpdateSDK.installPluginRelease(plan)
}
```
+
+只检查不会下载或修改本地状态。插件检查失败时,宿主应继续打开已有的本地健康版本。
+
+## 启动策略
+
+- 启动时先检查整包更新。
+- 不需要整包更新时,再检查 App 插件。
+- 业务插件在进入前分别检查。
+- Debug 默认不自动检查;开发页可以显式使用 `UpdateSDK.developer` 下的测试入口。
+- 更新 SDK 不绘制业务弹窗,所有提示和确认交互由宿主实现。
+
+## 打包与发布
+
+初始化生成的脚本支持 Debug、Release、插件构建和上传。发布令牌只通过 CI 环境变量
+`XUQM_API_TOKEN` 注入,不写入项目配置、源码或产物。
+
+SDK 自身制品发布与应用/插件发布是不同操作。应用开发者不需要发布 SDK,也不得把 SDK
+发布凭据加入宿主工程。
diff --git a/docs-site/docs/rn/xwebview.md b/docs-site/docs/rn/xwebview.md
new file mode 100644
index 0000000..3f8e338
--- /dev/null
+++ b/docs-site/docs/rn/xwebview.md
@@ -0,0 +1,249 @@
+# XWebView
+
+`@xuqm/rn-xwebview` 提供可直接打开的独立 WebView 页面,也支持在业务页面中嵌入浏览器
+内核。SDK 只提供通用容器和 Bridge 接入入口,不内置任何宿主业务协议。
+
+## 安装
+
+```bash
+pnpm add @xuqm/rn-common @xuqm/rn-xwebview \
+ react-native-webview react-native-svg \
+ @react-native-clipboard/clipboard react-native-safe-area-context
+```
+
+这些原生依赖需要由宿主直接声明,以便 React Native 自动链接。
+
+## 根部挂载 Host
+
+独立页面模式需要在应用根部挂载一次:
+
+```tsx
+import { XWebViewHost } from '@xuqm/rn-xwebview'
+
+export function App() {
+ return (
+ <>
+
+
+ >
+ )
+}
+```
+
+全局只能存在一个 `XWebViewHost`。
+
+## 打开独立页面
+
+```ts
+import { openWebView } from '@xuqm/rn-xwebview'
+
+const page = openWebView({
+ source: {
+ uri: 'https://example.com',
+ headers: {
+ 'X-From-App': 'true',
+ },
+ },
+ navigationBar: {
+ title: {
+ text: '服务页面',
+ mode: 'web',
+ },
+ },
+ statusBar: {
+ translucent: true,
+ contentStyle: 'dark-content',
+ },
+})
+
+const result = await page.closed
+console.log(result.reason, result.data)
+```
+
+返回的页面 Handle 可以:
+
+```ts
+page.reload()
+page.goBack()
+page.goForward()
+page.postMessage(JSON.stringify({ type: 'refresh' }))
+page.evaluateJavaScript('window.refreshPage?.()')
+page.suspend()
+page.resume()
+page.close('api', { completed: true })
+```
+
+`suspend()` 用于临时展示宿主原生页面,页面实例和网页历史会保留;完成后调用
+`resume()`。
+
+## 默认导航和返回规则
+
+- 左上角返回按钮只在当前网页存在历史页面时显示,只负责网页后退。
+- 右上角关闭按钮始终显示,点击立即关闭当前 WebView 页面。
+- Android 物理返回键优先返回网页历史。
+- 没有网页历史时,物理返回键需要在一秒内再次按下才关闭页面。
+- 默认菜单包含刷新、复制链接和在系统浏览器打开,不包含关闭页面。
+
+## 导航栏与状态栏
+
+```ts
+openWebView({
+ source: { uri: 'https://example.com' },
+ navigationBar: {
+ background: {
+ gradient: ['#1769E0', '#55A8FF'],
+ gradientAngle: 90,
+ // 也可以使用 color 或 image
+ },
+ title: {
+ text: '渐变导航栏',
+ color: '#FFFFFF',
+ mode: 'fixed',
+ },
+ backButton: { color: '#FFFFFF' },
+ closeButton: { color: '#FFFFFF' },
+ },
+ statusBar: {
+ backgroundColor: '#1769E0',
+ contentStyle: 'light-content',
+ translucent: true,
+ },
+})
+```
+
+网络背景图片异步加载,不会阻塞 `openWebView` 返回。
+
+完整替换导航栏时使用 `navigationBar.render(context)`。自定义返回按钮应调用
+`context.onBackPress()`,以保留统一的网页后退规则。
+
+## 自定义菜单
+
+```tsx
+openWebView({
+ source: { uri: 'https://example.com' },
+ navigationBar: {
+ menu: {
+ visible: true,
+ items: [
+ {
+ id: 'feedback',
+ label: '问题反馈',
+ onPress({ navigationState }) {
+ openFeedback({ url: navigationState.url })
+ },
+ },
+ ],
+ },
+ },
+})
+```
+
+不需要菜单时设置 `menu.visible: false`。
+
+## 摄像头和麦克风
+
+宿主先声明系统权限,然后为具体网页配置精确 Origin:
+
+```ts
+openWebView({
+ source: { uri: 'https://verify.example.com/start' },
+ permissions: {
+ allowedOrigins: ['https://verify.example.com'],
+ camera: true,
+ microphone: true,
+ },
+})
+```
+
+Android Manifest 示例:
+
+```xml
+
+
+```
+
+Origin 必须包含协议和主机,跨域后的页面不会继承原页面授权。宿主需要自定义决策时:
+
+```ts
+permissions: {
+ allowedOrigins: ['https://verify.example.com'],
+ camera: true,
+ microphone: true,
+ onRequest(request) {
+ if (canUseMedia(request.origin)) request.grant()
+ else request.deny()
+ },
+}
+```
+
+## 文件下载
+
+```ts
+openWebView({
+ source: { uri: 'https://example.com/files' },
+ downloads: {
+ auto: true,
+ destination: 'publicDownloads',
+ conflict: 'rename',
+ onProgress(progress) {
+ console.log(progress.percentage)
+ },
+ onComplete(result) {
+ console.log(result.filePath)
+ },
+ onError(url, error) {
+ console.warn(url, error)
+ },
+ },
+})
+```
+
+默认保存到应用沙盒。保存到系统 Downloads 只在目标平台支持时生效。
+
+## 宿主 Bridge
+
+宿主与 H5 自行约定消息协议,通过 `bridge` 注入:
+
+```ts
+openWebView({
+ source: { uri: 'https://example.com' },
+ bridge: {
+ async onMessage(raw, context) {
+ const message = JSON.parse(raw)
+
+ if (message.method === 'close') {
+ context.close('h5_close', message.data)
+ return true
+ }
+
+ if (message.method === 'setTitle') {
+ context.updateNavigation({
+ title: { text: String(message.title ?? '') },
+ })
+ return true
+ }
+
+ return false
+ },
+ },
+})
+```
+
+XWebView 不规定方法名、App ID、厂商、用户或业务参数。协议校验、权限和业务结果由宿主
+自己的 Bridge 负责。
+
+## 内嵌 WebView
+
+不需要独立页面和内置导航栏时:
+
+```tsx
+import { XWebViewView } from '@xuqm/rn-xwebview'
+
+
+```
+
+内嵌模式由所在页面负责尺寸、导航和关闭交互。
diff --git a/docs-site/package.json b/docs-site/package.json
index 53dec00..66c60db 100644
--- a/docs-site/package.json
+++ b/docs-site/package.json
@@ -4,6 +4,8 @@
"private": true,
"type": "module",
"scripts": {
+ "check:rn-contract": "node scripts/check-rn-docs.mjs",
+ "prebuild": "node scripts/check-rn-docs.mjs",
"dev": "vitepress dev docs",
"build": "vitepress build docs",
"preview": "vitepress preview docs"
diff --git a/docs-site/scripts/check-rn-docs.mjs b/docs-site/scripts/check-rn-docs.mjs
new file mode 100644
index 0000000..8c8c701
--- /dev/null
+++ b/docs-site/scripts/check-rn-docs.mjs
@@ -0,0 +1,171 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import process from 'node:process'
+import { fileURLToPath } from 'node:url'
+
+const docsSiteRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
+const rnDocsRoot = path.join(docsSiteRoot, 'docs', 'rn')
+const vitepressConfig = path.join(docsSiteRoot, 'docs', '.vitepress', 'config.ts')
+
+const requiredPages = [
+ 'index.md',
+ 'setup.md',
+ 'common.md',
+ 'session.md',
+ 'update.md',
+ 'bugcollect.md',
+ 'xwebview.md',
+ 'im.md',
+ 'push.md',
+ 'troubleshooting.md',
+]
+
+const forbiddenPatterns = [
+ [/@xuqm\/rn-sdk\b/g, '内部聚合包不应进入对外文档'],
+ [/\bXuqmSDK\.initialize\s*\(/g, '已删除的手动初始化 API'],
+ [/\bcheckRnUpdate\s*\(/g, '已删除的旧 Update API'],
+ [/\bdownloadAndApplyBundle\s*\(/g, '已删除的旧 Bundle API'],
+ [/\brequestNativeRegistration\s*\(/g, '已删除的 RN Push Token API'],
+ [/\bregisterToken\s*\(/g, '已删除的 RN Push Token API'],
+ [/\bsetDeviceToken\s*\(/g, '已删除的 RN Push Token API'],
+ [/@xuqm\/rn-common\/internal(?:-security)?\b/g, '官方扩展专用内部入口'],
+ [/\b(?:release[- ]set|Ed25519|NativeBundle|planReleaseSet)\b/gi, '内部更新实现细节'],
+ [/(?:原生基线|签名算法|数据库隔离规则|服务拓扑)/g, '内部架构细节'],
+]
+
+const requiredPackagePages = new Map([
+ ['@xuqm/rn-common', ['index.md', 'setup.md', 'common.md', 'session.md']],
+ ['@xuqm/rn-update', ['update.md']],
+ ['@xuqm/rn-bugcollect', ['bugcollect.md']],
+ ['@xuqm/rn-xwebview', ['xwebview.md']],
+ ['@xuqm/rn-im', ['im.md']],
+ ['@xuqm/rn-push', ['push.md']],
+])
+
+const failures = []
+const sourceByPage = new Map()
+
+for (const page of requiredPages) {
+ const file = path.join(rnDocsRoot, page)
+ if (!fs.existsSync(file)) {
+ failures.push(`缺少 RN 对外文档页面:${page}`)
+ continue
+ }
+ sourceByPage.set(page, fs.readFileSync(file, 'utf8'))
+}
+
+const actualMarkdownPages = fs
+ .readdirSync(rnDocsRoot, { withFileTypes: true })
+ .filter(entry => entry.isFile() && entry.name.endsWith('.md'))
+ .map(entry => entry.name)
+ .sort()
+
+for (const page of actualMarkdownPages) {
+ if (!requiredPages.includes(page)) {
+ failures.push(`存在未纳入公开信息架构的 RN 页面:${page}`)
+ }
+}
+
+for (const [page, source] of sourceByPage) {
+ for (const [pattern, reason] of forbiddenPatterns) {
+ pattern.lastIndex = 0
+ if (pattern.test(source)) {
+ failures.push(`${page} 包含禁止内容(${reason}):${pattern}`)
+ }
+ }
+}
+
+for (const [packageName, pages] of requiredPackagePages) {
+ for (const page of pages) {
+ if (!sourceByPage.get(page)?.includes(packageName)) {
+ failures.push(`${page} 未明确记录公开包 ${packageName}`)
+ }
+ }
+}
+
+const navigation = fs.readFileSync(vitepressConfig, 'utf8')
+for (const page of requiredPages) {
+ const route = page === 'index.md' ? "/rn/'" : `/rn/${page.replace(/\.md$/, '')}'`
+ if (!navigation.includes(route)) {
+ failures.push(`VitePress RN 导航缺少 ${page}`)
+ }
+}
+if (navigation.includes('/rn/group')) {
+ failures.push('VitePress RN 导航仍引用已经合并的重复群聊页面')
+}
+
+// 本地多仓工作区存在 RN SDK 源码时,继续核对文档中的命名导入确实由 package index 导出。
+// Jenkins 单仓构建没有兄弟仓库时跳过此增强检查,但上面的静态契约仍然生效。
+const sdkRoot =
+ process.env.XUQM_RN_SDK_DIR ??
+ path.resolve(docsSiteRoot, '..', '..', 'XuqmGroup-RNSDK')
+
+if (fs.existsSync(path.join(sdkRoot, 'packages'))) {
+ const indexByPackage = new Map()
+ for (const packageDir of ['common', 'update', 'bugcollect', 'xwebview', 'im', 'push']) {
+ const packageJson = JSON.parse(
+ fs.readFileSync(path.join(sdkRoot, 'packages', packageDir, 'package.json'), 'utf8'),
+ )
+ indexByPackage.set(
+ packageJson.name,
+ fs.readFileSync(path.join(sdkRoot, 'packages', packageDir, 'src', 'index.ts'), 'utf8'),
+ )
+ }
+
+ const namedImport =
+ /import\s*\{([\s\S]*?)\}\s*from\s*['"](@xuqm\/rn-(?:common|update|bugcollect|xwebview|im|push))['"]/g
+ for (const [page, source] of sourceByPage) {
+ for (const match of source.matchAll(namedImport)) {
+ const packageSource = indexByPackage.get(match[2])
+ const symbols = match[1]
+ .split(',')
+ .map(value => value.trim().replace(/^type\s+/, '').split(/\s+as\s+/)[0])
+ .filter(value => /^[A-Za-z_$][\w$]*$/.test(value))
+ for (const symbol of symbols) {
+ const exported = new RegExp(
+ `(?:export\\s+(?:const|class|function|type|interface)\\s+${symbol}\\b|export\\s*\\{[\\s\\S]*?\\b${symbol}\\b[\\s\\S]*?\\})`,
+ ).test(packageSource)
+ if (!exported) {
+ failures.push(`${page} 从 ${match[2]} 导入未公开的符号:${symbol}`)
+ }
+ }
+ }
+ }
+
+ const objectSources = new Map([
+ ['XuqmSDK', path.join(sdkRoot, 'packages', 'common', 'src', 'sdk.ts')],
+ ['UpdateSDK', path.join(sdkRoot, 'packages', 'update', 'src', 'UpdateSDK.ts')],
+ [
+ 'NativeAppUpdate',
+ path.join(sdkRoot, 'packages', 'update', 'src', 'NativeAppUpdate.ts'),
+ ],
+ ['XuqmRuntime', path.join(sdkRoot, 'packages', 'update', 'src', 'PluginRuntime.ts')],
+ ['BugCollect', path.join(sdkRoot, 'packages', 'bugcollect', 'src', 'BugCollect.ts')],
+ ['ImSDK', path.join(sdkRoot, 'packages', 'im', 'src', 'ImSDK.ts')],
+ ['PushSDK', path.join(sdkRoot, 'packages', 'push', 'src', 'PushSDK.ts')],
+ ])
+
+ for (const [objectName, sourceFile] of objectSources) {
+ const implementation = fs.readFileSync(sourceFile, 'utf8')
+ const usage = new RegExp(`\\b${objectName}\\.([A-Za-z_$][\\w$]*)\\s*\\(`, 'g')
+ for (const [page, source] of sourceByPage) {
+ for (const match of source.matchAll(usage)) {
+ const method = match[1]
+ const declared = new RegExp(
+ `(?:^|\\n)\\s*(?:async\\s+)?${method}\\s*(?:\\([^\\n]*|:)`,
+ ).test(implementation)
+ if (!declared) {
+ failures.push(`${page} 调用 ${objectName} 中不存在的公开方法:${method}`)
+ }
+ }
+ }
+ }
+}
+
+if (failures.length > 0) {
+ console.error('RN 对外文档契约检查失败:')
+ for (const failure of failures) console.error(`- ${failure}`)
+ process.exit(1)
+}
+
+console.log(`RN 对外文档契约检查通过:${requiredPages.length} 个页面,6 个公开模块`)
diff --git a/docs/RN_PUBLIC_DOCUMENTATION_POLICY.md b/docs/RN_PUBLIC_DOCUMENTATION_POLICY.md
new file mode 100644
index 0000000..c25022f
--- /dev/null
+++ b/docs/RN_PUBLIC_DOCUMENTATION_POLICY.md
@@ -0,0 +1,62 @@
+# RN 对外文档边界
+
+## 目的
+
+`docs-site/docs/rn/` 是面向宿主集成方的公开文档,只描述安装、配置、公开 API、宿主交互、
+错误处理、平台差异和可执行的排障步骤。内部设计继续保留在对应 SDK 仓库,不进入公开
+站点导航、搜索索引或示例。
+
+## 事实来源
+
+文档修改按以下优先级核对:
+
+1. `XuqmGroup-RNSDK/packages/*/src/index.ts` 的真实公开导出。
+2. 公开类型定义和公开对象方法。
+3. 当前测试、可运行示例和构建脚本。
+4. 租户平台当前实际提供的配置与功能。
+
+包内 README、历史文档、注释和旧站点内容只能作为线索,不能直接复制到公开站点。
+
+## 当前公开模块
+
+- `@xuqm/rn-common`
+- `@xuqm/rn-update`
+- `@xuqm/rn-bugcollect`
+- `@xuqm/rn-xwebview`
+- `@xuqm/rn-im`
+- `@xuqm/rn-push`
+
+未实现、未发布或没有稳定公开契约的模块和 API 不得预先出现在文档站。
+
+## 可以公开
+
+- 安装依赖和只读制品仓库。
+- 平台配置文件的下载位置与宿主放置方式。
+- 自动配置、公共登录和隐私授权的调用方式。
+- 宿主需要调用的公开 API、参数、返回状态和错误码。
+- Android/iOS 已验证范围及权限、Manifest、Info.plist 要求。
+- 插件化的创建、打包、上传和宿主调用方式。
+- 不涉及安全实现的默认行为、升级说明与排障清单。
+
+## 禁止公开
+
+- 官方扩展包专用的 internal 子路径和内部聚合包。
+- 配置解密、签名算法、可信密钥、不可变清单和校验实现。
+- 插件安装事务、内部状态表、回滚存储和版本规划算法。
+- 数据库选型、表结构、用户隔离键和内部缓存格式。
+- 服务拓扑、内部接口、内部令牌、私有化部署脚本和运维地址。
+- App4、医网信或其他单一宿主的页面、厂商、域名和业务协议。
+- 凭据、Token、配置文件正文、用户隐私数据和真实业务响应。
+- 没有源码依据的示例 API。
+
+## 自动检查
+
+`yarn workspace @xuqm/docs-site check:rn-contract` 检查:
+
+- 十个 RN 公开页面和导航完整性。
+- 六个公开包是否均有入口。
+- 历史 API、internal 入口和内部实现词汇。
+- 在本地多仓工作区中,文档命名导入和对象方法是否确实由当前 RN SDK 源码公开。
+
+文档站构建前自动执行同一检查。Jenkins 单仓构建无法读取兄弟 SDK 仓库时仍执行静态
+边界检查;合并前必须在完整本地工作区至少执行一次源码增强检查。
diff --git a/docs/SDK_PLATFORM_V2_HANDOFF.md b/docs/SDK_PLATFORM_V2_HANDOFF.md
index fec6fd4..a56d2b5 100644
--- a/docs/SDK_PLATFORM_V2_HANDOFF.md
+++ b/docs/SDK_PLATFORM_V2_HANDOFF.md
@@ -1,6 +1,8 @@
# 租户平台 SDK V2 页面接手说明
SDK 平台的完整服务端契约见 Server 仓库 `docs/SDK_PLATFORM_V2_HANDOFF.md`。
+RN 对外文档的内容边界、事实来源和自动检查见
+[`RN_PUBLIC_DOCUMENTATION_POLICY.md`](./RN_PUBLIC_DOCUMENTATION_POLICY.md)。
当前 Web 变更:
@@ -25,6 +27,7 @@ SDK 平台的完整服务端契约见 Server 仓库 `docs/SDK_PLATFORM_V2_HANDOF
yarn install --frozen-lockfile
yarn build:tenant
yarn build:ops
+yarn workspace @xuqm/docs-site check:rn-contract
yarn workspace @xuqm/docs-site build
yarn test:web-cache
nginx -t -p "$PWD/" -c scripts/nginx-test.conf