> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirobody.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 构建一个 Vendor 客户端

> 把一个新的健康数据源实现为 C++ vendor::Vendor：类、工厂，以及注册表条目。

## 概览

为 Mirobody 添加一个健康数据源，意味着编写一个 **C++ `vendor::Vendor`** —— 在 `src/health/vendor/` 下的一个 `.cpp` 文件，外加注册表中的一行。vendor 是**编译进程序**并按 id 解析的。

<Info>
  想**使用**现有 vendor？参见[使用 Provider](/zh/providers/using-providers)。关于其背后的模型 —— 契约、诚实的桩、注册表 —— 参见 [Vendor 系统](/zh/concepts/providers)。
</Info>

## 前置条件

<AccordionGroup>
  <Accordion title="构建工具链" icon="wrench">
    一个可用的 C++11 引擎构建（`./build.sh`）。参见[贡献指南](/zh/development/contributing)。
  </Accordion>

  <Accordion title="该源的公开 API" icon="book">
    该 vendor 的**公开** API 参考：base URL、认证方案、你将支持的数据域所对应的端点，以及任何 webhook 签名方案。只实现可公开确认的部分 —— 其余保留为诚实的桩。
  </Accordion>

  <Accordion title="凭据" icon="key">
    一个 API key 或 OAuth `client_id` / `client_secret`，经 `VendorConfig` 从 `MIROBODY_VENDOR_<ID>_*` 环境变量读取。
  </Accordion>
</AccordionGroup>

## 三处改动

<Steps>
  <Step title="创建 vendor 文件">
    选对类别目录：`platform/`（B2B 聚合器）、`device/`（消费品牌）、`phone/`（手机厂商云 API）或 `ehr/`。创建 `src/health/vendor/<bucket>/<id>.cpp`。
  </Step>

  <Step title="将其加入 CMake 源文件列表">
    vendor 文件是显式列出的（不用通配）：把你新的 `.cpp` 加到 `CMakeLists.txt` 中 `MIROBODY_CORE_SOURCES` 列表里、与兄弟 vendor 条目相邻的位置（紧接 `src/health/vendor/registry.cpp` 之后的那一块）。
  </Step>

  <Step title="注册它">
    在 [`src/health/vendor/registry.cpp`](https://github.com/thetahealth/mirobody/blob/main/src/health/vendor/registry.cpp) 中声明工厂并添加一行。
  </Step>
</Steps>

## vendor 类

一个 vendor 是派生自 `VendorBase` 的**匿名命名空间类**，外加一个自由函数 `make_<id>()` 工厂。它只重写它实现的操作；其余一切继承 `VendorBase` 的"not implemented"桩。骨架：

```cpp theme={null}
// 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));
}

}}
```

<Tip>
  动手前先研究最接近的真实客户端：**Fitbit**（`device/fitbit.cpp`）是完整 OAuth 2.0 品牌的范本（fetch + authorize\_url + revoke + 带签名的 webhook）；**Withings** 用于表单编码的服务；**ehr/ehr.cpp** 用于 FHIR R4 源；**garmin.cpp** 演示如何留下诚实的桩。
</Tip>

## 注册工厂

在 `registry.cpp` 中，与其他工厂并列声明你的工厂，并向 `kVendors` 表添加一行（按矩阵顺序）：

```cpp theme={null}
// 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()` 中。

<Note>
  与 MCP 工具（经宏在编译期自动注册）不同，vendor 使用这张**显式的工厂表** —— 你手动添加声明和 `kVendors` 行。
</Note>

## 你在实现的契约

| 操作                                                      | 何时实现……                      | 否则                  |
| ------------------------------------------------------- | --------------------------- | ------------------- |
| `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()` 为真。

## 构建并验证

```bash theme={null}
./build.sh
./build/mirobody           # your vendor is now in the registry
```

然后针对一个运行中的服务器手动验证 —— 参见 [Provider 测试](/zh/development/provider-testing)。

## 下一步

<CardGroup cols={2}>
  <Card title="OAuth 实现" icon="key" href="/zh/development/oauth-implementation">
    客户端中的 OAuth 2.0 / PKCE 模式
  </Card>

  <Card title="数据映射" icon="arrows-rotate" href="/zh/development/data-mapping">
    数据域、UCUM 单位与 FHIR 输出
  </Card>

  <Card title="Provider 测试" icon="flask" href="/zh/development/provider-testing">
    端到端验证你的 vendor
  </Card>

  <Card title="Garmin / Fitbit 示例" icon="code" href="/zh/examples/garmin-provider">
    对照契约的真实客户端
  </Card>
</CardGroup>
