Skip to content

Windows 空白工程接入晞晗IM

这份文档面向 .NET 8 WPF。XHIM Windows 包拆分为:

text
XHIM.SDK       Headless C# Facade + native runtime
XHIM.UI.Wpf    会话、聊天、通话占位控件和 Lucide Geometry

本文只处理 Windows 客户端。开发环境只需领取 Server URL、测试 User ID 和 测试 Conversation ID、App ID;生产环境再由登录层提供一个业务鉴权回调。 客户端不需要数据库密码、服务端密钥、C++ 工具链或手工 Token。 服务端操作仅见 XHIM Server 文档

跑通第一条消息后,所有公开方法、事件、模型和异常请查 SDK API Reference

最短路径是“添加 NuGet → ConnectAsyncSendTextAsync”。

先确认发行包

只使用 Release Manifest 对应的签名 NuGet。包内必须按声明 RID 携带 xhim_core_v1.dll、托管程序集、checksum、LICENSE、NOTICE 和 SBOM;客户只 依赖 NuGet,不读取、编译或编辑 C++ Core。

1. 新建工程

Visual Studio 选择 Create a new project → WPF Application

text
Framework      .NET 8
Architecture   与 XHIM native runtime 一致,例如 x64

不要选择 AnyCPU 后再混入单架构 native DLL。

2. 添加 NuGet

正式内部源:

powershell
dotnet add package XHIM.SDK --version <version>
dotnet add package XHIM.UI.Wpf --version <version>

本地验收可把发行目录作为 NuGet Source:

powershell
dotnet nuget add source C:\xhimsdk\packages -n XHIMLocal

NuGet 必须在 runtimes/win-x64/native/runtimes/win-arm64/native/ 等声明 RID 下携带已签名 DLL。客户不手工 P/Invoke 或复制随机版本 DLL。

3. 创建账号 Client

Development XHIM Server 已启用免密码登录时,只需服务器地址和当前用户:

csharp
using XHIM;

var client = await XHIMClient.ConnectAsync(
    "https://im.customer.example",
    currentUserId);

ConnectAsync 映射到底层最小配置:

Core 配置Windows 来源
AppId显式 appId,否则 Bootstrap 默认值
StoragePathstorageRootPath 下按 App ID + User ID 隔离
Deployment.BootstrapUrlserver
Endpoint Key ID + Public KeyBootstrap 公开配置

ConnectAsync 自动读取 GET /v1/sdk/config、选择默认 App ID、创建账号隔离 数据库、启动 Core,再调用 POST /v1/sdk/development:login 登录。前端不用 手工组装底层配置,也不用处理测试登录细节。

生产环境把现有业务登录态包装成一个鉴权回调:

csharp
var client = await XHIMClient.ConnectAsync(
    "https://im.customer.example",
    currentUserId,
    authentication: XHIMAuthentication.Business(async cancellationToken =>
        await businessApi.GetXHIMCredentialAsync(
            currentUserId,
            cancellationToken)));

回调只负责向购买方自己的后端取得当前用户 XHIM 登录票据;何时调用、续期和 并发合并由 SDK 处理。新项目使用 Business Provider,不把固定票据写入源码或 配置。

生产 Server 必须使用 HTTPS。HTTP 只有在公开配置明确返回 environment=development 时才允许;发现请求禁止跳转,Server URL 不能带 userinfo、query 或 fragment,单个响应上限为 256 KiB。

在 App 的账号 Session/ViewModel 中持有 Client,并在退出账号时 await client.DisposeAsync()。Window 关闭重开不应创建第二个同账号数据库 连接。同一个 DLL 可连接不同客户域名,API/WSS/上传/下载地址只能使用服务端 签名结果。

Production Release 不允许 Local Preview、Development Easy Login、固定测试 账号或匿名降级。Window 和 ViewModel 不保存 XHIM 登录票据。

3.1 状态、会话和首条消息

csharp
var stateTask = Task.Run(async () =>
{
    await foreach (var value in client.Events(cancellationToken))
        if (value is XHIMEvent.StateChanged state)
            Console.WriteLine($"XHIM state: {state.State}");
}, cancellationToken);

var conversationId = "xhim-demo-direct";
var clientMessageId = Guid.NewGuid().ToString("N");
await client.SendTextAsync(
    conversationId,
    "Hello from Windows",
    clientMessageId,
    cancellationToken);

var conversations = await client.ListConversationsAsync(
    limit: 50,
    cancellationToken: cancellationToken);
var page = await client.ListMessagesAsync(
    conversationId,
    limit: 50,
    cancellationToken: cancellationToken);
var through = page.Messages
    .Where(message => message.ServerSequence.HasValue)
    .Select(message => message.ServerSequence!.Value)
    .DefaultIfEmpty()
    .Max();
if (through > 0)
    await client.MarkConversationReadAsync(
        conversationId,
        through,
        cancellationToken);

var persisted = await client.GetMessageAsync(
    clientMessageId,
    cancellationToken);
if (persisted.State is XHIMMessageState.Failed or
    XHIMMessageState.Cancelled)
    await client.RetryMessageAsync(clientMessageId, cancellationToken);

发送成功表示可靠 Outbox 已受理;最终状态以 ServerAccepted 为准。NextCursor 是不透明 byte[],下一页只能原样传回对应查询。

在线状态与“正在输入”

Client 进入 Ready 后直接调用 SDK,不需要业务层处理 Token、HTTP 或 WebSocket:

csharp
await client.PublishPresenceAsync(
    XHIMPresenceStatus.Online,
    cancellationToken: cancellationToken);
await client.PublishTypingAsync(
    conversationId,
    isTyping: true,
    cancellationToken: cancellationToken);

// 输入框清空、发送成功或页面退出时立即清除。
await client.PublishTypingAsync(
    conversationId,
    isTyping: false,
    cancellationToken: cancellationToken);

Events(...) 中分别处理 XHIMEvent.PresenceChangedXHIMEvent.TypingChanged。不要每次按键都调用,只上报开始/停止两种转换; 显示层必须按 ExpiresAtMilliseconds 自动清除过期状态。TTL 留为 0 时由 服务端使用统一默认值。

3.2 服务端历史与账号视图

csharp
var history = await client.GetMessageHistoryAsync(
    conversationId,
    limit: 50,
    cancellationToken: cancellationToken);
if (history.Continuation is not null)
    _ = await client.GetMessageHistoryAsync(
        conversationId,
        history.Continuation,
        limit: 50,
        cancellationToken: cancellationToken);

var serverMessageId = history.Messages
    .Select(message => message.ServerMessageId)
    .FirstOrDefault(value => value is not null);
if (serverMessageId is not null)
    _ = await client.DeleteMessageForSelfAsync(
        conversationId,
        serverMessageId,
        Guid.NewGuid().ToString("N"),
        cancellationToken: cancellationToken);

if (history.LatestServerSequence > 0)
{
    var cleared = await client.ClearConversationAsync(
        conversationId,
        history.LatestServerSequence,
        Guid.NewGuid().ToString("N"),
        expectedRevision: history.View?.Revision ?? 0,
        cancellationToken: cancellationToken);
    _ = await client.HideConversationAsync(
        conversationId,
        Guid.NewGuid().ToString("N"),
        expectedRevision: cleared.View.Revision,
        cancellationToken: cancellationToken);
}

服务端历史从新到旧返回,Continuation 只能原样传回。删除、清空和隐藏只改变 当前账号视图;同一次失败重试必须复用原 mutationId。后续 CAS 使用最新 View.Revision,冲突时先重拉历史。四个 API 都接受 CancellationToken 并 转发到 native 通用取消。

3.3 用户资料、单聊、Push 和取消

csharp
using System.Globalization;
using XHIM;

var me = await client.GetCurrentUserProfileAsync(cancellationToken);
var profiles = await client.GetUserProfilesAsync(
    new[] { me.UserId, "bob" },
    cancellationToken);

// null 表示保持不变;空字符串表示明确清空。
var updated = await client.UpdateCurrentUserProfileAsync(
    new XHIMUserProfileUpdate(
        DisplayName: "Alice Windows",
        AvatarUrl: null,
        Bio: ""),
    cancellationToken);

var direct = await client.GetOrCreateDirectConversationAsync(
    "bob",
    cancellationToken);
await client.SendTextAsync(
    direct.ConversationId,
    "Hello from Windows",
    cancellationToken: cancellationToken);

Windows Push 或厂商 Push 回调取得 token 后,使用保存在系统凭据存储中的安装级 deviceId 注册。输入的 ToString() 会脱敏,回执也不含 token;业务日志仍 不得记录 provider token:

csharp
var registration = await client.RegisterPushDeviceAsync(
    new XHIMPushDevice(
        XHIMPushPlatform.Web,
        pushInstallationId,
        providerToken,
        "production",
        CultureInfo.CurrentUICulture.Name),
    cancellationToken);

await client.DisablePushDeviceAsync(
    pushInstallationId,
    cancellationToken);

每个 API 都接受 CancellationToken。取消只会调用 native cancel_request 并终止本次等待,不会 Dispose 共享 Client:

csharp
using var requestCancellation = new CancellationTokenSource();
var lookup = client.GetUserProfilesAsync(
    new[] { "alice", "bob" },
    requestCancellation.Token);
requestCancellation.Cancel();
try
{
    await lookup;
}
catch (OperationCanceledException)
{
    // 本次查询已取消,账号级 client 仍然可用。
}

4. 加入 WPF 控件

XAML:

xml
<Window
    xmlns:xhim="clr-namespace:XHIM.UI.Wpf;assembly=XHIM.UI.Wpf">
  <xhim:ChatView x:Name="Chat"
                 Messages="{Binding Messages}" />
</Window>

订阅动作:

csharp
Chat.SendRequested += async (_, text) =>
{
    await client.SendTextAsync(conversationId, text);
    await viewModel.ReloadAsync();
};

Chat.AttachmentPicked += async (_, attachment) =>
{
    await viewModel.SendAttachmentAsync(
        attachment.Kind,
        attachment.LocalFilePath);
};

Chat.AttachmentActionRequested += (_, action) =>
{
    if (action.Kind == XHIMAttachmentKind.Camera)
        viewModel.OpenWindowsCamera();
};

ConversationListChatView 使用 WPF ResourceDictionary 中的 XHIM 颜色与 Lucide Geometry。你可以在宿主 ResourceDictionary 覆盖主题 Brush; 业务 Cell 只读取托管模型,不读取 SQLite、不持有 native handle。

5. 发图片、视频和文件

ChatView 自带照片、视频和文件 OpenFileDialog;“拍照”动作通过 AttachmentActionRequested 交给宿主调用 Windows CameraCaptureUI,避免 SDK 擅自声明客户的相机权限和保存目录。

选择文件后直接提交持久化上传意图,不需要在 UI 层创建 uploader:

csharp
var accepted = await client.SendImageAsync(
    conversationId,
    attachment.LocalFilePath,
    "image/jpeg",
    imageWidth,
    imageHeight,
    cancellationToken: cancellationToken);

var fileAccepted = await client.SendFileAsync(
    conversationId,
    attachment.LocalFilePath,
    detectedMimeType,
    displayName: Path.GetFileName(attachment.LocalFilePath),
    cancellationToken: cancellationToken);

accepted/fileAccepted 只表示任务和依赖消息意图已经持久化受理,不表示 上传或消息发送已经完成。SDK 在后台线程读取文件元数据并计算 SHA-256; 任务快照、事件、诊断和网络 Payload 都不包含本地路径。视频和语音分别调用 SendVideoAsyncSendAudioAsync

订阅事件后按 TaskId 查询完整任务:

csharp
await foreach (var value in client.Events(cancellationToken))
{
    if (value is not XHIMEvent.MediaTaskUpdated update)
        continue;

    // 事件只携带 task id、原始 state 和 schema;详情始终重新查询。
    var task = await client.GetMediaTaskAsync(
        update.TaskId,
        cancellationToken);
    if (task.State == XHIMMediaTaskState.Completed)
        ShowSent(task.ClientMessageId);
}

WPF ViewModel 使用独立投影通道重查,不与生命周期事件竞争,也不读取 SQLite:

csharp
await using var projectionRefresh = new XHIMProjectionRequeryController(
    client,
    async (change, cancellation) =>
    {
        await viewModel.ReloadAsync(cancellation);
    });

控制器捕获当前 SynchronizationContext(通常是 WPF Dispatcher),订阅建立后 先查询一次,再串行处理 MessageUpserted/MessageStateChanged/ ConversationChanged/SocialChanged/SyncApplied。未知 kind/schema 保留为 ProjectionInvalidated 并宽范围重查;P/Invoke 回调返回前已复制所有字符串。

取消一次 GetMediaTaskAsyncCancellationToken 只会调用通用 cancel_request 并终止本次等待;要持久化取消上传任务,必须显式调用:

csharp
var cancelled = await client.CancelMediaTaskAsync(taskId, cancellationToken);

业务需要自定义元数据时使用 CreateMediaUploadAsync(XHIMMediaUploadIntent)TaskIdClientMessageId 可作为幂等键传入;未知 state/direction 映射为 Unknown,原值保留在 NativeState/NativeDirection。旧 XHIMServerMediaUploaderIXHIMMediaUploader 和带 uploader 参数的 Send*Async 已标记为过时的非持久化兼容路径,不再是默认接入方式。

收到消息中的稳定 XHIMMediaRef 后,媒体下载也直接创建持久化任务:

csharp
var download = await client.CreateMediaDownloadAsync(
    new XHIMMediaDownloadIntent(
        cacheKey: Guid.NewGuid().ToString("N"),
        media: mediaRef),
    cancellationToken);

cacheKey 是 Core 私有缓存的扁平标识,不是文件路径。下载到达 Completed 前不可读取;不要拼接 StoragePath 猜测内部位置,也不要保存签名 URL。运行中可以调用 client.GetDiagnostics() 取得同步、只读、纯内存健康 快照;其中不含账号、消息正文、用户凭证、本地路径或临时 URL。

任务到达 Completed 后读取已校验字节:

csharp
await using var reader = await client.OpenMediaCacheReaderAsync(
    cacheKey,
    mediaRef,
    cancellationToken);

await foreach (var chunk in reader.ReadChunksAsync(
    cancellationToken: cancellationToken))
{
    decoder.Append(chunk);
}

open 会在 worker 上执行完整 SHA-256 校验;同一 Reader 的 read/dispose 串行, 停止异步枚举就是取消。当前 Windows Reference Adapter 会明确返回 XHIMException.Code == 103/media_cache_unsupported;客户 Product Adapter 实现安全 no-follow reader 后无需修改调用代码,SDK 永远不会退化为拼路径。

系统从离线恢复时可调用:

csharp
NetworkChange.NetworkAvailabilityChanged += (_, available) =>
{
    if (available) client.NotifyNetworkAvailable();
};

这是同步 best-effort 提示,无 callback/request ID;Core 的退避定时器仍保底。

5.1 位置、名片、回复、合并转发与本地搜索

csharp
using XHIM;

var location = XHIMStandardMessageFactory.Location(
    31.2304,
    121.4737,
    "人民广场",
    "上海市黄浦区");
await client.SendMessageAsync(conversationId, location);

var card = XHIMStandardMessageFactory.ContactCard("bob", "Bob");
await client.SendMessageAsync(conversationId, card);

var result = await client.SearchMessagesAsync(
    new XHIMMessageSearchQuery(
        "合同",
        conversationId,
        null,
        ["text/plain", "xhim.media.file"]));
foreach (var message in result.Messages)
    Console.WriteLine(message.FallbackText);

搜索只访问当前账号的本地安全文本投影。NextCursor 是不透明二进制值,继续 翻页时必须原样传回。

5.2 自定义消息

csharp
using System.Text;

var payload = Encoding.UTF8.GetBytes(
    """{"orderId":"order-1001","title":"待付款订单"}""");
var custom = new XHIMOutgoingMessage(
    "com.customer.message.order-card",
    1,
    payload,
    "[订单] 待付款订单");
await client.SendMessageAsync(
    conversationId,
    custom,
    cancellationToken: cancellationToken);

接收端按 ContentType + ContentVersion 解码,未知版本显示 FallbackText。 需要集中校验、会话预览和 WPF Renderer 时注册 XHIMMessagePluginRegistry;插件失败必须回退,不能阻塞时间线。

6. 收消息和退出

csharp
await foreach (var value in client.Events(cancellationToken))
{
    if (value is XHIMEvent.StateChanged state &&
        state.State == XHIMClientState.CredentialRequired)
        ShowReconnectingCredentialState();
    if (value is XHIMEvent.MessageMutated mutated)
        Console.WriteLine($"Message changed: {mutated.ClientMessageId}");
    await viewModel.ReloadAsync();
}

使用 XHIMAuthentication.Business 或 Development 登录时,登录状态维护由 SDK 串行去重处理;事件仍公开给 UI 展示状态。Business Provider 失败时由账号会话 层恢复业务登录态,页面不直接调用登录票据接口。

编辑与撤回只接受已有 ServerMessageId 的服务端消息。CAS 版本来自 MutationRevision,同一次网络重试必须复用同一个 mutationId

csharp
if (message.ServerMessageId is not { } serverMessageId) return;

var edited = await client.EditTextAsync(
    message.ConversationId,
    serverMessageId,
    Guid.NewGuid().ToString("N"),
    message.MutationRevision,
    "修改后的内容",
    cancellationToken);

var recalled = await client.RecallAsync(
    edited.ConversationId,
    serverMessageId,
    Guid.NewGuid().ToString("N"),
    edited.MutationRevision,
    cancellationToken);

返回值是服务端确认后的完整 XHIMMessage。未知 mutation 枚举映射到 Unknown,原始值仍在 NativeMutationKind;传入的 CancellationToken 会取消对应 native request。

置顶和免打扰使用服务端 CAS,同步版本来自 conversation.PreferenceRevision;草稿只保存到当前账号的加密本地库:

csharp
await client.SetConversationPreferenceAsync(
    conversation.ConversationId,
    isPinned: true,
    notificationsMuted: false,
    Guid.NewGuid().ToString("N"),
    conversation.PreferenceRevision,
    cancellationToken);

var text = "尚未发送的内容";
await client.SetLocalDraftAsync(
    conversation.ConversationId,
    new XHIMOutgoingMessage(
        "text/plain",
        1,
        Encoding.UTF8.GetBytes(text),
        text),
    cancellationToken);
var draft = await client.GetLocalDraftAsync(
    conversation.ConversationId,
    cancellationToken);
await client.ClearLocalDraftAsync(
    conversation.ConversationId,
    cancellationToken);

draft.IsPresent == false 表示没有草稿;本地草稿不会跨设备同步。

退出账号:

csharp
await client.LogoutAsync();
await client.DisposeAsync();

7. 常见问题

  • BadImageFormatException:App RID、进程架构和 NuGet native DLL 不一致;
  • DevelopmentLoginDisabled:当前地址不允许 Easy Login,改用 Business Provider;
  • 状态未到 Ready:检查 Bootstrap HTTPS、系统时间、防火墙和业务会话;
  • 能发送但页面不刷新:消费事件后重新调用 ListMessagesAsync,不要缓存旧 Page;
  • WPF 图标缺失:确认 XHIM.UI.Wpf ResourceDictionary 已进入输出目录。

连接错误按 XHIMConnectionException.Code 分支;运行时按 XHIMException.Code/Domain/StableCode/Retryable 分支。不要解析错误 Message 决定重试,也不要记录凭证、完整 Payload 或附件路径。

8. 双端与 Production 发布验收

  • Alice/Bob 使用各自业务账号 Provider 加入同一会话互发;
  • 验证 Server 序号、本地 ServerAccepted、断网恢复和自动保持登录;
  • 覆盖 win-x64/win-arm64 等声明 RID;
  • 使用业务鉴权回调,禁用 Local Preview 和 Easy Login;
  • 验证会话/历史分页、文本/媒体/自定义消息、已读、编辑撤回和社交治理;
  • 验证 CancellationToken、失败幂等重试、前后台、推送和账号切换;
  • 校验 Authenticode、NuGet 签名、DLL hash、符号、LICENSE、NOTICE 和 SBOM;
  • 使用 Windows 防火墙允许 App 访问正式 HTTPS/WSS,不降低系统 TLS。

P/Invoke、SafeHandle、CancellationToken 和发布细节见 Windows 完整指南

附录 A:好友、群组和黑名单写入

csharp
var request = await client.SendFriendRequestAsync(
    "bob", "", Guid.NewGuid().ToString(), cancellationToken);
_ = await client.ResolveFriendRequestAsync(
    request.RequestId, XHIMFriendRequestDecision.Accept,
    Guid.NewGuid().ToString(), cancellationToken);
_ = await client.DeleteFriendshipAsync(
    "bob", Guid.NewGuid().ToString(), cancellationToken);
var group = await client.CreateGroupAsync(
    "项目群", ["alice", "bob"], Guid.NewGuid().ToString(),
    cancellationToken);
var roster = await client.ChangeGroupMembersAsync(
    group.Group.ConversationId, ["carol"], [],
    group.Group.Revision, Guid.NewGuid().ToString(), cancellationToken);
_ = await client.SetBlockAsync(
    "spam-user", true, Guid.NewGuid().ToString(), cancellationToken);
var join = await client.RequestGroupJoinAsync(
    roster.Group.ConversationId, "", Guid.NewGuid().ToString(),
    cancellationToken);
_ = await client.ResolveGroupJoinAsync(
    join.Request.RequestId, XHIMGroupJoinDecision.Accept,
    Guid.NewGuid().ToString(), cancellationToken);
_ = await client.ChangeGroupGovernanceAsync(
    roster.Group.ConversationId,
    roster.Group.Revision,
    Guid.NewGuid().ToString(),
    new XHIMGroupGovernanceChange.SetJoinApprovalRequired(true),
    cancellationToken);
var left = await client.LeaveGroupAsync(
    memberGroup.ConversationId,
    memberGroup.Revision,
    Guid.NewGuid().ToString(),
    cancellationToken);
var dismissed = await client.DismissGroupAsync(
    ownedGroup.ConversationId,
    ownedGroup.Revision,
    Guid.NewGuid().ToString(),
    cancellationToken);

mutationId 是逻辑写幂等键:同一请求重试复用,不同参数必须生成新值,否则为 IDEMPOTENCY_CONFLICTexpectedRevision 使用最新 Group.Revision,CAS 冲突后先重查。CancellationToken 会取消 native 请求等待,但不回滚已经提交 的事务。业务只根据 XHIMException.Code/Domain/StableCode 分支,不解析 Message,也不要记录用户凭证、申请附言或资料内容。 退出和解散结果的 GroupChange 是权威服务端投影,IdempotentReplay 表示 返回的是同一逻辑写入的既有结果。示例中的 memberGroupownedGroup 分别来自最新成员群、本人拥有群查询。

附录 B:离线只读数据库

使用 XHIMOfflineReader 读取已有账号数据库。它实现 IAsyncDisposable, SQLite/P/Invoke 与释放都在线程池运行:

csharp
await using var reader = await XHIMOfflineReader.OpenAsync(
    new XHIMOfflineReaderConfiguration(
        accountDatabasePath,
        currentUserId),
    async cancellationToken =>
    {
        // 从 DPAPI/Credential Manager 解封数据库随机 Key。
        return await secureDatabaseKeys.UnwrapAsync(
            currentUserId,
            cancellationToken);
    });

var first = await reader.MessagesAsync(conversationId);
if (first.NextCursor is { } cursor)
    _ = await reader.MessagesAsync(conversationId, cursor);

_ = await reader.SearchMessagesAsync(
    new XHIMMessageSearchQuery("合同"));
_ = await reader.ConversationsAsync();
_ = await reader.FriendRequestsAsync();
_ = await reader.FriendshipsAsync();
_ = await reader.GroupsAsync();
_ = await reader.GroupMembersAsync(groupId);
_ = await reader.BlocksAsync();
_ = await reader.GroupJoinRequestsAsync();

不要把数据库 Key 放进 appsettings.json、Registry、源码或命令行。SDK 清理 自己的临时 Key 副本;SemaphoreSlim 串行每个 SafeHandle,且在 P/Invoke 返回前把 borrowed view 深拷贝成托管值。NextCursor 必须原样传回 SDK。 DisposeAsync() 可重复调用,关闭后所有查询都会以保留原始 status 的 XHIMException 失败。

附录 C:登录设备管理

csharp
var page = await client.ListDeviceSessionsAsync(cancellationToken);
var target = page.Sessions.FirstOrDefault(
    session => !session.Current && session.Active);
if (target is not null)
{
    var result = await client.RevokeDeviceSessionAsync(
        target.SessionId,
        Guid.NewGuid().ToString(),
        cancellationToken);
    // Changed=false 表示此前已过期/失活,仍是成功幂等结果。
}

// 同步只读、调用方持有;不依赖先调用列表,也不执行网络或磁盘 I/O。
var policy = client.DeviceSessionPolicySnapshot;

同一次精确重试复用 mutationId,新操作生成新值。CancellationToken 走通用 native request cancel。自撤销返回 Current=trueActive=false,应用应回到 登录态。登录完成前或登出后读取策略会抛出 XHIMException(Code=2),即原生 INVALID_STATE。模型 ToString() 已脱敏,日志也不得输出会话 ID、设备 ID 或撤销原因。

附录 D:协议兼容快照(仅诊断/灰度)

csharp
var compatibility = client.CompatibilitySnapshot;
logger.LogInformation(
    "XHIM protocol {ClientProtocol}/{ServerProtocol}",
    compatibility.ClientProtocolVersion,
    compatibility.ServerProtocolVersion);

普通业务无需处理该快照:登录已经对不兼容服务端 fail-closed。该同步属性只 读取登录时严格验证并深拷贝的内存状态,不进行网络或磁盘 I/O;仅供诊断、支持 包和灰度发布观测。登录前或登出后会保留 XHIMException(Code=2)(原生 INVALID_STATE)。不要用 capability 绕过 登录结论,也不要记录 SDK/Server 版本或 capability 值;ToString() 只保留 协议号和数量。

XHIM 客户端 SDK 与服务端文档