为 Mirobody 添加一个健康数据源,意味着编写一个 C++ vendor::Vendor —— 在 src/health/vendor/ 下的一个 .cpp 文件,外加注册表中的一行。vendor 是编译进程序并按 id 解析的。
前置条件
一个可用的 C++11 引擎构建(./build.sh)。参见贡献指南。
该 vendor 的公开 API 参考:base URL、认证方案、你将支持的数据域所对应的端点,以及任何 webhook 签名方案。只实现可公开确认的部分 —— 其余保留为诚实的桩。
一个 API key 或 OAuth client_id / client_secret,经 VendorConfig 从 MIROBODY_VENDOR_<ID>_* 环境变量读取。
三处改动
创建 vendor 文件
选对类别目录:platform/(B2B 聚合器)、device/(消费品牌)、phone/(手机厂商云 API)或 ehr/。创建 src/health/vendor/<bucket>/<id>.cpp。
将其加入 CMake 源文件列表
vendor 文件是显式列出的(不用通配):把你新的 .cpp 加到 CMakeLists.txt 中 MIROBODY_CORE_SOURCES 列表里、与兄弟 vendor 条目相邻的位置(紧接 src/health/vendor/registry.cpp 之后的那一块)。
vendor 类
一个 vendor 是派生自 VendorBase 的匿名命名空间类,外加一个自由函数 make_<id>() 工厂。它只重写它实现的操作;其余一切继承 VendorBase 的”not implemented”桩。骨架:
// src/health/vendor/device/acme.cpp
#include "health/vendor/vendor.hpp"
#include "client/http_client.hpp"
#include <string>
#include <utility>
namespace mirobody { namespace vendor {
namespace {
class Acme : public VendorBase {
public:
explicit Acme(VendorConfig cfg) : VendorBase(make_info(), std::move(cfg)) {}
// Fetch a normalized DataDomain over [start_iso, end_iso]; return the vendor's JSON.
std::string fetch(const std::string& user_id, DataDomain domain,
const std::string& start_iso, const std::string& end_iso) override {
require_token();
const std::string url = base_url() + endpoint(domain, user_id, start_iso, end_iso);
const std::vector<std::string> headers = {
"Authorization: Bearer " + config().api_key,
"Accept: application/json",
};
client::HttpResponse res = client::HttpClient().get(url, /*timeout_ms=*/30000, headers);
if (res.status < 200 || res.status >= 300) {
throw VendorError(info_.id + ": fetch failed (HTTP " +
std::to_string(res.status) + "): " + res.body.substr(0, 300));
}
return res.body;
}
// Implement authorize_url / handle_webhook / revoke / list_providers ONLY if the
// contract is publicly documented. Otherwise leave them as inherited stubs.
private:
static std::string endpoint(DataDomain d, const std::string& u,
const std::string& s, const std::string& e) {
switch (d) {
case DataDomain::HeartRate: return "/v1/users/" + u + "/heart?from=" + s + "&to=" + e;
// … the domains this source actually brokers …
default:
throw VendorError(std::string("acme: unsupported domain '") + to_string(d) + "'");
}
}
std::string base_url() const {
std::string b = config().base_url.empty() ? std::string("https://api.acme.example") : config().base_url;
while (!b.empty() && b.back() == '/') b.pop_back();
return b;
}
void require_token() const {
if (config().api_key.empty()) {
throw VendorError(info_.id + ": api_key is required — set MIROBODY_VENDOR_ACME_API_KEY");
}
}
static VendorInfo make_info() {
VendorInfo i;
i.id = "acme";
i.display_name = "Acme Wearables";
i.positioning = "Consumer wearable brand with a public OAuth2 REST API";
i.data_source_coverage = "Acme bands and watches";
i.integration_method = "REST (OAuth2)";
i.docs_url = "https://developer.acme.example/";
i.region = Region::Global;
i.open_source = false;
i.domains = {DataDomain::Activity, DataDomain::HeartRate, DataDomain::Sleep};
i.integrations = {Integration::Rest};
return i;
}
};
} // namespace
std::unique_ptr<Vendor> make_acme(const VendorConfig& cfg) {
return std::unique_ptr<Vendor>(new Acme(cfg));
}
}}
动手前先研究最接近的真实客户端:Fitbit(device/fitbit.cpp)是完整 OAuth 2.0 品牌的范本(fetch + authorize_url + revoke + 带签名的 webhook);Withings 用于表单编码的服务;ehr/ehr.cpp 用于 FHIR R4 源;garmin.cpp 演示如何留下诚实的桩。
注册工厂
在 registry.cpp 中,与其他工厂并列声明你的工厂,并向 kVendors 表添加一行(按矩阵顺序):
// forward declaration (top of registry.cpp)
std::unique_ptr<Vendor> make_acme(const VendorConfig&);
// … inside the kVendors[] table …
{"acme", &make_acme},
这就是全部接线:open_vendor("acme", cfg) 现在能构造它,且它会出现在 vendor_ids() / all_vendor_info() 中。
与 MCP 工具(经宏在编译期自动注册)不同,vendor 使用这张显式的工厂表 —— 你手动添加声明和 kVendors 行。
你在实现的契约
| 操作 | 何时实现…… | 否则 |
|---|
info() | 始终 —— 完整填充 VendorInfo | (必填) |
fetch(user_id, domain, start, end) | 数据端点有文档 | 桩 |
authorize_url(redirect_uri, state, user_id, provider) | 同意是一个你能构建的普通重定向 URL | 桩(如有状态的 PKCE 应放到别处) |
handle_webhook(raw_headers, body) | 签名方案公开 | 未签名 / 无文档则为桩 |
revoke(user_id, provider) | 断连端点有文档 | 桩 |
list_providers(user_id) | 该源有 provider 目录 | 单一品牌则为桩 / 不适用 |
行事准则:
- 绝不臆造 base URL 或端点。 若主机随部署而异,就要求
base_url 并在其为空时抛错(如 ehr 客户端那样)。
- 在文件头部记录任何操作为何是桩 —— 三选一:受限/无文档、契约不匹配、无此端点。
- 若 vendor 的参考受限,用”已确认 vs 推断”的注释标注推断出的字段名。
- 从
fetch 返回该 vendor 的 JSON(FHIR 原生源则返回 FHIR JSON)。归一化是下游的关注点。
配置约定
凭据来自 VendorConfig::from_env(id):
MIROBODY_VENDOR_ACME_API_KEY / _CLIENT_ID / _CLIENT_SECRET / _BASE_URL
当存在一个 API key,或一对 client id+secret 时,VendorConfig::configured() 为真。
构建并验证
./build.sh
./build/mirobody # your vendor is now in the registry
然后针对一个运行中的服务器手动验证 —— 参见 Provider 测试。
下一步
OAuth 实现
客户端中的 OAuth 2.0 / PKCE 模式
Provider 测试
端到端验证你的 vendor
Garmin / Fitbit 示例
对照契约的真实客户端