跳转到主要内容

什么是 vendor

在 Mirobody 中,vendor 是针对某个健康数据源的 C++ 客户端,在 src/health/vendor/ 下一源一文件。每个 vendor 都实现同一个抽象接口 —— src/health/vendor/vendor.hpp 中的 vendor::Vendor —— 因此引擎的其余部分能通过同一套契约与可穿戴品牌、B2B 聚合器、医院 EHR 对话,并在运行时按 id 挑选具体客户端。
每个数据源都是一个 vendor::Vendor,编译进程序并通过注册表按 id 解析。

四大类别

各源的客户端按四个目录归类,位于 src/health/vendor/ 下:

聚合平台

platform/ —— 15 家 B2B 健康数据聚合器(Terra、Validic、Human API、Junction、Metriport、Rook、Spike、Thryve、WeFitter、Vitalera、Open Wearables、Redox、Particle Health、HealthConnect、LexisNexis)

设备品牌

device/ —— 消费级可穿戴品牌的直连客户端:Fitbit、Garmin、Withings

临床 EHR

ehr/ —— 一个通用 SMART-on-FHIR 客户端,覆盖所有 ONC 认证的 EHR(Epic、Oracle Health / Cerner、athenahealth 等),以各租户的 FHIR base URL 参数化

手机健康存储

phone/ —— 提供云端 API 的手机厂商健康存储。目前:华为 Health Kit
仅设备本地的存储没有 vendor 客户端。 Apple Health、Samsung Health、Google Health Connect 以及小米 / Mi Fitness 不暴露任何服务端可调用的 API,因此它们不是 vendor::Vendor。取而代之,宿主 App 在设备本地读取样本,并以 FHIR Observation 的形式 POST 到内嵌的 FHIR 端点。参见数据流
拥有各自 OAuth REST API 的设备品牌通常通过聚合器接入(Terra、Validic、Thryve、Rook、Spike)。只有三家拥有专属直连客户端(Fitbit、Garmin、Withings);其余 —— Oura、Polar、Whoop 等 —— 仍只能经聚合器接入,除非有人专门添加一个客户端以刻意绕过聚合器。

vendor 契约

每个 vendor 都派生自 VendorBase,并只重写那些其线路契约能从该源的公开文档中确认的操作。接口(摘自 vendor.hpp):
namespace mirobody { namespace vendor {

class Vendor {
public:
  virtual ~Vendor() = default;

  // Static metadata (no network, no credentials).
  virtual const VendorInfo& info() const = 0;

  // Begin consumer-mediated consent; returns the entry point handed to the user
  // (usually an OAuth authorize URL).
  virtual std::string authorize_url(const std::string& redirect_uri,
                                    const std::string& state,
                                    const std::string& user_id,
                                    const std::string& provider) = 0;

  // The data sources a connected user can link, as the vendor's JSON.
  virtual std::string list_providers(const std::string& user_id) = 0;

  // Fetch `domain` for `user_id` over [start_iso, end_iso], as the vendor's JSON.
  virtual std::string fetch(const std::string& user_id,
                            DataDomain domain,
                            const std::string& start_iso,
                            const std::string& end_iso) = 0;

  // Validate and parse an inbound webhook delivery; returns the event as JSON.
  virtual std::string handle_webhook(const std::string& raw_headers,
                                     const std::string& body) = 0;

  // Revoke a user's authorization / disconnect them.
  virtual void revoke(const std::string& user_id,
                      const std::string& provider) = 0;
};

}}
  • DataDomain 是归一化的数据类别:ActivitySleepHeartRateGlucoseNutritionBodyMetricsLabsClinical
  • 时间边界为 ISO-8601 字符串;为空表示”该 vendor 的默认时间窗口”。
  • 返回数据的操作会原样交回该 vendor 自己的 JSON 字符串。(在此 JSON 之上的归一化模型是后续步骤;若干基于 FHIR 的 vendor 已直接返回 FHIR 资源。)

诚实的桩实现

VendorBase 将每个操作实现为一个抛出 VendorError("... not implemented") 的桩。具体 vendor 只重写它能忠实实现的操作;其余刻意保留为桩,原因记录在以下三类之一:
  • 受限 / 无文档 —— 契约(例如 webhook 签名方案)未公开,实现它意味着捏造一个无法验证的方案;
  • 契约不匹配 —— 有文档的端点需要一个接口未携带的参数;
  • 无此端点 —— 该平台根本没有暴露此类形态的操作。
每个 <id>.cpp 的头部都记录了为何某处保留为桩。捏造契约被视为比诚实的”未实现”更糟。

无需凭据的元数据

每个 vendor 都携带一份 VendorInfo,描述其市场定位、地区、合规态势、数据域和集成方式 —— 无需任何凭据或网络调用即可查询。这让注册表能描述整个领域,用于选型与比较。
struct VendorInfo {
  std::string id;              // stable lowercase key, e.g. "terra"
  std::string display_name;    // "Terra API"
  std::string positioning;
  std::string data_source_coverage;
  std::string docs_url;
  Region region;               // Global | US | EU | SelfHosted
  bool open_source;
  std::vector<std::string> compliance;   // "HIPAA", "GDPR", "SOC2_TYPE_II", …
  std::vector<DataDomain>   domains;
  std::vector<Integration>  integrations; // Rest | Sdk | Webhook | WebSocket | Fhir | Hl7 | WhiteLabel
  // …
};

通过注册表按 id 解析

具体的 vendor 类永远不会在其自身的翻译单元之外被命名。调用方只能通过注册表访问它们(src/health/vendor/registry.hpp):
namespace mirobody { namespace vendor {

// Every registered vendor id, in matrix order.
std::vector<std::string> vendor_ids();

// Static metadata for all vendors — no credentials, no network.
std::vector<VendorInfo> all_vendor_info();

// Construct the vendor for `id`; throws VendorError if unknown.
std::unique_ptr<Vendor> open_vendor(const std::string& id, const VendorConfig& config);

}}

配置

vendor 通过一份按约定从环境变量构建的 VendorConfig 进行配置:
MIROBODY_VENDOR_<ID>_API_KEY        # bearer / API key (most REST vendors)
MIROBODY_VENDOR_<ID>_CLIENT_ID      # OAuth client id
MIROBODY_VENDOR_<ID>_CLIENT_SECRET  # OAuth client secret
MIROBODY_VENDOR_<ID>_BASE_URL       # API host override; empty => vendor default
<ID> 是 vendor id 的大写形式(如 HUMAN_APIFITBITEHR)。对于主机随部署而异的 vendor(自托管或需合同授权,如通用 ehr 客户端),base_url必填的 —— 客户端会拒绝执行,而不是猜测一个主机。

从 HTTP 到 vendor

健康模块(src/health/)是位于这些客户端之上的 HTTP 前端。访问是所有权门控的:用户必须先证明其拥有该外部账号,才能读取任何数据。
方法路由用途
POST/vendors/{id}/bind将用户的外部账号 id 记录为 PENDING 关联
POST/vendors/{id}/bind/verify通过该 vendor 的同意流程证明所有权 → VERIFIED
GET/vendors/{id}/data?domain=&start=&end=为已验证的关联在时间区间内获取数据(Vendor::fetch
SMART-on-FHIR EHR 连接流程由浏览器驱动,拥有自己位于 /health/ehr/* 下的路由(providersauthorizecallbacksync)。每条路由都需要一个 bearer JWT,并限定在已认证用户范围内。
/api/providers 是一个不同的端点 —— 它为模型选择器列出聊天用的 LLM providers(OpenAI、Gemini、MiroThinker 等),而非健康 vendor。健康数据源即本文所述的 vendor::Vendor 客户端。

下一步

Provider 概览

真实的 vendor 类别及各自支持的内容

使用 Provider

配置凭据并关联账号

数据流

vendor 数据如何到达 FHIR 存储与 Agent

Provider 集成

实现一个新的 vendor::Vendor