Skip to content

XHIM Message Enricher 接入与安全边界

状态:C++ Core Provider SPI 已可用;不内置云厂商凭证或伪造的本地 语音/翻译模型。客户根据合规、私有化和成本要求注入自己的 Provider。

Message Enricher 是一个与消息传输、服务端同步和 UI 解耦的客户端 可选模块,当前提供:

  • 语音转文字(Transcription);
  • 文本翻译(Translation);
  • 请求 ID 幂等、有界输入、进度、超时、取消、错误分类和迟到回调 fence;
  • 语言、置信度、时间分段、说话人标签、供应商请求 ID 和有界 metadata;
  • 本地模型与远程服务共用的稳定 C++17 Provider SPI。

1. 模块边界

text
App UI(用户明确点击“转文字/翻译”)
  │ client plaintext / client-decrypted media

XHIM::Enrichment / MessageEnricher
  ├── 验证、超时、取消、epoch fence
  └── Customer MessageEnrichmentProvider
        ├── 阿里云/腾讯云/其他合规服务
        └── 客户自带本地模型

该模块不调用 XHIM Server,不读取服务端消息正文,不要求服务端 保存转写或翻译明文。当前也没有把该模块绑入 ClientEngine 或稳定 C ABI;购买方可以先以独立 XHIM::Enrichment 组件集成,平台 Facade 在 后续 ABI 版本中以只增方式公开。

2. E2EE 强制边界

Message Enricher 不是解密后门。对 E2EE 会话必须按以下顺序:

  1. XHIM E2EE Provider 在客户端解密消息或媒体;
  2. UI 向当前用户明确展示转写/翻译动作和远程处理提示;
  3. 用户显式发起后,App 将 input_boundary 设为 kClientDecryptedE2EEWithExplicitConsent
  4. 只有远程 Provider 还必须设置 allow_remote_processing = true;未授权时 Core 直接 fail closed;
  5. 不得把 MLS 密文、附件密文或服务端密钥交给 Enricher 并期待它 自动解密。

MessageEnricher 把请求 move 给 Provider,自身不持久化输入。 allow_provider_persistence 默认为 false;Provider 必须在完成、取消、 超时或关闭时清理临时明文。如业务确实要保存结果,应在 App 层执行 数据分类、用户告知、保留期和加密策略,不要让 Provider 默认留存。

Core 本身不输出内容日志。Provider 和宿主日志也不得记录:

  • 原始语音/文本、转写/翻译结果;
  • 安全本地句柄、临时路径或 Bearer Token;
  • E2EE 密钥、MLS epoch secret 或附件内容密钥。

3. CMake 接入

cmake
find_package(XHIM 0.1 CONFIG REQUIRED)
target_link_libraries(your_app PRIVATE XHIM::Enrichment)

公共头文件:

cpp
#include <xhim/enrichment/message_enricher.h>

Provider 必须由一个账号级组合根持有,不要在每个聊天 Cell 中创建。

4. Provider 适配器

cpp
class CustomerEnrichmentProvider final
    : public xhim::enrichment::MessageEnrichmentProvider {
public:
  xhim::enrichment::ProviderDescriptor descriptor() const override {
    return {"customer-provider",
            xhim::enrichment::ProviderExecutionMode::kRemoteService,
            true,
            true};
  }

  xhim::enrichment::ProviderStartResult start_transcription(
      xhim::enrichment::TranscriptionRequest request,
      std::shared_ptr<
          xhim::enrichment::MessageEnrichmentProviderSink> sink) override {
    // 把 request 映射到客户已审核的厂商 SDK。不要记录内容。
    // 异步调用 sink->on_progress(...) / on_completed(...)。
    return startCustomerTranscription(std::move(request), std::move(sink));
  }

  xhim::enrichment::ProviderStartResult start_translation(
      xhim::enrichment::TranslationRequest request,
      std::shared_ptr<
          xhim::enrichment::MessageEnrichmentProviderSink> sink) override {
    return startCustomerTranslation(std::move(request), std::move(sink));
  }

  void cancel(const std::string& requestID) noexcept override {
    cancelCustomerRequest(requestID); // 必须幂等
  }

  void shutdown() noexcept override { shutdownCustomerClient(); }
};

XHIM 不内置“默认可用”的网络 Provider,也不会把测试 Fake Provider 打包 进商业制品。Provider 适配器必须完成:

  • 凭证由宿主安全注入,不写入 SDK 源码;
  • 将可重试网络、永久业务、安全拒绝、不支持、内部错误分别映射到 EnrichmentFailureKind
  • retry_after_ms 只用于服务端明确给出的可重试节流;
  • cancel() 可重复调用,不抛异常,尽快释放上传体和临时文件;
  • 一个请求最多一次 terminal completion,迟到回调仍允许进入 sink, Enricher 会使用 request_epoch 安全忽略;
  • start_*() 必须尽快返回;允许同步调用 sink,Core 会捕获 start 异常并隔离重复/迟到 completion,但耗时识别不能阻塞发起线程;
  • 不把原文复制到 error code、diagnostic 或 metadata。

5. 发起翻译

cpp
auto provider = std::make_shared<CustomerEnrichmentProvider>(/* credentials */);
auto enricher = xhim::enrichment::MessageEnricher::create(provider);
if (!enricher) {
  // Provider descriptor 或安全上限配置无效。
}

xhim::enrichment::TranslationRequest request;
request.request_id = createUUID();
request.plaintext = clientDecryptedText;
request.source_language = "auto";
request.target_language = "zh-Hans";
request.timeout_ms = 30'000;
request.privacy.input_boundary =
    xhim::enrichment::EnrichmentInputBoundary::
        kClientDecryptedE2EEWithExplicitConsent;
request.privacy.allow_remote_processing = true;

auto admitted = enricher->translate(
    std::move(request),
    [](const xhim::enrichment::EnrichmentCompletion& completed) {
      if (!completed.succeeded()) {
        handleEnrichmentFailure(completed.failure);
        return;
      }
      showTranslatedText(completed.output.text,
                         completed.output.source_language,
                         completed.output.confidence);
    });

6. 发起语音转文字

TranscriptionRequest 只允许两种输入之一:

  • audio_bytes:App 已获得授权并解密的有界媒体字节;
  • secure_local_handle:平台适配器和 Provider 共同约定的短期不透明句柄。

不允许同时提供,也不允许都为空。media_type 必须是有界的媒体 类型,例如 audio/wavaudio/mp4。对长语音应优先使用受控句柄, 避免 App 同时持有多份大字节数组。

7. 上限、回调和生命周期

默认上限:

项目默认
并发请求64
音频字节64 MiB
翻译原文1 MiB UTF-8
结果文本2 MiB UTF-8
结果分段10,000
metadata64 项 / 64 KiB
默认超时60 秒(可配 100 ms–10 分钟)

配置不能绕过 Core 硬上限。请求 ID 在活动期和有界已完成窗口内 不可重用;商业 App 应使用 UUID/ULID。

set_observer() 每次都推进 observer epoch。在旧 observer 下提交的请求 不会把进度或完成事件泄漏到新 observer;每个请求的 completion 仍保证 最多一次。cancel() 是幂等的,超时会主动取消 Provider, shutdown() 会完成所有未结束请求并忽略之后的供应商回调。

Observer 和每请求 completion 由 Core 串行调用,异常不会逃回 Provider 线程。回调内允许调用 cancel()shutdown():本地 terminal 状态先 完成,若 Provider 正处于同步回调,供应商取消/关闭会在回调展开后安全 转发,避免供应商等待自身回调导致死锁。不要在回调栈内析构 MessageEnricher,也不要直接提交下一条 enrichment;请调度回 App 的 拥有线程后再析构或发起新请求。

Deadline 从请求被 Core 接纳时开始,并采用闭区间:Provider completion 与超时同时到达时超时优先。成功 completion 的 failure code、diagnostic 和 retry-after 必须为空;检测语言不能回传 auto。输入、进度、结果的 metadata 都要求 key 唯一、严格 UTF-8 且受总字节数约束,违规统一归类为 kProviderProtocol,不会把不可信字段继续交给 App。

8. 发布验收

接入真实 Provider 后至少要执行:

  • 成功、静音/空结果、多语言检测、分段置信度;
  • 手动取消、超时、网络断开、节流 Retry-After、凭证过期;
  • Provider 重复 completion、取消后迟到 completion、关闭后迟到进度;
  • 超大输入、非法 UTF-8、非法语言标签、超大/重复 metadata;
  • E2EE 会话未同意、只同意本地处理、明确同意远程处理三种用户路径;
  • 检查日志、崩溃报告、APM 和 Provider 控制台均不包含原文/结果。

仓库的 xhim_message_enricher_tests 使用 Fake Provider 验证 Core 状态机; Fake 只用于单元测试,不代表真实语音或翻译质量。

XHIM 客户端 SDK 与服务端文档