跳转到主要内容

概览

为 Mirobody 添加一个健康数据源,意味着编写一个 C++ vendor::Vendor —— 在 src/health/vendor/ 下的一个 .cpp 文件,外加注册表中的一行。vendor 是编译进程序并按 id 解析的。
使用现有 vendor?参见使用 Provider。关于其背后的模型 —— 契约、诚实的桩、注册表 —— 参见 Vendor 系统

前置条件

一个可用的 C++11 引擎构建(./build.sh)。参见贡献指南
该 vendor 的公开 API 参考:base URL、认证方案、你将支持的数据域所对应的端点,以及任何 webhook 签名方案。只实现可公开确认的部分 —— 其余保留为诚实的桩。
一个 API key 或 OAuth client_id / client_secret,经 VendorConfigMIROBODY_VENDOR_<ID>_* 环境变量读取。

三处改动

1

创建 vendor 文件

选对类别目录:platform/(B2B 聚合器)、device/(消费品牌)、phone/(手机厂商云 API)或 ehr/。创建 src/health/vendor/<bucket>/<id>.cpp
2

将其加入 CMake 源文件列表

vendor 文件是显式列出的(不用通配):把你新的 .cpp 加到 CMakeLists.txtMIROBODY_CORE_SOURCES 列表里、与兄弟 vendor 条目相邻的位置(紧接 src/health/vendor/registry.cpp 之后的那一块)。
3

注册它

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));
}

}}
动手前先研究最接近的真实客户端:Fitbitdevice/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 模式

数据映射

数据域、UCUM 单位与 FHIR 输出

Provider 测试

端到端验证你的 vendor

Garmin / Fitbit 示例

对照契约的真实客户端