主题
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
├── 阿里云/腾讯云/其他合规服务
└── 客户自带本地模型1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
该模块不调用 XHIM Server,不读取服务端消息正文,不要求服务端 保存转写或翻译明文。当前也没有把该模块绑入 ClientEngine 或稳定 C ABI;购买方可以先以独立 XHIM::Enrichment 组件集成,平台 Facade 在 后续 ABI 版本中以只增方式公开。
2. E2EE 强制边界
Message Enricher 不是解密后门。对 E2EE 会话必须按以下顺序:
- XHIM E2EE Provider 在客户端解密消息或媒体;
- UI 向当前用户明确展示转写/翻译动作和远程处理提示;
- 用户显式发起后,App 将
input_boundary设为kClientDecryptedE2EEWithExplicitConsent; - 只有远程 Provider 还必须设置
allow_remote_processing = true;未授权时 Core 直接 fail closed; - 不得把 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)1
2
2
公共头文件:
cpp
#include <xhim/enrichment/message_enricher.h>1
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(); }
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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);
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
6. 发起语音转文字
TranscriptionRequest 只允许两种输入之一:
audio_bytes:App 已获得授权并解密的有界媒体字节;secure_local_handle:平台适配器和 Provider 共同约定的短期不透明句柄。
不允许同时提供,也不允许都为空。media_type 必须是有界的媒体 类型,例如 audio/wav 或 audio/mp4。对长语音应优先使用受控句柄, 避免 App 同时持有多份大字节数组。
7. 上限、回调和生命周期
默认上限:
| 项目 | 默认 |
|---|---|
| 并发请求 | 64 |
| 音频字节 | 64 MiB |
| 翻译原文 | 1 MiB UTF-8 |
| 结果文本 | 2 MiB UTF-8 |
| 结果分段 | 10,000 |
| metadata | 64 项 / 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 只用于单元测试,不代表真实语音或翻译质量。