docs: 添加 CLAUDE.md + 各 module README + sdk-webview JSBridge 补全

Agent 7 + Agent 8:
- CLAUDE.md 项目上下文
- 各 module README (core/im/push/update/webview/log)
- XWebViewStandardHandlers.kt 补全标准 JSBridge handler
这个提交包含在:
XuqmGroup 2026-06-16 12:14:54 +08:00
父节点 fbafc8d802
当前提交 4a18d06c63
共有 10 个文件被更改,包括 488 次插入96 次删除

查看文件

@ -2,10 +2,12 @@
## 项目定位
XuqmGroup Android SDK,Gradle multi-module 项目。为集成宿主 App如 YwxMobileApp提供初始化、用户认证、OTA 更新、WebView、IM、推送、证书、**日志追踪**
XuqmGroup Android SDK,Gradle multi-module 项目。为集成宿主 App如 YwxMobileApp提供初始化、用户认证、OTA 更新、WebView、IM、推送、证书、日志追踪。
- Git 远端:`https://xuqinmin.com/xuqmGroup/XuqmGroup-AndroidSDK.git`
- Nexus Maven 发布:`https://nexus.xuqinmin.com/repository/android-hosted/`
- groupId`com.xuqm`
- 技术栈Kotlin 2.3.10,AGP 9.1.0,minSdk 24,compileSdk 36,Java 21
## 模块结构
@ -59,13 +61,6 @@ XuqmSDK.logEnabled // 是否开启日志
XuqmSDK.appKey // 当前 appKey
```
## 向下兼容约束
- `XuqmSDK.initialize()` 现有参数不得删除或修改类型
- `XuqmSDK.setUserInfo()` 现有 `XuqmUserInfo` 字段不得删除
- `SdkPlatformConfig` 新增字段一律为可选(`val xxx: Type? = null`
- 旧服务端不返回新字段时,客户端使用合理默认值
## 日志 SDKsdk-log
```kotlin
@ -86,16 +81,18 @@ XLog.defineFunnel("checkout", listOf("cart_view", "checkout_start", "payment_don
`logApiUrl` 由 SDK 在 init 后从平台配置自动获取,无需 App 传入。
## 向下兼容约束
- `XuqmSDK.initialize()` 现有参数不得删除或修改类型
- `XuqmSDK.setUserInfo()` 现有 `XuqmUserInfo` 字段不得删除
- `SdkPlatformConfig` 新增字段一律为可选(`val xxx: Type? = null`
- 旧服务端不返回新字段时,客户端使用合理默认值
## 集成验证项目
YwxMobileApp`/Users/xuqinmin/Projects/TrustProjects/Pad/YwxMobileApp`
使用 Nexus Maven 引入 SDK,开发阶段可改为 `project()` 本地引用。
## 任务文档
- Agent 7 任务:`/Users/xuqinmin/Projects/XuqmProjects/YiwangxinApp4/docs/agent-tasks/agent7-android-sdk.md`
- Agent 8 任务:`/Users/xuqinmin/Projects/XuqmProjects/YiwangxinApp4/docs/agent-tasks/agent8-docs.md`
## 常用命令
```bash
@ -103,4 +100,17 @@ YwxMobileApp`/Users/xuqinmin/Projects/TrustProjects/Pad/YwxMobileApp`
./gradlew :sdk-core:assembleDebug
./gradlew :sample-app:installDebug
./gradlew publish # 发布所有模块到 Nexus
# 发布单个模块
./gradlew :sdk-core:publish -PSDK_CORE_VERSION=1.0.0
```
## 发版配置
`gradle.properties` 中配置:
```properties
NEXUS_USER=your_username
NEXUS_PASSWORD=your_password
```
发布至 `https://nexus.xuqinmin.com/repository/android-hosted/`,版本号在各模块 `build.gradle.kts` 中维护。

93
sdk-core/README.md 普通文件
查看文件

@ -0,0 +1,93 @@
# sdk-core
XuqmGroup Android SDK 核心模块。提供 SDK 初始化、HTTP 请求、Token 管理、文件操作、通用工具。
## 依赖
```kotlin
implementation("com.xuqm:sdk-core:VERSION")
```
## 初始化
### 方式 A — ContentProvider 自动初始化(推荐)
`config.xuqm` 放入 `src/main/assets/xuqm/`,SDK 在 App 启动时自动读取并初始化。
```kotlin
// XuqmInitializerProvider 自动触发,无需代码
```
### 方式 B — 手动初始化
```kotlin
// Application.onCreate() 中:
XuqmSDK.initialize(context, appKey = "xxx")
XuqmSDK.initialize(context, appKey = "xxx", platformUrl = "https://xxx")
```
## API
### XuqmSDK全局单例
| API | 说明 |
|-----|------|
| `XuqmSDK.initialize(context, appKey, platformUrl?, logLevel?)` | 手动初始化 |
| `XuqmSDK.awaitInitialization()` | 等待平台配置拉取coroutine |
| `XuqmSDK.isInitialized()` | 检查是否已初始化 |
| `XuqmSDK.setUserInfo(info)` | 设置用户信息,触发所有子模块登录 |
| `XuqmSDK.setUserInfo(null)` | 登出,触发全局登出 |
| `XuqmSDK.getUserId()` | 获取当前 userId |
| `XuqmSDK.getUserInfo()` | 获取当前用户信息 |
| `XuqmSDK.appKey` | 当前 appKey |
| `XuqmSDK.platformConfig` | 平台配置init 后可用) |
| `XuqmSDK.logApiUrl` | 日志服务地址(从平台配置获取) |
| `XuqmSDK.logEnabled` | 是否启用日志上报 |
| `XuqmSDK.useLocalServiceEndpoints(ip)` | 切换本地联调环境 |
| `XuqmSDK.tokenStore` | Token 存储EncryptedSharedPreferences |
### XuqmUserInfo
```kotlin
data class XuqmUserInfo(
val userId: String, // 必填
val userSig: String? = null, // IM 登录凭证(可选)
val name: String? = null,
val phone: String? = null,
val avatar: String? = null,
)
```
### TokenStore
基于 `EncryptedSharedPreferences` 持久化存储。
```kotlin
XuqmSDK.tokenStore.saveToken("eyJ...")
val token = XuqmSDK.tokenStore.getToken()
XuqmSDK.tokenStore.clear()
```
### ApiClient
基于 OkHttp 5 + Retrofit 3,自动附加 Bearer Token。
```kotlin
val service = RetrofitFactory.create(MyApiService::class.java)
```
### FileSDK
文件上传、下载、打开的统一入口(`com.xuqm.sdk.file`)。
```kotlin
// 上传
val result = FileSDK.upload(context, uri, onProgress = { /* 0-100 */ })
val result = FileSDK.uploadBytes(fileName, mimeType, bytes, onProgress)
// 下载
val file = FileSDK.download(context, url, fileName, destination, notificationTitle, onProgress)
// 打开
FileSDK.openFile(context, file)
```

56
sdk-im/README.md 普通文件
查看文件

@ -0,0 +1,56 @@
# sdk-im
XuqmGroup Android SDK IM 模块。提供 WebSocket 实时通信、消息收发、群组管理。
## 依赖
```kotlin
implementation("com.xuqm:sdk-im:VERSION")
implementation("com.xuqm:sdk-core:VERSION") // 必须
```
## 使用
**无需手动初始化。** `XuqmSDK.login(userId, userSig)` 成功后自动完成 IM 登录。
```kotlin
// ImClient 直接使用
val imClient = ImClient()
imClient.listener = object : ImEventListener {
override fun onConnected() { }
override fun onMessage(msg: ImMessage) { }
override fun onRevoke(msgId: String, operatorId: String) { }
}
imClient.connect()
imClient.send(SendMessageParams(
toId = "user_002",
chatType = ChatType.SINGLE,
msgType = MsgType.TEXT,
content = "Hello!"
))
```
## API
### ImClient
| API | 说明 |
|-----|------|
| `imClient.connect()` | 连接 WebSocket |
| `imClient.disconnect()` | 断开连接 |
| `imClient.send(params)` | 发送消息 |
| `imClient.revoke(msgId)` | 撤回消息 |
| `imClient.listener` | 事件监听器 |
### 消息类型
`TEXT` / `IMAGE` / `VIDEO` / `AUDIO` / `FILE` / `CUSTOM` / `LOCATION` / `NOTIFY` / `RICH_TEXT` / `CALL_AUDIO` / `CALL_VIDEO` / `FORWARD` / `QUOTE` / `MERGE`
### 自动重连
断线后指数退避重连,初始间隔 3 秒,最大间隔 30 秒。调用 `disconnect()` 后停止。
### 群聊
支持 `@userId` 提及,写入 `mentionedUserIds`。群组管理 API 包含:创建、解散、禁言、设置管理员、转让群主。

查看文件

@ -1,94 +1,39 @@
# Xuqm License SDK
# sdk-license
Android SDK for device license registration and verification.
XuqmGroup Android SDK 证书授权模块。提供设备 License 验证能力。
## Features
- **Stable Device ID**: Uses `ANDROID_ID` (stable per app signing key + device + user), with UUID fallback
- **Token Caching**: Caches valid status for configurable window (default 10 min) to reduce network calls
- **Auto Recovery**: Automatically re-registers when token is invalid or revoked
- **Offline Support**: Returns cached OK status when network is unavailable
- **Encrypted License File**: Reads `assets/xuqm/license.xuqm`; the file does not expose appKey or server URL as readable text
- **AppKey Change Detection**: Automatically clears old data when decrypted appKey changes
## Integration
## 依赖
```kotlin
dependencies {
implementation("com.xuqm:sdk-license:0.1.0-SNAPSHOT")
}
implementation("com.xuqm:sdk-license:VERSION")
implementation("com.xuqm:sdk-core:VERSION") // 必须
```
## Usage
## 使用
### License File
Download the encrypted License file from the tenant platform and place it at:
```text
app/src/main/assets/xuqm/license.xuqm
```
The SDK also accepts a tenant-platform downloaded file such as
`app/src/main/assets/xuqm/MyApp.xuqmlicense`.
No explicit initialization code is required.
### Check License
**无需独立初始化。** 内部自动等待 `XuqmSDK` 就绪。
```kotlin
LicenseSDK.checkLicense { isValid ->
if (isValid) {
// allow app usage
} else {
// block app usage or show warning
// 验证 License
LicenseSDK.checkLicense(context, callback = object : LicenseCallback {
override fun onResult(result: LicenseResult) {
when (result) {
is LicenseResult.Success -> { /* 验证通过 */ }
is LicenseResult.Denied -> { /* 验证失败 */ }
is LicenseResult.Error -> { /* 错误 */ }
}
}
})
// 获取状态
val status = LicenseSDK.getStatus()
```
Optional user metadata can be uploaded during the same check. All fields are optional.
## API
```kotlin
LicenseSDK.checkLicense(
userInfo = LicenseUserInfo(
userId = "u_10001",
name = "张三",
email = "zhangsan@example.com",
phone = "13800000000",
),
) { isValid ->
// allow or block app usage
}
```
### Get Status (no network)
```kotlin
val status = LicenseSDK.getStatus() // OK, DENIED, or UNKNOWN
```
### Re-register (force)
```kotlin
lifecycleScope.launch {
val result = LicenseSDK.reRegister()
}
```
### Get Device ID
```kotlin
val deviceId = LicenseSDK.getDeviceId()
```
## Configuration
| Parameter | Default | Description |
|-----------|---------|-------------|
| `appKey` | from encrypted file | AppKey from tenant platform |
| `deviceName` | `${MANUFACTURER} ${MODEL}` | Device display name |
| `baseUrl` | `https://auth.dev.xuqinmin.com/` | License server base URL |
## Data Storage
All data (device ID, token, status) is stored in `EncryptedSharedPreferences` with AES-256 encryption.
| API | 说明 |
|-----|------|
| `LicenseSDK.checkLicense(context, callback)` | 验证设备 License |
| `LicenseSDK.getStatus()` | 获取当前 License 状态 |
| `LicenseSDK.getDeviceId()` | 获取设备 ID |
| `LicenseSDK.clear()` | 清除本地 License 缓存 |

55
sdk-log/README.md 普通文件
查看文件

@ -0,0 +1,55 @@
# sdk-log
XuqmGroup Android SDK 日志模块。提供日志采集、Crash 捕获、漏斗分析能力。
## 依赖
```kotlin
implementation("com.xuqm:sdk-log:VERSION")
implementation("com.xuqm:sdk-core:VERSION") // 必须
```
## 快速开始
```kotlin
// Application.onCreate() 中XuqmSDK.initialize 之后):
XLog.setLogLevel(LogLevel.INFO)
XLog.setEnvironment("production")
XLog.startCrashCapture()
// 业务代码中
XLog.event("page_view", mapOf("page" to "home"))
XLog.captureError(exception)
```
## API
### XLog 对象
| API | 说明 |
|-----|------|
| `XLog.setLogLevel(level)` | 设置日志级别(`DEBUG` / `INFO` / `WARN` / `ERROR` |
| `XLog.setEnvironment(env)` | 设置环境标签 |
| `XLog.startCrashCapture()` | 开启全局 UncaughtExceptionHandler |
| `XLog.event(name, properties?)` | 记录自定义事件 |
| `XLog.captureError(error, metadata?)` | 上报异常 |
| `XLog.warn(message, metadata?)` | 记录警告 |
| `XLog.info(message, metadata?)` | 记录信息 |
| `XLog.defineFunnel(id, steps)` | 定义漏斗 |
### LogLevel
```kotlin
enum class LogLevel { DEBUG, INFO, WARN, ERROR, NONE }
```
### 工作原理
- **LogQueue**:事件进入内存队列,按批次异步上传到 `logApiUrl`
- **CrashCapture**:注册 `Thread.UncaughtExceptionHandler`,捕获 Native Crash 并写入文件,下次启动时上传
- **Fingerprint**:为错误生成指纹(基于 message + stack,服务端去重聚合
- **FunnelTracker**:客户端维护漏斗进度,服务端跨 session 聚合
### 配置
`logApiUrl``logEnabled``sdk-core` 在 init 后从平台配置自动获取,无需 App 传入。

查看文件

@ -1,6 +1,5 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("maven-publish")
}
@ -8,7 +7,7 @@ group = rootProject.group
android {
namespace = "com.xuqm.sdk.log"
compileSdk = 35
compileSdk = 36
defaultConfig { minSdk = 24 }
publishing { singleVariant("release") { withSourcesJar() } }

34
sdk-push/README.md 普通文件
查看文件

@ -0,0 +1,34 @@
# sdk-push
XuqmGroup Android SDK 推送模块。支持多厂商推送(华为/小米/OPPO/vivo/荣耀/FCM/APNs
## 依赖
```kotlin
implementation("com.xuqm:sdk-push:VERSION")
implementation("com.xuqm:sdk-core:VERSION") // 必须
```
## 使用
**无需手动初始化。** `XuqmSDK.login()` 成功后自动完成推送 token 注册。`logout()` 自动解绑。
```kotlin
// 开启/关闭接收推送
PushSDK.setReceivePush(context, enabled = false)
PushSDK.setReceivePush(context, enabled = true)
```
## 支持厂商
HUAWEI / XIAOMI / OPPO / VIVO / HONOR / FCM / APNS
## 与 IM 联动
IM 和推送服务均已开启时,IM 离线消息自动触发推送通知。
## 注意事项
- 需要各厂商 SDK 配置App ID / App Secret
- Firebase Messaging 需提供 `google-services.json`
- FCM token 通过 `FirebaseMessagingService.onNewToken()` 自动接收

49
sdk-update/README.md 普通文件
查看文件

@ -0,0 +1,49 @@
# sdk-update
XuqmGroup Android SDK 更新模块。提供 App 版本检查和 APK 下载安装能力。
## 依赖
```kotlin
implementation("com.xuqm:sdk-update:VERSION")
implementation("com.xuqm:sdk-core:VERSION") // 必须
```
## 快速开始
```kotlin
val result = UpdateSDK.checkUpdate(
appKey = XuqmSDK.appKey,
platform = Platform.ANDROID,
)
if (result.needsUpdate) {
UpdateSDK.downloadAndInstall(context, result.downloadUrl)
}
```
## API
| API | 说明 |
|-----|------|
| `UpdateSDK.checkUpdate(appKey, platform)` | 检查 App 更新 |
| `UpdateSDK.downloadAndInstall(context, url)` | 下载 APK 并调起系统安装器 |
### UpdateResult
```kotlin
data class UpdateResult(
val needsUpdate: Boolean,
val versionName: String?,
val versionCode: Int?,
val downloadUrl: String?,
val changeLog: String?,
val forceUpdate: Boolean?,
)
```
## 工作原理
- `downloadAndInstall` 将 APK 下载到 `getExternalFilesDir(null)`,通过 `FileProvider` 触发系统安装
- AndroidManifest 中已配置 `@xml/file_paths``external-files-path`
- 支持 WebSocket 实时推送更新通知

69
sdk-webview/README.md 普通文件
查看文件

@ -0,0 +1,69 @@
# sdk-webview
XuqmGroup Android SDK WebView 模块。提供嵌入式 WebView 组件和独立页面,内置 JSBridge、文件选择、下载拦截。
## 依赖
```kotlin
implementation("com.xuqm:sdk-webview:VERSION")
implementation("com.xuqm:sdk-core:VERSION") // 必须
```
## 使用
### 嵌入式组件
```kotlin
import com.xuqm.sdk.webview.XWebViewView
import com.xuqm.sdk.webview.XWebViewConfig
XWebViewView(
config = XWebViewConfig(
url = "https://example.com",
title = "嵌入式网页",
),
)
```
### 独立页面
```kotlin
import com.xuqm.sdk.webview.openXWebView
import com.xuqm.sdk.webview.XWebViewConfig
openXWebView(XWebViewConfig(url = "https://example.com", title = "独立页面"))
// navigate("xwebview") 后使用 XWebViewScreen()
```
## XWebViewConfig 参数
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `url` | String | `""` | 初始加载地址 |
| `title` | String | `""` | 页面标题 |
| `hideToolbar` | Boolean | `false` | 隐藏顶栏 |
| `hideStatusBar` | Boolean | `false` | 隐藏状态栏 |
| `userAgent` | String? | `null` | 自定义 User-Agent |
| `injectedJavaScript` | String? | `null` | 注入的 JS |
| `jsBridgeName` | String | `"XWebViewBridge"` | JS 桥接对象名 |
| `debugEnabled` | Boolean | `false` | 开启远程调试 |
| `downloadDestination` | FileDownloadDestination | `Sandbox` | 下载存储目标 |
| `downloadNotificationTitle` | String? | `null` | 通知栏下载进度 |
| `onMessage` | (String) -> Unit? | `null` | H5 消息回调 |
## JSBridge 通信
```javascript
// H5 → Native
window.XWebViewBridge.postMessage(JSON.stringify({ type: 'login', token: '...' }))
// 监听下载进度
window.addEventListener('__xwvDownloadProgress', (e) => { ... })
window.addEventListener('__xwvDownloadDone', (e) => { ... })
```
## 文件选择与拍照
- `<input type="file">` + `accept="image/*"` + `capture` → 系统相机
- `<input type="file">` + `.docx/.xlsx` → 文件选择器
- `getUserMedia()` WebRTC → 自动请求 CAMERA 权限

查看文件

@ -0,0 +1,82 @@
package com.xuqm.sdk.webview
import android.os.Build
import android.widget.Toast
import com.xuqm.sdk.XuqmSDK
import org.json.JSONObject
/**
* Standard JSBridge handlers for xuqm.* messages.
* These provide default implementations for hosts that don't use a custom protocol.
*
* Usage: register in XWebViewView's xwvMessageHandler or onMessage callback.
*/
object XWebViewStandardHandlers {
fun handle(message: String, webView: XWebViewController?): String {
val json = runCatching { JSONObject(message) }.getOrNull() ?: return errorResponse("invalid JSON")
val name = json.optString("name", "")
return when (name) {
"xuqm.getDeviceInfo" -> handleGetDeviceInfo()
"xuqm.getToken" -> handleGetToken()
"xuqm.getUserInfo" -> handleGetUserInfo()
"xuqm.closeWebView" -> handleCloseWebView(webView)
"xuqm.showToast" -> handleShowToast(json)
else -> errorResponse("unknown handler: $name")
}
}
private fun handleGetDeviceInfo(): String {
val data = JSONObject().apply {
put("platform", "android")
put("osVersion", Build.VERSION.RELEASE)
put("sdkVersion", Build.VERSION.SDK_INT)
put("manufacturer", Build.MANUFACTURER)
put("model", Build.MODEL)
put("brand", Build.BRAND)
}
return successResponse(data)
}
private fun handleGetToken(): String {
val token = XuqmSDK.currentLoginSession?.userSig ?: ""
val data = JSONObject().apply { put("token", token) }
return successResponse(data)
}
private fun handleGetUserInfo(): String {
val userInfo = XuqmSDK.getUserInfo()
if (userInfo == null) return errorResponse("not logged in")
val data = JSONObject().apply {
put("userId", userInfo.userId ?: "")
put("nickname", userInfo.nickname ?: "")
put("avatar", userInfo.avatar ?: "")
put("phone", userInfo.phone ?: "")
}
return successResponse(data)
}
private fun handleCloseWebView(webView: XWebViewController?): String {
webView?.let { wv ->
if (wv.canGoBack()) wv.goBack() else wv.loadUrl("about:blank")
}
return successResponse(JSONObject())
}
private fun handleShowToast(json: JSONObject): String {
val message = json.optString("message", "")
if (message.isNotBlank()) {
val ctx = XuqmSDK.appContext
android.os.Handler(android.os.Looper.getMainLooper()).post {
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
}
}
return successResponse(JSONObject())
}
private fun successResponse(data: JSONObject): String =
JSONObject().apply { put("code", 0); put("data", data) }.toString()
private fun errorResponse(message: String): String =
JSONObject().apply { put("code", -1); put("message", message) }.toString()
}