From eb2baae07ecdd61d38bf6c159eceb0421b722224 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Wed, 29 Jul 2026 02:43:17 +0800 Subject: [PATCH] docs(rn): rebuild public sdk documentation --- docs-site/docs/.vitepress/config.ts | 10 +- docs-site/docs/rn/bugcollect.md | 128 ++++++++++ docs-site/docs/rn/common.md | 157 ++++++++++++ docs-site/docs/rn/group.md | 109 -------- docs-site/docs/rn/im.md | 329 ++++++++++++++----------- docs-site/docs/rn/index.md | 326 +++++------------------- docs-site/docs/rn/push.md | 172 +++++-------- docs-site/docs/rn/session.md | 83 +++++++ docs-site/docs/rn/setup.md | 205 +++++++-------- docs-site/docs/rn/troubleshooting.md | 142 +++++++++++ docs-site/docs/rn/update.md | 306 ++++++++++++++--------- docs-site/docs/rn/xwebview.md | 249 +++++++++++++++++++ docs-site/package.json | 2 + docs-site/scripts/check-rn-docs.mjs | 171 +++++++++++++ docs/RN_PUBLIC_DOCUMENTATION_POLICY.md | 62 +++++ docs/SDK_PLATFORM_V2_HANDOFF.md | 3 + 16 files changed, 1601 insertions(+), 853 deletions(-) create mode 100644 docs-site/docs/rn/bugcollect.md create mode 100644 docs-site/docs/rn/common.md delete mode 100644 docs-site/docs/rn/group.md create mode 100644 docs-site/docs/rn/session.md create mode 100644 docs-site/docs/rn/troubleshooting.md create mode 100644 docs-site/docs/rn/xwebview.md create mode 100644 docs-site/scripts/check-rn-docs.mjs create mode 100644 docs/RN_PUBLIC_DOCUMENTATION_POLICY.md 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