小天互连移动端 SDK 接入指南

将小天互连 OA / IM 能力集成到第三方 iOS 与 Android 原生应用

iOS · HtOAFunctions 1.0.7 Android · com.oa8000.htim:main 1.0.1
切换后本页的平台相关内容会同步变化

1. 概述

移动端 SDK 以整页功能模块的形式提供能力:宿主 App 完成初始化与登录后,调用对应方法即可拉起小天互连的原生页面(消息、通讯录、待办、流程、会议、AI 助手等),无需自行实现界面。

能力清单

分类能力
IM消息列表、发起聊天、通讯录、未读角标、离线推送
OA动态门户 / 工作台、待办、发起申请、信息发布、邮件
音视频视频会议、音视频通话(需 HTRTC 授权)
其他扫一扫、AI 助手

接入主流程

iOS
引入 Pod → 配置权限 → 登录 → 设置代理(角标)→ 调用功能入口
Android
引入依赖 → 工程配置 → Application 初始化 → 登录 → 注册监听
  → setHtImRouteListener → initHtImData → 调用功能入口
两端差异提示:iOS 的初始化在登录时一并完成,Android 需要在 Application.onCreate() 中单独初始化; Android 在登录成功后还多出一步"登录后初始化"(见第 9 节)。其余能力两端一一对应, 完整映射见 API 对照速查

2. 环境要求

项目要求
Xcode15 及以上
CocoaPods1.11 及以上(Demo 使用 1.16.2 验证)
iOS 部署目标13.0 及以上
调试设备真机(详见下方说明)
私有源需可访问小天互连 CocoaPods 私有源(地址与凭据在对接时提供
必须用真机调试:SDK 的 xcframework 只提供 ios-arm64(真机)与 ios-x86_64-simulator 两个 slice, 不含 arm64 模拟器 slice。Apple Silicon Mac 上的 iOS 模拟器无法链接,需在 post_install 中对模拟器排除 arm64。
项目要求
Android Gradle Plugin7.4.2(Demo 验证版本)
Gradle7.5
compileSdk / targetSdk33
minSdk24(SDK 自身声明 23,宿主可下探到 23)
Java1.8(sourceCompatibility / targetCompatibility)
ABIarm64-v8a(详见下方说明)
私有源需可访问小天互连 Maven 私有源(地址与凭据在对接时提供
ABI 限制:SDK 携带的语音识别、人脸/证件识别等 native 库只提供 arm64-v8aarmeabi-v7a 两套 .so。 需要支持 32 位设备时把 armeabi-v7a 一并加入,但不要引入 x86 / x86_64——没有对应 slice,模拟器会在加载 native 库时崩溃。

3. 接入准备

正式接入前,需要向小天互连获取以下信息与文件。带 必需 的项目缺失会导致无法编译或核心功能不可用。

项目用途平台
私有源地址与凭据必需拉取 SDK 依赖两端
服务器地址必需登录时传入 serverUrl两端
测试账号必需联调验证两端
HTRTC appId / appKey音视频通话与会议两端
userSdkKey 开放接口
(含 app_id / app_secret)
免密登录两端
htrtc-2.2.3.aar必需音视频包,不随依赖链传递Android
native 库 .so必需语音识别、证件识别、录音编码Android
推送证书配置离线推送(在服务端管理后台配置)两端
私有源地址需单独获取。本文所有配置示例中的 <小天互连私有源地址> 均为占位符,请联系小天互连商务或技术对接人获取真实地址与访问凭据后再替换。

4. 引入 SDK

两端都只需声明一个入口依赖,其余模块由依赖链自动解析引入。

iOS · Podfile
platform :ios, '13.0'
use_frameworks!

# 私有源放最前面,官方源跟在后面
source '<小天互连私有源地址>'
source 'https://cdn.cocoapods.org/'

target 'YourApp' do
  pod 'HtOAFunctions', '1.0.7'
end
私有源必须写在官方源之前,否则 CocoaPods 可能优先命中官方源而找不到 SDK。
pod repo update            # 首次配置私有源后执行
pod install
open YourApp.xcworkspace   # 注意不是 .xcodeproj
版本精确锁定:请使用 '1.0.7' 这样的精确版本号而非 ~>,避免解析到未经验证的新版本。

自动带出的模块

模块版本说明
HtOAFunctions1.0.7OA 业务功能库,唯一入口
HTChatProject1.0.7IM 聊天模块
HtOACore1.0.4基础底座
HTAIKit1.0.6AI 助手,勿降级(1.0.6 修复了空表 NSNull 崩溃)
HtCardKit1.0.2卡片消息渲染
HtSocket1.0.1长连接
HtQRCode4.1.0扫码

同时会引入的开源三方库:AFNetworking 4.0.1、FMDB 2.7.5、Masonry 1.1.0、MBProgressHUD 1.2.0、MJExtension 3.4.1、 MJRefresh 3.7.5、SDWebImage 5.0.6、Toast 4.0.0、TZImagePickerController 3.8.9、RealReachability 1.3.0、 IQKeyboardManager 6.5.10、PGDatePicker 2.6.9、HTRTC 2.2.0。

无需手动添加系统库或三方 SDK。百度地图、微信 OpenSDK、讯飞 iflyMSC 等静态库已在编译期打进 xcframework 二进制, 不会出现在 Podfile / Podfile.lock 中。重复声明会导致运行时 duplicate class 警告。
Android · settings.gradle
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        // 小天互连私有源(地址请向小天互连获取)
        maven { url '<小天互连私有源地址>' }

        // SDK 依赖的三方仓库,缺一不可
        maven { url 'https://maven.aliyun.com/repository/public' }
        maven { url 'https://jitpack.io' }
        maven { url 'https://developer.huawei.com/repo/' }                          // 华为 HMS Push
        maven { url 'https://mvn.getui.com/nexus/content/repositories/releases/' }  // 个推
        google()
        mavenCentral()

        // 本地 aar 目录(存放 htrtc 音视频包)
        flatDir { dirs 'repositories' }
    }
}
Android · app/build.gradle
dependencies {
    implementation 'com.oa8000.htim:main:1.0.1'

    // htrtc 音视频包由私有源以外的渠道提供,需放入工程根目录 repositories/ 后本地引入
    implementation(name: 'htrtc-2.2.3', ext: 'aar')

    implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
}
htrtc 为什么要本地引入:该 aar 是以 flatDir 方式引入的本地包, 这类依赖不会写进发布的 pom,因此不会随 com.oa8000.htim:main 自动传递。 需要小天互连单独提供,放进工程根目录的 repositories/ 文件夹。

自动带出的模块

artifactId版本说明
main1.0.1门户与统一管理入口,唯一需要声明的依赖
lib-core1.0.1网络、存储、权限、扫码等基础底座
common1.0.1公共实体与工具
chat1.0.1IM 聊天与视频会议界面
simsdk1.0.1IM 长连接
email / trace / information
meeting / calendar / hrwork
office / car / task
file-center / document
1.0.1邮件、流程、信息发布、会议、日程、人事考勤、办公、用车、任务、文件中心、文档中心

随依赖链引入的三方库:OkHttp 3.9.1 / Retrofit 2.4.0 / RxJava 2.2.3 / Glide 4.9.0 / Gson 2.8.5 / Room 2.5.0-alpha01 / MMKV 1.2.11 / LiveEventBus 1.5.7 / SmartRefreshLayout 1.1.0 / XXPermissions 16.6 / ImmersionBar 3.0.0 等,以及个推 gtsdk 3.2.14.0 + gtc 3.2.1.0 与华为/小米/OPPO/vivo 厂商推送通道。

版本冲突:若宿主 App 已依赖上述库的其他大版本(尤其是 OkHttp / Retrofit / Glide / RxJava), Gradle 会按最高版本仲裁,可能导致运行时 NoSuchMethodError。 用 ./gradlew :app:dependencies 排查,必要时用 resolutionStrategy.force 锁回上表版本。

5. 工程配置

SDK 依赖链中包含若干已停止维护的三方库,在 Xcode 15+ 上直接编译会失败。请将下面的 post_install 原样加入 Podfile, pod install 时会自动完成修补,无需手工改源码。

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      # Toast / Masonry 等老 pod 仍停在 iOS 8/9,动态链接时会去找
      # Xcode 14.3 起已移除的 libarclite
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
      # 语音识别库无 arm64 模拟器 slice
      config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
    end
  end

  # ── 必须:修补两个已停止维护的依赖 ──────────────────────────
  # AFNetworking (最后更新 2020) 与 RealReachability (2021) 引用了系统私有头
  # netinet6/in6.h,Xcode 15+ 会报 "Use of private header from outside its
  # module" 而编译失败。两者均无更新版本可升,只能在安装后修补源码。
  root = installer.sandbox.root

  [
    'AFNetworking/AFNetworking/AFHTTPSessionManager.m',
    'AFNetworking/AFNetworking/AFNetworkReachabilityManager.m',
    'RealReachability/RealReachability/Connection/LocalConnection.m',
  ].each do |rel|
    path = File.join(root, rel)
    next unless File.exist?(path)
    text = File.read(path)
    patched = text.gsub(/^#import <netinet6\/in6\.h>\n/, '')
    File.write(path, patched) if patched != text
  end

  # PingFoundation.h 用到 sa_family_t 却未导入其所在模块
  ping = File.join(root, 'RealReachability/RealReachability/Ping/PingFoundation.h')
  if File.exist?(ping)
    text = File.read(ping)
    unless text.include?('_sa_family_t.h')
      text = text.sub(/(#import <Foundation\/Foundation\.h>\n)/,
                      "\\1#import <sys/_types/_sa_family_t.h>\n")
      File.write(ping, text)
    end
  end
end
修补项不做会怎样
IPHONEOS_DEPLOYMENT_TARGET老 pod 停留在 iOS 8/9,链接时查找 Xcode 14.3 起已移除的 libarclite 而失败
EXCLUDED_ARCHS[simulator]Apple Silicon 模拟器编译报找不到 arm64 slice
netinet6/in6.hAFNetworking / RealReachability 报 "Use of private header from outside its module"
PingFoundation.hsa_family_t 未声明

容器要求

SDK 的所有功能入口都是从当前 keyWindow 的导航栈打开页面,请确保 App 的根视图是 UINavigationController(或其子类):

self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
self.rootNaviController = [[UINavigationController alloc]
                           initWithRootViewController:[[HLoginViewController alloc] init]];
self.window.rootViewController = self.rootNaviController;
[self.window makeKeyAndVisible];

gradle.properties

android.useAndroidX=true
android.enableJetifier=true          # SDK 依赖链中仍有 support 库,必须开启
android.nonTransitiveRClass=true
android.injected.testOnly=false      # 允许直接安装 debug 包到设备
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8

app/build.gradle

android {
    compileSdk 33

    defaultConfig {
        minSdk 24
        targetSdk 33

        ndk {
            // SDK 的 native 库只有 arm64-v8a / armeabi-v7a
            abiFilters "arm64-v8a"
        }

        manifestPlaceholders = [
                // 个推 APPID(3.1.2.0 起占位符名为 GETUI_APPID)
                GETUI_APPID: "你的个推 AppId",
                // 高德地图 key(定位、位置消息、考勤打卡使用)
                AMAP_APIKEY: "你的高德 Key",
        ]
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    // SDK 的界面使用 DataBinding,宿主必须一并开启
    dataBinding { enabled = true }
    buildFeatures { viewBinding true }

    configurations {
        // 与 SDK 依赖链中的注解库冲突,必须排除
        implementation.exclude group: 'com.intellij', module: 'annotations'
    }
}
manifestPlaceholders 必填。GETUI_APPIDAMAP_APIKEY 由 SDK 内部 Manifest 引用, 宿主不声明会导致 manifest 合并失败,构建直接报错。两个 key 需分别在个推、高德开放平台申请,并绑定宿主 App 的包名与签名。

AndroidManifest.xml

SDK 与宿主都声明了 android:themeandroid:allowBackup,需要用 tools:replace 指定以宿主为准:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:name=".MyApp"
        android:allowBackup="false"
        android:theme="@style/Theme.YourApp"
        tools:replace="android:theme, android:allowBackup"
        tools:targetApi="33">
        ...
    </application>
</manifest>

native 库

以下 .so 需放入 app/src/main/jniLibs/<abi>/(由小天互连随 SDK 提供):

文件用途
libmsc.so讯飞语音识别(语音转文字)
libmp3lame.so语音消息 MP3 编码
libclientalg.so识别算法库
libIDCARDDLL.so身份证识别
libcloudwalksdk.so云从人脸识别

混淆

SDK 各模块自带 consumer-rules.pro,宿主开启 minifyEnabled 时只需补充地图相关规则:

-keep class com.baidu.** {*;}
-keep class vi.com.** {*;}
-keep class com.baidu.vi.** {*;}
-dontwarn com.baidu.**
首次接入建议先关闭混淆跑通功能,再逐步开启验证。

6. 权限配置

SDK 涉及相机、相册、麦克风、定位、日历等能力,需要在 Info.plist 中声明对应用途说明,否则调用时会直接崩溃。

Key用于
NSCameraUsageDescription扫一扫、拍照发送、审批附件上传
NSPhotoLibraryUsageDescription相册选图发送
NSPhotoLibraryAddUsageDescription保存图片到相册
NSMicrophoneUsageDescription语音消息、音视频通话与会议
NSLocationWhenInUseUsageDescription考勤打卡、发送位置
NSLocationAlwaysAndWhenInUseUsageDescription同上(后台定位场景)
NSCalendarsUsageDescription日程写入系统日历
NSLocalNetworkUsageDescription与服务器的本地网络数据交互
<key>NSCameraUsageDescription</key>
<string>请点击"好"以允许访问。如不允许,您将不能使用审批上传等功能。</string>
<key>NSMicrophoneUsageDescription</key>
<string>用于发送语音消息、发起通话和会议</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>用于发送位置、考勤打卡</string>

ATS

若服务端为 HTTP 或使用自签证书,需要放开 App Transport Security:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>
上架提示:NSAllowsArbitraryLoads 全量放开会被 App Store 审核问询。生产环境建议服务端启用受信任的 HTTPS 证书, 或改用 NSExceptionDomains 只对特定域名放开。

SDK 各模块的 Manifest 已声明所需权限,合并后会自动进入宿主 App,无需手工重复声明。宿主需要关注的是:这些权限会出现在应用商店的权限清单里,以及 Android 6.0+ 的运行时授权。

权限用于运行时授权
INTERNET
ACCESS_NETWORK_STATE
ACCESS_WIFI_STATE
网络通信、长连接
CAMERA扫一扫、拍照发送、视频通话
RECORD_AUDIO
MODIFY_AUDIO_SETTINGS
语音消息、音视频通话与会议
READ_EXTERNAL_STORAGE
WRITE_EXTERNAL_STORAGE
附件上传下载、图片选择
MANAGE_EXTERNAL_STORAGE文件中心(Android 11+ 全文件访问)(需跳设置页)
ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
考勤打卡、位置消息
POST_NOTIFICATIONSAndroid 13+ 通知展示
READ_PHONE_STATE设备唯一标识(登录设备绑定)
SYSTEM_ALERT_WINDOW来电悬浮窗(需跳设置页)
CALL_PHONE通讯录拨号
REQUEST_INSTALL_PACKAGES应用内升级安装
BLUETOOTH / BLUETOOTH_CONNECT音视频通话音频路由Android 12+
VIBRATE / WAKE_LOCK消息提醒、保活
SDK 内部使用 XXPermissions 16.6 处理运行时授权,进入对应功能页时会自动弹窗申请并附带用途说明。宿主一般无需自行申请。
合规提示:READ_SMS / SEND_SMS / MANAGE_EXTERNAL_STORAGE 等敏感权限在国内应用商店上架时需要单独说明用途。 如果宿主 App 用不到相关功能,可在自己的 Manifest 中用 <uses-permission android:name="…" tools:node="remove" /> 移除,但需先与小天互连确认不影响所用功能。

7. 初始化

iOS 端无需单独的初始化步骤,SDK 在登录时一并完成内部初始化。直接进入第 8 节 登录即可。

如果需要在登录流程之外提前写入配置(例如从缓存恢复会话),可使用 +[HtClienInitUtil initClientDataWithConfig:]。常规接入无需调用。
#import <HtOAFunctions/HtClienInitUtil.h>

[HtClienInitUtil initClientDataWithConfig:configM];   // 可选

Android 端必须在 Application.onCreate() 中初始化,且早于任何其他 SDK 调用。

HtImClientManager.initHtImSdk(Application, HtImInitConfig) 接口调用

初始化工具类、MMKV 存储、换肤框架、加密数据库、音视频 SDK 与前后台切换监听。方法内部有幂等保护,重复调用只生效一次。

参数类型说明
application必填ApplicationApplication 上下文
htImInitConfig可选HtImInitConfig初始化配置,传 null 时读取 SDK 内置默认值

HtImInitConfig 参数

参数类型说明
htrtcAppId可选StringHTRTC 音视频 appId,使用音视频通话 / 会议时必填
htrtcAppKey可选StringHTRTC 音视频 appKey,同上
extra可选String扩展字段
public class MyApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

        HtImInitConfig config = new HtImInitConfig();
        config.setHtrtcAppId("你的 htrtc appId");
        config.setHtrtcAppKey("你的 htrtc appKey");
        HtImClientManager.getInstance().initHtImSdk(this, config);

        // 服务端为自签证书时放行 HTTPS
        try {
            HttpsURLConnection.setDefaultSSLSocketFactory(SSLSocketClient.getSSLSocketFactory());
            HttpsURLConnection.setDefaultHostnameVerifier(SSLSocketClient.getHostnameVerifier());
        } catch (Exception e) {
            e.printStackTrace();
        }

        KLog.init(true);   // 开发期打开 SDK 日志,上线前置为 false
    }
}
htrtcAppId / htrtcAppKey 为空时 SDK 会跳过音视频初始化并打日志, 此时视频会议与音视频通话不可用,其余功能正常。
HtImClientManager.cleanHtImSdk() 接口调用

释放音视频资源。在 Application.onTerminate() 中调用。

@Override
public void onTerminate() {
    super.onTerminate();
    HtImClientManager.getInstance().cleanHtImSdk();
}

下拉刷新样式(建议)

SDK 的列表页使用 SmartRefreshLayout,宿主可在 Application 的静态块中统一配置全局头尾样式:

static {
    ClassicsFooter.REFRESH_FOOTER_LOADING = "加载中...";
    SmartRefreshLayout.setDefaultRefreshHeaderCreator(
            (context, layout) -> new ClassicsHeader(context));
    SmartRefreshLayout.setDefaultRefreshFooterCreator(
            (context, layout) -> new ClassicsFooter(context).setDrawableSize(20));
}

8. 登录

两端都支持账号密码登录令牌免密登录两种方式,登录是使用其余所有接口的前提。

-[HtImClientManager doHtLoginWithLoginConfigM:callBlock:] 接口调用

使用配置模型登录,成功后 SDK 内部会完成长连接建立、本地库初始化与用户信息缓存。

-(void)doHtLoginWithLoginConfigM:(HtLoginCofigModel *)cofigM
                       callBlock:(HtImClientBlock)callBlock;

HtLoginCofigModel 参数

参数类型说明
serverUrl必填NSString服务器地址,如 https://im.example.com
loginId必填NSString登录账号 ID
password二选一NSString登录密码。账号密码登录时使用
userSdkKey二选一NSString用户身份令牌,由后端签发。用于免密集成
deviceToken可选NSStringAPNs 设备唯一标识,用于离线推送。见第 13 节
languageType可选NSString语言:CN 中文(默认)/ EN 英文
htrtcAppId可选NSStringHTRTC 音视频 appId,使用视频会议时必填
htrtcAppKey可选NSStringHTRTC 音视频 appKey,使用视频会议时必填
extra可选NSString扩展字段,透传给服务端

回调结果码 HtReqCode

枚举含义
HtReqCodeSuccess1登录成功
HtReqCodeServerIPErr0服务器地址错误 / 不可达
HtReqCodeServerErr-1服务端异常
HtReqCodeLoginErr-2登录失败(账号或密码错误)

回调第二个参数 response 为服务端返回的原始内容,失败时可用于展示错误原因。

账号密码登录

HtLoginCofigModel *configM = HtLoginCofigModel.new;
configM.serverUrl    = self.serverField.text;
configM.loginId      = self.accountField.text;
configM.password     = self.pwdField.text;
configM.languageType = @"CN";

[[HtImClientManager shareInstance] doHtLoginWithLoginConfigM:configM
                                                  callBlock:^(NSInteger type, id response) {
    NSLog(@"登录===%@", response);
    if (type == HtReqCodeSuccess) {
        HFunctionsViewController *funcVC =
            [[HFunctionsViewController alloc] initWithNibName:@"HFunctionsViewController"
                                                       bundle:[NSBundle bundleForClass:self.class]];
        [self.navigationController pushViewController:funcVC animated:YES];
    }
}];

令牌免密登录

HtLoginCofigModel *configM = HtLoginCofigModel.new;
configM.serverUrl  = @"https://im.example.com";
configM.loginId    = user.account;
configM.userSdkKey = tokenFromYourBackend;   // 后端下发,勿硬编码

[[HtImClientManager shareInstance] doHtLoginWithLoginConfigM:configM
                                                  callBlock:^(NSInteger type, id response) {
    if (type == HtReqCodeSuccess) { /* ... */ }
}];
HtImClientManager.doHtImLogin(HtImLoginConfig, ServerInitCallbackListener) 接口调用

依次完成服务器初始化 → 登录初始化 → 用户登录 → IM 长连接鉴权。全流程为异步,结果通过回调返回。

HtImLoginConfig 参数

参数类型说明
serverUrl必填String服务器地址,如 http://im.example.com:31921
loginId二选一String登录账号 ID。与 password 成对使用
password二选一String登录密码
userSdkKey二选一String用户身份令牌,由后端签发。传了它就不再校验 loginId / password
languageType可选StringCN 中文(默认)/ EN 英文
vendorDeviceToken可选String厂商推送 token,见第 13 节
cidDeviceToken可选String个推 CID,见第 13 节
extra可选String扩展字段
参数校验规则(来自 SDK 实现):serverUrl 为空 → 回调"服务器地址校验失败"; userSdkKey 为空且 loginId / password 任一为空 → 回调"用户校验失败"; languageType 为空时自动补 CN

ServerInitCallbackListener 回调

回调 / 常量说明
onSuccess()登录成功,可以进入业务页面
onError(code, error)失败,error 为可直接展示的中文提示
LOGIN_ERROR-1登录错误(地址不可达、账号密码错误、服务器初始化失败等)
LOGIN_UPDATE_PASSWORD-2使用初始密码登录被拒,需先修改密码
未先调用 initHtImSdk 就登录会抛 IllegalStateException: The htim sdk is not initialized

账号密码登录

HtImLoginConfig config = new HtImLoginConfig();
config.setServerUrl(url);            // 服务器地址
config.setLoginId(loginId);          // 登录账号 Id
config.setPassword(password);        // 登录密码
config.setLanguageType("CN");        // "CN"-中文 "EN"-英文

HtImClientManager.getInstance().doHtImLogin(config, new ServerInitCallbackListener() {
    @Override
    public void onSuccess() {
        loadingDialog.dismiss();
        startActivity(new Intent(LoginActivity.this, MainActivity.class));
    }

    @Override
    public void onError(int code, String error) {
        loadingDialog.dismiss();
        ToastUtils.showShort(error);
    }
});

令牌免密登录

HtImLoginConfig config = new HtImLoginConfig();
config.setServerUrl("http://im.example.com:31921");
config.setUserSdkKey(tokenFromYourBackend);   // 后端下发,勿硬编码
config.setLanguageType("CN");

HtImClientManager.getInstance().doHtImLogin(config, listener);
userSdkKey 是用户身份凭证,请勿硬编码在客户端源码或打包进二进制,建议每次启动时从宿主后端接口动态获取,并设置合理的有效期。 获取 userSdkKey 的开放接口地址、app_idapp_secret 由小天互连在对接时提供。 生产环境推荐使用令牌免密登录,客户端不接触明文密码。

9. 登录后初始化

iOS 端无此步骤。登录成功后直接调用功能入口即可,如需未读角标请设置代理(见第 11 节)。

本节内容仅适用于 Android。切换到 Android 标签可查看。

登录成功后,在承载功能入口的 Activity 中还需要完成两步初始化,否则模块间跳转、会话列表、未读数都不会工作。

HtImClientManager.setHtImRouteListener(Context) 接口调用

注册模块间跳转路由与各类事件观察者:单据选择/查看、文档选择/查看、系统消息跳转、模块跳转,以及未读数、待办数、登录失效、长连接状态的监听。 不调用会导致聊天中发送单据/文档、点击系统消息等功能无响应。

HtImClientManager.getInstance().setHtImRouteListener(this);
HtImClientManager.initHtImData(Activity) 接口调用

加载本地通讯录/群组缓存、注册 IM 长连接客户端、载入会话列表并计算未读总数。 内部通过定时器等待 IM 服务地址就绪后再注册,无需宿主自行重试。

HtImClientManager.getInstance().initHtImData(this);
调用顺序:addHtImChatListener,再 setHtImRouteListener,最后 initHtImData。 监听注册晚于 initHtImData 会漏掉首次未读数回调。
这两个方法在未登录时都会抛 IllegalStateException: Please call the doHtImLogin method first, 因此必须放在登录成功之后。
HtImClientManager.removeHtImRoutListener() 接口调用

移除 setHtImRouteListener 注册的全部观察者。宿主销毁 SDK 承载页或退出登录时调用,避免观察者泄漏。

HtImClientManager.getInstance().removeHtImRoutListener();

10. 退出登录

退出登录 接口调用

断开长连接、通知服务端登出、清理本地登录态与缓存。

平台方法
iOS-[HtImClientManager doHtLogout](无参数、无回调)
AndroiddoHtImLogout(Activity activity)
[[HtImClientManager shareInstance] doHtLogout];
[self.navigationController popViewControllerAnimated:YES];
建议在承载 SDK 入口的控制器 dealloc 中也调用一次,避免页面销毁后长连接与角标轮询继续运行。
HtImClientManager.getInstance().doHtImLogout(MainActivity.this);
finish();
退出后如需再次使用,重新调用 doHtImLogin 即可,不需要重复调用 initHtImSdk

11. 事件监听与角标

两端都通过回调把消息未读数、待办数推给宿主,由宿主自行决定角标的展示形式。Android 额外提供登录失效与长连接状态回调。

事件iOSAndroid
注册方式delegate 属性(weak,单个)addHtImChatListener()(可多个)
消息未读数-receiveMsgUnreadNum:onRecentMsgUnreadNum(int)
待办数-receiveTodoUnreadNum:onRecentToDoNum(int)
登录失效onLogoutStatus(Map)
长连接状态onHIMSocketStatus(boolean)
HtImClientDelegate 代理回调

设置 HtImClientManager.delegate 后,SDK 会在未读数变化时主动回调。两个方法均为 @optional

delegateweak 属性,请确保持有者存活;建议在登录成功后设置。
@interface HFunctionsViewController () <HtImClientDelegate>
@property (weak, nonatomic) IBOutlet UILabel *msgUnreadLabel;
@property (weak, nonatomic) IBOutlet UILabel *todoUnreadLabel;
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    [HtImClientManager shareInstance].delegate = self;
}

#pragma mark - HtImClientDelegate
- (void)receiveMsgUnreadNum:(NSInteger)unreadNum {
    self.msgUnreadLabel.text   = [NSString stringWithFormat:@"%ld", unreadNum];
    self.msgUnreadLabel.hidden = (unreadNum <= 0);
}

- (void)receiveTodoUnreadNum:(NSInteger)unreadNum {
    self.todoUnreadLabel.text   = [NSString stringWithFormat:@"%ld", unreadNum];
    self.todoUnreadLabel.hidden = (unreadNum <= 0);
}
如需同步 App 图标角标,在回调中一并调用 [UIApplication sharedApplication].applicationIconBadgeNumber = unreadNum;(需已申请通知权限)。
HtImChatListener 回调接口

接口的四个方法都是 default 方法,按需重写即可。支持注册多个监听器。

登录失效状态码

onLogoutStatus 的入参形如 {"type": 1, "message": "您的账号已经在其他设备登录…"}message 可直接展示。

type含义
1账号在其他设备登录(多端互踢)
9账号被系统强制退出,请联系管理员
-1IM token 已过期
-2IM 地址已失效
-3IM token 已失效
-11token 已过期
-12地址已失效

注册与反注册

方法说明
addHtImChatListener(listener)添加监听,内部去重,同一实例不会重复添加
removeHtImChatListener(listener)移除指定监听
removeAllHtImChatListener()清空全部监听
HtImClientManager.getInstance().addHtImChatListener(new HtImChatListener() {
    @Override
    public void onRecentMsgUnreadNum(int unreadNum) {
        TextView tv = findViewById(R.id.chatMsgUnreadNum);
        if (tv != null) {
            tv.setVisibility(unreadNum > 0 ? View.VISIBLE : View.GONE);
            tv.setText(String.valueOf(unreadNum));
        }
    }

    @Override
    public void onRecentToDoNum(int num) {
        TextView tv = findViewById(R.id.chatMsgToDoNum);
        if (tv != null) {
            tv.setVisibility(num > 0 ? View.VISIBLE : View.GONE);
            tv.setText(String.valueOf(num));
        }
    }

    @Override
    public void onLogoutStatus(Map<String, Object> map) {
        ToastUtils.showShort(String.valueOf(map.get("message")));
        startActivity(new Intent(MainActivity.this, LoginActivity.class));
        finish();
    }

    @Override
    public void onHIMSocketStatus(boolean isConnect) {
        // 可用于展示"连接中/网络异常"提示条
    }
});
监听器持有 Activity 引用,务必在 onDestroy() 中移除,否则会内存泄漏: removeAllHtImChatListener()

12. 功能入口

登录成功后即可调用以下方法拉起对应的原生页面。除特别说明外,均为无返回值的页面跳转。

Android 端所有 openHtIm* / sendHtImChat 方法在未登录时都会抛 IllegalStateException: Please call the doHtImLogin method first
12.1 动态门户 / 工作台 页面跳转

打开动态门户(工作台首页),展示服务端配置的门户布局与应用入口。

[[HtImClientManager shareInstance] goHomeView];
HtImClientManager.getInstance().openHtImWorkStation(this);
12.2 消息列表 页面跳转

打开 IM 消息会话列表页,可在其中选择会话进入聊天。

[[HtImClientManager shareInstance] goMsgView];
HtImClientManager.getInstance().openHtImChatMessage(context);
12.3 通讯录 页面跳转

打开组织架构通讯录,支持按部门浏览与搜索人员。

[[HtImClientManager shareInstance] goAddressView];
HtImClientManager.getInstance().openHtImContact(this);
12.4 发起聊天 页面跳转

打开选人界面,由用户从通讯录挑选聊天对象后进入会话。

// 选人聊天
[[HtImClientManager shareInstance] goChatView];

// 跳过选人,直接进入与指定对象的会话
// uuid 为聊天对象的用户 ID 或群 ID
[[HtImClientManager shareInstance] goChatViewWith:@"zzc"];
uuid 需与小天互连侧的账号体系一致。若宿主 App 使用自有 ID,需先由服务端完成映射。传入不存在的 uuid 会打开空会话。
// 选人聊天。当前登录用户会被自动置为已选中且不可取消
HtImClientManager.getInstance().sendHtImChat(context);
Android 暂无"按 ID 直达会话"的公开接口。当前版本只提供选人入口; 如果业务需要从详情页直接跳到与某个人的会话(iOS 的 goChatViewWith: 能力),请联系小天互连确认版本支持情况。
12.5 扫一扫 页面跳转

打开二维码扫描页。SDK 内部会处理登录授权、加好友等已知格式的二维码。

扫描结果通过 block 回传,type 为结果码(HtReqCodeSuccess 表示成功),response 为扫描到的内容。

[[HtImClientManager shareInstance] goScanCallBlock:^(NSInteger type, id response) {
    if (type == HtReqCodeSuccess) {
        NSLog(@"扫码结果:%@", response);
    }
}];
需在 Info.plist 中声明 NSCameraUsageDescription,否则打开扫描页会崩溃。

支持从相册选图识别。SDK 内部会先申请相机权限并展示用途说明;扫描结果由 SDK 内部处理,不回传给宿主

HtImClientManager.getInstance().openHtImScanQr(context);
12.6 发送邮件 页面跳转

打开写邮件页面,支持选择收件人、添加附件并发送。

[[HtImClientManager shareInstance] goSendMailView];
HtImClientManager.getInstance().openHtImEmailSend(this);
12.7 待办列表 页面跳转

打开待办事项列表,可查看并处理待审批、待办理的流程。待办数量通过第 11 节的回调获取。

[[HtImClientManager shareInstance] goTodoView];
HtImClientManager.getInstance().openHtImPlatformTodo(this);
12.8 发起申请 页面跳转

打开流程模板选择页,列出服务端配置的可发起流程(请假、报销、用车等),选择后进入表单填写。

[[HtImClientManager shareInstance] goApplyView];
HtImClientManager.getInstance().openHtImTraceTemplateSelect(this);
12.9 信息发布 页面跳转

打开信息发布 / 发送通知页面。发布权限由服务端按账号角色控制。

[[HtImClientManager shareInstance] goSendNotice];
HtImClientManager.getInstance().openHtImInformation(this);
12.10 视频会议 页面跳转

打开视频会议模块,可创建会议、加入会议与查看会议列表。

[[HtImClientManager shareInstance] goVideoMeeting];
前置条件:登录时需在 HtLoginCofigModel 中配置 htrtcAppIdhtrtcAppKey, 并已声明 NSCameraUsageDescriptionNSMicrophoneUsageDescription
HtImClientManager.getInstance().openHtImChatMeeting(context);
前置条件:初始化时需在 HtImInitConfig 中配置 htrtcAppIdhtrtcAppKey, 且工程已引入 htrtc-2.2.3.aar。缺任一项,会议页面可打开但无法建立音视频通道。
12.11 AI 助手 页面跳转

打开 AI 助手会话页,支持自然语言问答与卡片形式的业务结果展示。

[[HtImClientManager shareInstance] goAIChat];
HTAIKit 版本锁死在 1.0.6,勿降级 —— 1.0.6 修复了空表返回 NSNull 导致的崩溃。

首次调用会先完成 AI SDK 初始化,成功后自动进入聊天页;已初始化过则直接打开。初始化失败时 Toast 提示"AI 初始化失败"。

HtImClientManager.getInstance().openHtImAiAssistant(MainActivity.this);
当前版本 Android 端 AI 助手仍处于联调状态:SDK 中该方法的 AI 服务器地址、登录密码与 agentId 为硬编码的联调值。 接入生产环境前请与小天互连确认该模块是否已切换为动态配置,否则 AI 助手在客户环境下无法连通。

13. 离线推送

iOSAndroid
通道APNs个推 + 华为/小米/OPPO/vivo 厂商通道
上报字段deviceTokenvendorDeviceToken + cidDeviceToken
点击处理HtOAPushHandleManager个推 GTIntentService 回调

① 注册并上报 deviceToken

AppDelegate 中申请通知权限并注册 APNs,把拿到的 token 写入登录配置:

- (void)application:(UIApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    NSMutableString *token = [NSMutableString string];
    const unsigned char *bytes = deviceToken.bytes;
    for (NSUInteger i = 0; i < deviceToken.length; i++) {
        [token appendFormat:@"%02x", bytes[i]];
    }
    self.deviceTokenString = token;   // 登录时赋给 configM.deviceToken
}
deviceTokenHtLoginCofigModel 的字段,需要在登录前拿到。 若 token 晚于登录返回,可在拿到后重新调用一次登录以完成绑定。

② 交给 SDK 处理推送内容

-[HtOAPushHandleManager pushHandleData:] 接口调用

把 APNs 回调中的 userInfo 原样传入,SDK 会解析并跳转到对应的会话或业务页面。

#import <HtOAFunctions/HtOAPushHandleManager.h>

// 用户点击通知
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
         withCompletionHandler:(void (^)(void))completionHandler {
    NSDictionary *userInfo = response.notification.request.content.userInfo;
    [[HtOAPushHandleManager new] pushHandleData:userInfo];
    completionHandler();
}

③ 服务端配置推送证书

需要把 App 的 APNs 推送证书(或 p8 密钥)配置到小天互连服务端,并确保 Bundle ID 与证书一致。此步骤由服务端同学在管理后台完成。

Xcode 中需为 Target 开启 Push NotificationsBackground Modes → Remote notifications Capability。

SDK 采用个推 + 厂商通道双通道方案:个推负责统一调度,厂商通道负责应用被杀后的到达率。 厂商通道的 SDK 已打进 com.oa8000.htim:main,宿主只需完成个推接入与两个 token 的上报。

① 申请并配置个推 AppId

在个推开放平台创建应用(包名需与宿主 App 一致),把 AppId 填入 manifestPlaceholders,并在 Manifest 中声明:

<application ...>
    <meta-data
        android:name="PUSH_APPID"
        android:value="你的个推 AppId" />

    <service
        android:name=".push.getui.HtGTPushService"
        android:exported="false"
        android:label="PushService"
        android:process=":pushservice" />
    <service android:name=".push.getui.HtGTIntentService" />
</application>

<queries>
    <!-- 个推适配 Android 11+ -->
    <intent>
        <action android:name="com.getui.sdk.action" />
    </intent>
</queries>

② 实现个推服务类

两个类都必须由宿主 App 自己实现并在 Manifest 中声明,个推才能回调:

public class HtGTPushService extends PushService {
}
public class HtGTIntentService extends GTIntentService {

    // 接收 CID —— 这是要上报给 SDK 的推送标识
    @Override
    public void onReceiveClientId(Context context, String clientid) {
        App.cidDeviceToken = clientid;
    }

    @Override
    public void onReceiveMessageData(Context context, GTTransmitMessage msg) {
        String data = new String(msg.getPayload());
        // 透传消息,按需自行处理
    }

    @Override public void onReceiveServicePid(Context context, int pid) { }
    @Override public void onReceiveOnlineState(Context context, boolean online) { }
    @Override public void onReceiveCommandResult(Context context, GTCmdMessage msg) { }
    @Override public void onNotificationMessageArrived(Context context, GTNotificationMessage msg) { }
    @Override public void onNotificationMessageClicked(Context context, GTNotificationMessage msg) { }
}

③ 登录时上报 token

把两类 token 写入登录配置,SDK 会拼成 vendorDeviceToken;cidDeviceToken 一并提交服务端:

字段来源格式示例
vendorDeviceToken厂商通道 tokenOPPO_CN_81d9c923b0f6…
cidDeviceToken个推 onReceiveClientId 回调e7cee28a8ffb5272…
config.setVendorDeviceToken(vendorToken);  // 厂商推送 token
config.setCidDeviceToken(cid);             // 个推 CID,非必填
时序问题:个推 CID 通过异步回调返回,可能晚于用户点击登录。建议在启动页预热个推、拿到 CID 后再放行登录; 若 CID 到得晚,可在拿到后重新调用一次 doHtImLogin 完成绑定。
角标:SDK 内置了华为 / 荣耀桌面角标的清零逻辑,切回前台时自动执行,宿主无需处理。 厂商推送证书需在小天互连服务端管理后台配置,并确保包名与签名一致。

14. 完整集成示例

AppDelegate

#import "AppDelegate.h"
#import "HLoginViewController.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
    self.window.backgroundColor = UIColor.whiteColor;

    // SDK 的功能页依赖导航栈,根控制器需为 UINavigationController
    self.rootNaviController = [[HtNaviViewController alloc]
                               initWithRootViewController:[[HLoginViewController alloc] init]];
    self.window.rootViewController = self.rootNaviController;
    [self.window makeKeyAndVisible];
    return YES;
}

@end

登录页

#import <HtOAFunctions/HtImClientManager.h>

- (IBAction)goLogin:(UIButton *)sender {
    HtLoginCofigModel *configM = HtLoginCofigModel.new;
    configM.serverUrl = self.serverField.text;
    configM.loginId   = self.accountField.text;
    configM.password  = self.pwdField.text;

    [[HtImClientManager shareInstance] doHtLoginWithLoginConfigM:configM
                                                      callBlock:^(NSInteger type, id response) {
        NSLog(@"登录===%@", response);
        if (type == HtReqCodeSuccess) {
            HFunctionsViewController *funcVC =
                [[HFunctionsViewController alloc] initWithNibName:@"HFunctionsViewController"
                                                           bundle:[NSBundle bundleForClass:self.class]];
            [self.navigationController pushViewController:funcVC animated:YES];
        }
    }];
}

功能入口页

#import <HtOAFunctions/HtImClientManager.h>

@interface HFunctionsViewController () <HtImClientDelegate>
@property (weak, nonatomic) IBOutlet UILabel *msgUnreadLabel;
@property (weak, nonatomic) IBOutlet UILabel *todoUnreadLabel;
@end

@implementation HFunctionsViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"功能列表";

    self.msgUnreadLabel.backgroundColor  = UIColor.redColor;
    self.msgUnreadLabel.layer.cornerRadius = 10;
    self.msgUnreadLabel.layer.masksToBounds = YES;
    self.msgUnreadLabel.text = @"";

    [HtImClientManager shareInstance].delegate = self;
}

#pragma mark - 功能入口
- (IBAction)goHome:(id)sender        { [[HtImClientManager shareInstance] goHomeView]; }
- (IBAction)goMsg:(id)sender         { [[HtImClientManager shareInstance] goMsgView]; }
- (IBAction)goAddress:(id)sender     { [[HtImClientManager shareInstance] goAddressView]; }
- (IBAction)goChat:(id)sender        { [[HtImClientManager shareInstance] goChatView]; }
- (IBAction)goMail:(id)sender        { [[HtImClientManager shareInstance] goSendMailView]; }
- (IBAction)goTodo:(id)sender        { [[HtImClientManager shareInstance] goTodoView]; }
- (IBAction)goApply:(id)sender       { [[HtImClientManager shareInstance] goApplyView]; }
- (IBAction)goInformation:(id)sender { [[HtImClientManager shareInstance] goSendNotice]; }
- (IBAction)goVideoMeeting:(id)sender{ [[HtImClientManager shareInstance] goVideoMeeting]; }
- (IBAction)goAIChat:(id)sender      { [[HtImClientManager shareInstance] goAIChat]; }

- (IBAction)goScan:(id)sender {
    [[HtImClientManager shareInstance] goScanCallBlock:^(NSInteger type, id response) {
        NSLog(@"扫码结果:%@", response);
    }];
}

- (IBAction)goLogout:(id)sender {
    [[HtImClientManager shareInstance] doHtLogout];
    [self.navigationController popViewControllerAnimated:YES];
}

#pragma mark - HtImClientDelegate
- (void)receiveMsgUnreadNum:(NSInteger)unreadNum {
    self.msgUnreadLabel.text   = [NSString stringWithFormat:@"%ld", unreadNum];
    self.msgUnreadLabel.hidden = (unreadNum <= 0);
}

- (void)receiveTodoUnreadNum:(NSInteger)unreadNum {
    self.todoUnreadLabel.text   = [NSString stringWithFormat:@"%ld", unreadNum];
    self.todoUnreadLabel.hidden = (unreadNum <= 0);
}

- (void)dealloc {
    [[HtImClientManager shareInstance] doHtLogout];
}

@end

Swift 调用

SDK 以 Objective-C framework 形式提供。Podfile 中使用了 use_frameworks!,因此 Swift 工程可直接 import 使用,无需桥接头文件。

import HtOAFunctions

let config = HtLoginCofigModel()
config.serverUrl = "https://im.example.com"
config.loginId   = account
config.userSdkKey = tokenFromYourBackend

HtImClientManager.shareInstance().doHtLogin(withLoginConfigM: config) { type, response in
    if type == HtReqCodeSuccess.rawValue {
        HtImClientManager.shareInstance().goMsgView()
    }
}
方法名映射遵循 Clang 的 ObjC→Swift 规则。实际签名请以 Xcode 中 ⌃⌘↑ 生成的 Swift 接口为准。

Application

public class MyApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        HtImInitConfig config = new HtImInitConfig();
        config.setHtrtcAppId("你的 htrtc appId");
        config.setHtrtcAppKey("你的 htrtc appKey");
        HtImClientManager.getInstance().initHtImSdk(this, config);

        try {
            HttpsURLConnection.setDefaultSSLSocketFactory(SSLSocketClient.getSSLSocketFactory());
            HttpsURLConnection.setDefaultHostnameVerifier(SSLSocketClient.getHostnameVerifier());
        } catch (Exception e) {
            e.printStackTrace();
        }

        KLog.init(true);
    }

    @Override
    public void onTerminate() {
        super.onTerminate();
        HtImClientManager.getInstance().cleanHtImSdk();
    }

    static {
        ClassicsFooter.REFRESH_FOOTER_LOADING = "加载中...";
        SmartRefreshLayout.setDefaultRefreshHeaderCreator(
                (context, layout) -> new ClassicsHeader(context));
        SmartRefreshLayout.setDefaultRefreshFooterCreator(
                (context, layout) -> new ClassicsFooter(context).setDrawableSize(20));
    }
}

登录页

public class LoginActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        findViewById(R.id.textView).setOnClickListener(view -> {
            LoadingDialog loadingDialog = new LoadingDialog(this);
            loadingDialog.show("正在登录");

            HtImLoginConfig config = new HtImLoginConfig();
            config.setServerUrl(sUrl.getText().toString());      // 服务器地址
            config.setLoginId(sLoginId.getText().toString());    // 登录账号 Id
            config.setPassword(sPassword.getText().toString());  // 登录密码
            config.setLanguageType("CN");                        // "CN"-中文 "EN"-英文
            config.setVendorDeviceToken(vendorToken);            // 厂商推送 token
            config.setCidDeviceToken(cid);                       // 个推 CID,非必填

            HtImClientManager.getInstance().doHtImLogin(config, new ServerInitCallbackListener() {
                @Override
                public void onSuccess() {
                    loadingDialog.dismiss();
                    startActivity(new Intent(LoginActivity.this, MainActivity.class));
                }

                @Override
                public void onError(int code, String error) {
                    loadingDialog.dismiss();
                    ToastUtils.showShort(error);
                }
            });
        });
    }
}

功能入口页

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 1. 先注册监听
        HtImClientManager.getInstance().addHtImChatListener(new HtImChatListener() {
            @Override
            public void onRecentMsgUnreadNum(int unreadNum) {
                TextView tv = findViewById(R.id.chatMsgUnreadNum);
                tv.setVisibility(unreadNum > 0 ? View.VISIBLE : View.GONE);
                tv.setText(String.valueOf(unreadNum));
            }

            @Override
            public void onRecentToDoNum(int num) {
                TextView tv = findViewById(R.id.chatMsgToDoNum);
                tv.setVisibility(num > 0 ? View.VISIBLE : View.GONE);
                tv.setText(String.valueOf(num));
            }
        });

        // 2. 再注册路由,最后加载数据
        HtImClientManager.getInstance().setHtImRouteListener(this);
        HtImClientManager.getInstance().initHtImData(this);

        HtImClientManager mgr = HtImClientManager.getInstance();
        findViewById(R.id.textView)             .setOnClickListener(v -> mgr.openHtImWorkStation(this));         // 工作台
        findViewById(R.id.textView2)            .setOnClickListener(v -> mgr.openHtImChatMessage(this));         // 消息
        findViewById(R.id.textView3)            .setOnClickListener(v -> mgr.openHtImContact(this));             // 通讯录
        findViewById(R.id.textView_send_chat)   .setOnClickListener(v -> mgr.sendHtImChat(this));                // 发起聊天
        findViewById(R.id.textView_scan)        .setOnClickListener(v -> mgr.openHtImScanQr(this));              // 扫一扫
        findViewById(R.id.textView_send_email)  .setOnClickListener(v -> mgr.openHtImEmailSend(this));           // 发邮件
        findViewById(R.id.textView_todo)        .setOnClickListener(v -> mgr.openHtImPlatformTodo(this));        // 待办
        findViewById(R.id.textView_trace)       .setOnClickListener(v -> mgr.openHtImTraceTemplateSelect(this)); // 发起申请
        findViewById(R.id.textView_information) .setOnClickListener(v -> mgr.openHtImInformation(this));         // 信息发布
        findViewById(R.id.textView_chat_meeting).setOnClickListener(v -> mgr.openHtImChatMeeting(this));         // 视频会议
        findViewById(R.id.textView_ai)          .setOnClickListener(v -> mgr.openHtImAiAssistant(this));         // AI 助手

        findViewById(R.id.textView4).setOnClickListener(v -> {
            mgr.doHtImLogout(MainActivity.this);
            finish();
        });
    }

    @Override
    protected void onDestroy() {
        HtImClientManager.getInstance().removeAllHtImChatListener();
        super.onDestroy();
    }
}
示例中的防重复点击已省略。SDK 页面启动较重,实际接入建议保留防抖,避免用户连点导致多次 startActivity

15. API 对照速查

两端能力一一对应,仅方法命名与调用形式不同。 表示该平台无对应接口。

生命周期

能力iOSAndroid
单例[HtImClientManager shareInstance]HtImClientManager.getInstance()
SDK 初始化—(登录时完成)initHtImSdk(Application, HtImInitConfig)
预置配置HtClienInitUtil.initClientDataWithConfig:
登录doHtLoginWithLoginConfigM:callBlock:doHtImLogin(config, listener)
登录配置类HtLoginCofigModelHtImLoginConfig + HtImInitConfig
结果码HtReqCode(成功 = 1)ServerInitCallbackListeneronSuccess()
登录后路由注册setHtImRouteListener(Context)
登录后数据加载initHtImData(Activity)
退出登录doHtLogoutdoHtImLogout(Activity)
释放路由监听removeHtImRoutListener()
释放音视频资源cleanHtImSdk()

事件回调

能力iOSAndroid
回调协议HtImClientDelegatedelegate 属性)HtImChatListeneraddHtImChatListener
消息未读数receiveMsgUnreadNum:onRecentMsgUnreadNum(int)
待办数receiveTodoUnreadNum:onRecentToDoNum(int)
登录失效onLogoutStatus(Map)
长连接状态onHIMSocketStatus(boolean)

功能入口

能力iOSAndroid
动态门户 / 工作台goHomeViewopenHtImWorkStation(Context)
消息列表goMsgViewopenHtImChatMessage(Context)
通讯录goAddressViewopenHtImContact(Context)
发起聊天(选人)goChatViewsendHtImChat(Context)
直达指定会话goChatViewWith:
扫一扫goScanCallBlock:(有结果回调)openHtImScanQr(Context)(无回调)
发送邮件goSendMailViewopenHtImEmailSend(Context)
待办列表goTodoViewopenHtImPlatformTodo(Context)
发起申请goApplyViewopenHtImTraceTemplateSelect(Context)
信息发布goSendNoticeopenHtImInformation(Context)
视频会议goVideoMeetingopenHtImChatMeeting(Context)
AI 助手goAIChatopenHtImAiAssistant(Context)

登录配置字段

字段iOS · HtLoginCofigModelAndroid
服务器地址serverUrlserverUrl
账号 / 密码loginId / passwordloginId / password
免密令牌userSdkKeyuserSdkKey
语言languageTypelanguageType
推送标识deviceToken(APNs)vendorDeviceToken + cidDeviceToken
音视频授权htrtcAppId / htrtcAppKey
(在登录配置中)
htrtcAppId / htrtcAppKey
(在 HtImInitConfig 中)
扩展字段extraextra
注意音视频授权的位置差异:iOS 把 htrtcAppId / htrtcAppKey 放在登录配置中, Android 放在 Application 初始化的 HtImInitConfig 中。跨端移植代码时容易漏配。

16. 常见问题

通用

现象原因与处理
拉取不到 SDK 依赖 私有源仍是占位符 <小天互连私有源地址>,或该地址无访问权限。向小天互连获取真实地址与凭据后替换。
登录提示地址错误 / 不可达 serverUrl 填写有误或服务器不可达;HTTP / 自签证书场景还需检查 ATS(iOS)或 SSLSocketClient(Android)配置。
令牌登录失败 userSdkKey 已过期或与 serverUrl 不匹配。令牌应每次启动时从宿主后端动态获取。
视频会议进不去 / 没有画面 htrtcAppId / htrtcAppKey 未配置。注意两端配置位置不同,见第 15 节
收不到未读角标 回调注册时机早于登录成功,或持有者已释放。
收不到离线推送 登录时未上报推送 token;或服务端未配置推送证书;或包名 / Bundle ID 与证书不匹配。

iOS

现象原因与处理
模拟器编译报
找不到 arm64 slice
SDK 无 arm64 模拟器 slice。请用真机调试,并确认 post_install 中已设置 EXCLUDED_ARCHS[sdk=iphonesimulator*] = 'arm64'
Use of private header
from outside its module
AFNetworking / RealReachability 引用了 netinet6/in6.h。加上第 5 节post_install 后重新 pod install
找不到 libarclite 老 pod 的部署目标停在 iOS 8/9。post_install 中统一抬到 13.0。
运行时 duplicate class 警告 宿主工程重复声明了已打进 xcframework 的三方库(百度地图 / 微信 OpenSDK / iflyMSC)。从 Podfile 中移除这些声明。
登录回调 type = 0 / -2 0 为服务器地址错误或不可达;-2 为账号或密码错误。
点击功能入口无反应 未登录成功,或根控制器不是 UINavigationController,SDK 找不到可用的导航栈。
扫一扫 / 发语音闪退 Info.plist 缺少对应的 NS*UsageDescription,见第 6 节
AI 助手打开后崩溃 HTAIKit 被降级到 1.0.6 以下。锁回 1.0.6。

Android

现象原因与处理
Manifest merger failed
缺少 GETUI_APPID / AMAP_APIKEY
manifestPlaceholders 未配置。见第 5 节,两个 key 都是必填。
Manifest merger failed
android:theme / allowBackup 冲突
<application> 上加 tools:replace="android:theme, android:allowBackup"
找不到 htrtc-2.2.3 该 aar 不随依赖链传递。向小天互连索取后放入工程根目录 repositories/,并确保 flatDir { dirs 'repositories' } 已配置。
UnsatisfiedLinkError
加载 .so 失败
ABI 不匹配。SDK 只有 arm64-v8a / armeabi-v7a,abiFilters 中不要包含 x86 / x86_64;模拟器需使用 arm64 镜像或改用真机。
duplicate class
com.intellij.annotations
加上 implementation.exclude group: 'com.intellij', module: 'annotations'
SDK 界面报
DataBinding 相关错误
宿主未开启 DataBinding。在 android { dataBinding { enabled = true } } 中打开。
IllegalStateException:
The htim sdk is not initialized
登录前未调用 initHtImSdk,或没在 Application.onCreate() 中调用。
IllegalStateException:
Please call the doHtImLogin method first
在登录成功前调用了 setHtImRouteListener / initHtImData 或任意 openHtIm* 方法。
登录回调"用户校验失败" userSdkKey 为空,且 loginId / password 未成对填写。
登录回调 code = -2 禁止使用初始密码登录,需先在 Web 端修改密码。
聊天中发送单据 / 文档无响应 未调用 setHtImRouteListener,模块间跳转路由没注册。
退出 Activity 后内存泄漏 onDestroy() 中补 removeAllHtImChatListener(),必要时再调 removeHtImRoutListener()
参考工程:两端各有一套 Demo 工程,演示了本文所有接口的调用方式,可直接对照。