---
name: openlark-api
description: OpenLark 项目 API 接口实现规范（速查）。用于添加/重构飞书开放平台 API：确定落盘路径、实现 Body/Response + Builder(Request)、对齐 endpoints 常量/enum、补齐 mod.rs 导出，并明确"调用服务端 API"的方法签名/RequestOption 传递约定。触发关键词：API 接口、API 文件、飞书 API、添加 API、调用服务端 API
argument-hint: "[api-id|path|bizTag]"
allowed-tools: Bash, Read, Grep, Glob, Edit
---

# OpenLark API 接口实现规范（速查）

## 🧭 技能路由指南

**本技能适用场景：**
- 添加/重构单个飞书开放平台 API
- 需要确定 API 落盘路径（bizTag → crate → 文件路径）
- 需要参考代码模板（Body/Response + Builder）
- 需要了解端点规范、RequestOption 约定、Service 链式调用

**其他技能：**
- 字段核对 / 抓飞书文档（playwright）→ `Skill(openlark-api-field-verify)`（**读文档唯一入口**）
- 项目级规范体检（架构/API/导出/校验一体）→ `Skill(openlark-code-standards)`
- 审查整体设计规范 → `Skill(openlark-design-review)`
- 统一 `validate()` 写法 → `Skill(openlark-validation-style)`
- 覆盖率（文件在不在）→ `Skill(openlark-api-validation)`

### 关键词触发映射

- 新增 API、重构 API、Builder、Request/Response、mod.rs 导出、RequestOption → `openlark-api`
- 字段核对、文档抓取、playwright、飞书文档字段 → `openlark-api-field-verify`
- 代码规范、规范检查、风格一致性、体检 → `openlark-code-standards`
- 架构设计、public API、收敛方案、feature gating、兼容策略 → `openlark-design-review`
- validate、必填校验、validate_required、空白字符串、校验聚合 → `openlark-validation-style`
- 覆盖率、缺失 API、实现数量、CSV 对比、验证脚本 → `openlark-api-validation`

### 双向跳转规则

- **读文档 / 字段核对**：一律转 `openlark-api-field-verify`（勿用本技能下的 `fetch_docpath.py` 在线抓取）。
- 实现完成后必须跑字段核对门禁（见 §0 步骤 8 / §4）；差异修正仍在本技能落地。
- 若实现问题本质是架构范式冲突（Request/Service 边界），转 `openlark-design-review`。
- 若实现前需要先做全仓规范体检，先跑 `openlark-code-standards`。
- 若实现完成后要核验覆盖率与缺失清单，转 `openlark-api-validation`。

---

本文件只保留"可执行的最小流程"。标准示例见 `references/`；**官方文档抓取**见 `Skill(openlark-api-field-verify)`。

## 🔒 核心契约（所有 crate 必须遵守，不可违反）

> 这些是仓库**唯一规范**，违反任何一条都会导致接口不统一/调用失败。完整正确模板见 `references/standard-example.md`。

1. **新代码默认 `config: Config`（owned）；现有 `Arc<Config>` 的 Service/Client 保持现状，勿为统一所有权单独重构**。`openlark_core::Config` 内部已 `Arc<ConfigInner>`，clone 廉价。新 Request/Service 默认用 `config: Config`，构造用 `Config::build()`（直接返回 `Config`，**不要** `.unwrap()`）。但仓库现有 573+ 文件仍用 `Arc<Config>`（如 `openlark-docs` 的 `DocsClient`/`BaseClient`/`CcmClient`，见 `crates/openlark-docs/src/common/chain.rs:611-629`）——这些保持现状，**不为统一所有权单独重构**。两种形态都不持 HTTP client，"走 Transport"是硬约束。

2. **`R`（`ApiRequest<R>` 泛型）是响应 `data` 字段的内容类型，不是包装层**。`Transport::request` 返回 `ApiResponse<R> = {code,msg,data: R,...}`，`resp.data: Option<R>`。
   - 无 schema/透传：`R = serde_json::Value`，`execute` 返回 `SDKResult<serde_json::Value>`。
   - 有 schema：`R` 就是 data 内容的 typed struct，并 `impl ApiResponseTrait { fn data_format() -> ResponseFormat::Data }`。
   - **❌ 禁止**写成 `XxxResponse { data: Option<T> }` 外面再包一层——core 已自动把 `R` 当作 data 内容解析，再包会**双重嵌套**（运行时才暴露，极难发现）。

3. **禁止绕过 `Transport`**：业务 crate（除 `openlark-core`）**不得** `reqwest::Client::new()` 或自建 HTTP client、不得手工塞 `Authorization` 头、不得手工取 token。全部走 `openlark_core::http::Transport::request`。`Service`/`Request` 只持 `Config`，不持任何 HTTP client 字段。

4. **Token 类型按接口文档「请求头 → Authorization」逐接口选择**：`ApiRequest` 默认 `supported_access_token_types = [User, Tenant]`（覆盖飞书大多数接口）。仅当接口文档 Authorization 标注的凭证类型不在默认集内时，才用 `.with_supported_access_token_types(...)` 显式声明。**判断方法：以每个接口官方文档「请求头 → Authorization」标注为准——要求 `tenant_access_token` → `AccessTokenType::Tenant`；要求 `user_access_token` → `User`；要求 `app_access_token` → `App`。不存在「应用级接口 → App」的通则**（`App` = `app_access_token`，与 `tenant_access_token` 是不同凭证；把要求 `tenant_access_token` 的接口设成 `App` 会被飞书鉴权拒绝，证据见 #511）。

5. **端点路径用常量/enum**（禁止手写 `"/open-apis/..."` 字符串字面量散落），**必填校验统一用 `validate_required!`/`validate_required_list!`**，**每个 Request 必须提供 `execute_with_options(..., RequestOption)` 并把 option 透传到 `Transport::request(..., Some(option))`**。

## 0. 快速工作流（新增一个 API）

1) **定位 API**：在 `./api_list_export.csv` 拿到 `id`、`bizTag`、`meta.*`、**`fullPath`**
   - URL 权威源是 `fullPath`（拼 `https://open.feishu.cn` + `fullPath`）。**不要**用手拼路径，也**不要**默认用 `docPath`（常与 `fullPath` 不一致）。
2) **读文档（门禁）**：用 playwright 抓取（见 §5）。产出文件 `<500` 字符 = 失败，**禁止**继续实现或推断字段。
3) **选 crate**：根据 bizTag 选择 feature crate（见 §1）
4) **定路径**：`crates/{crate}/src/{bizTag}/{project}/{version}/{resource...}/{name}.rs`
5) **写代码**：按抓取到的真实字段写 `Body/Response` + Builder（`execute/send`）+ 端点常量/enum
   - **必须支持 RequestOption**：用于 `user_access_token` / `tenant_key` / 自定义 header
6) **补导出**：在 `mod.rs` 中 `pub mod ...` / `pub use ...`
7) **补链路**：在约定入口补齐链式调用（默认 `service.rs`，但 `openlark-docs` 例外，见 §2）
8) **验证（含字段核对门禁）**：
   - 新增 API 所属 crate feature 已在 `Cargo.toml [features]` 声明；
   - 涉及该 feature 的测试用 `#[cfg(test)]` 模块内 `#![cfg(feature = "...")]`（或模块级 `#[cfg(feature)]`）门控；
   - 若新增 `examples/` 示例，须在 `Cargo.toml [[example]]` 声明 `required-features`；
   - 跑 `just fmt && just lint && just test`，其中 `just lint` 须含 `--all-targets`（覆盖 examples + tests）；
   - **字段核对（必做）**：`python3 tools/verify_api_fields.py --api-id <CSV的id> --fetch-docs`；进程须以 0 退出（抓取失败 / error / warning 均非 0）。详细流程见 `Skill(openlark-api-field-verify)`。**门禁通过不覆盖嵌套结构与响应体完整性**，见 field-verify 技能「工具核对边界」。

## 1. Feature Crate ↔ bizTag

仓库以 `tools/api_coverage.toml` 作为 **crate→bizTag** 的唯一来源。

```bash
# 查看所有映射
python3 tools/validate_apis.py --list-crates

# 验证特定 crate 的覆盖率
python3 tools/validate_apis.py --crate openlark-docs
```

**反查技巧**：落盘路径以"目标 crate 现有结构"为准，参考 `references/file-layout.md`

## 2. Service 链式调用（实现 + 调用约定）

> 本节提供"如何实现"的技术规范。若需要审查"是否应该统一范式"（Request 自持 Config vs Builder → Service），见 `Skill(openlark-design-review) §1`。

### 2.1 实现侧：service.rs

> **event/webhook 类 P2 模块不进统一 `service.rs` 链路**：这类模块（如长连接回调、事件订阅）的入口形态与普通 CRUD API 不同，放在各自独立入口，**不强行塞进** `client.<biz>.service()...` 链。

目标：让 `openlark-client` 能走 `client.<biz>.service().<project>().<version>()...<api>()`

- 若 crate 已有 `src/service.rs`：在顶层 service 新增 `pub fn {bizTag}(&self) -> ...`
- 若没有：创建 `src/service.rs` 并在 `lib.rs` 中 `pub mod service;`
- `openlark-docs` 特例：为避免 strict API 校验脚本把"链式入口"计为 API 实现文件，链式入口放在 `crates/openlark-docs/src/common/chain.rs`，只做模块级入口与 Config 透传，不为 200+ API 手写方法。

#### ⚠️ Service 层标准模式

> 注：这是 `openlark-docs` crate 的真实写法（用 `Arc<Config>`，见 `crates/openlark-docs/src/common/chain.rs:611-629` 的 `DocsClient`）。
> **核心契约 1 已放宽**：新代码默认 owned（`config: Config`），现有 `Arc<Config>` 的 Service/Client 保持现状、勿为统一所有权单独重构。两种形态都**不持 HTTP client**，这点是硬约束。

**正确示例**（参考 `openlark-docs/src/common/chain.rs`）：

```rust
use std::sync::Arc;
use openlark_core::config::Config;

/// DocClient 只持有 Arc<Config>
#[derive(Debug, Clone)]
pub struct DocClient {
    config: Arc<Config>,
}

impl DocClient {
    pub fn new(config: Config) -> Self {
        Self { config: Arc::new(config) }
    }

    /// 子 Service 只透传 Arc<Config>
    pub fn drive(&self) -> DriveService {
        DriveService::new(self.config.clone())
    }
}

/// Service 层只持有 Arc<Config>，不持有独立 HTTP client
#[derive(Debug, Clone)]
pub struct DriveService {
    config: Arc<Config>,
}

impl DriveService {
    pub fn new(config: Arc<Config>) -> Self {
        Self { config }
    }

    pub fn v1(&self) -> DriveV1 {
        DriveV1::new(self.config.clone())
    }
}
```

**❌ 禁止模式**：
- ❌ **`reqwest::Client::new()` 或任何自建 HTTP client**（业务 crate 除 core 外全部禁止，见核心契约 3）
- ❌ 手工塞 `Authorization` 头 / 手工取 token（由 `Transport` 自动注入）
- ❌ Service/Request 持有独立的 HTTP client 字段（只持 `Config`）
- ❌ 使用 `LarkClient` 作为具体类型（它是 trait）
- ❌ 在测试中使用 `.unwrap()` 调用 `Config::build()`（build() 直接返回 Config）

**✅ 正确模式**：
- ✅ Service 只持有 `Arc<Config>`
- ✅ `Config::build()` 直接返回 `Config`，不需要 `.unwrap()`
- ✅ HTTP 传输由 `openlark_core::Transport` 处理

### 2.2 调用侧：RequestOption 约定

**必须提供** `execute_with_options(..., RequestOption)` 或等价签名，并将 option 透传到 `Transport::request(..., Some(option))`

**使用场景**：
- 用户态 API → `user_access_token`
- 商店应用 → `tenant_key` / `app_ticket`
- 链路追踪 → `request_id` / 自定义 header

> ⚠️ 不要只调用 `ApiRequest::request_option(...)`，它仅合并 header，token 推断需要走 Transport

详细示例见 `references/standard-example.md`

## 3. API 模板（以仓库现有风格为准）

> 以下提供两种仓库中真实存在的风格。**实现时优先模仿目标 crate 的现有文件风格**，避免在同一 project/version 内混用多种范式。
>
> 范式一致性审查见 `Skill(openlark-design-review) §1`。

### 3.1 Request / Response

```rust
use openlark_core::{api::ApiRequest, config::Config, http::Transport, SDKResult};
use openlark_core::req_option::RequestOption;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct {Name}Body {
    // 字段按官方文档，用 serde rename 对齐
    // 可选：Option<T> + #[serde(skip_serializing_if = "Option::is_none")]
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct {Name}Response {
    // 字段按官方文档
}
```

### 3.2 Builder + execute/send

> ⚠️ 下面的模板是**骨架**，必须配合 §"🔒 核心契约" 理解。完整正确示例见
> `references/standard-example.md`（以仓库现有风格为准）。

```rust
use openlark_core::{
    api::ApiRequest, config::Config, http::Transport, validate_required, SDKResult,
};
use openlark_core::req_option::RequestOption;
use serde::{Deserialize, Serialize};

// R（ApiRequest<R> 泛型）：无 schema 用 serde_json::Value；有 schema 用 typed struct
// （见 references/standard-example.md 的 B 范式 + impl ApiResponseTrait）。
// ❌ 不要在外面再包一层 XxxResponse { data: Option<...> }——会双重嵌套。

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct {Name}Body {
    // 字段按官方文档，用 serde rename 对齐
    // 可选：Option<T> + #[serde(skip_serializing_if = "Option::is_none")]
}

pub struct {Name}Request {
    config: Config,            // owned（见核心契约 1）
    // 路径/查询参数（按需）
}

impl {Name}Request {
    pub fn new(config: Config) -> Self { /* ... */ }

    pub async fn execute(self, body: {Name}Body) -> SDKResult<serde_json::Value> {
        self.execute_with_options(body, RequestOption::default()).await
    }

    pub async fn execute_with_options(
        self,
        body: {Name}Body,
        option: RequestOption,
    ) -> SDKResult<serde_json::Value> {
        validate_required!(body.<必填字段>, "<字段> 不能为空"); // 见核心契约 5
        // 端点必须复用 crate 的 endpoints 常量或 enum（禁止手写 "/open-apis/..."）
        // 默认 [User, Tenant] 已覆盖大多数接口；仅当接口文档 Authorization 要求 app_access_token
        // 等非默认凭证时，才 .with_supported_access_token_types(vec![...]) 显式声明（见核心契约 4）。
        let req: ApiRequest<serde_json::Value> = ApiRequest::post({ENDPOINT_CONST_OR_ENUM});
        let resp = Transport::request(req, &self.config, Some(option)).await?; // 见核心契约 3
        // resp.data: Option<serde_json::Value>，R 是 data 内容（见核心契约 2）
        resp.data.ok_or_else(|| openlark_core::error::validation_error("响应数据为空", "服务器没有返回有效的数据"))
    }
}
```

## 4. 提交前检查清单

- [ ] 落盘路径正确（与同模块现有结构一致）
- [ ] **已用 playwright 按 `fullPath` 抓取文档**（产出 ≥500 字符）；字段来自真实文档，非同族推断
- [ ] Request/Response 字段对齐官方文档（含 `serde(rename)`）
- [ ] **核心契约 1**：`config: Config`（owned），未用 `Arc<Config>`
- [ ] **核心契约 2**：`R` 是响应 data 内容类型，未在外面再包 `XxxResponse{data}`
- [ ] **核心契约 3**：无 `reqwest::Client::new()` / 手工 token，全走 `Transport::request`
- [ ] **核心契约 4**：token 类型与接口文档「请求头 → Authorization」标注一致（默认 `[User, Tenant]`；仅当文档要求 `app_access_token` 等非默认凭证时才 `.with_supported_access_token_types([...])`）
- [ ] **核心契约 5**：端点用常量/enum（禁手写 URL）；必填字段用 `validate_required!`
- [ ] `execute_with_options(..., RequestOption)` 已提供并透传到 Transport
- [ ] `mod.rs` 已导出；`service.rs`/链式入口已补
- [ ] 新增 feature 已在 `Cargo.toml [features]` 声明；测试/示例的 `#[cfg(feature)]` 与 `[[example]] required-features` 已补
- [ ] `just fmt && just lint --all-targets && just test` 通过
- [ ] **字段核对门禁**：`python3 tools/verify_api_fields.py --api-id <id> --fetch-docs` 无未处理的 error/warning（门禁通过不覆盖嵌套结构与响应体完整性，见 field-verify 技能「工具核对边界」）

## 5. 官方文档读取（唯一入口）

飞书文档是 **SPA**，禁止用本目录 `scripts/fetch_docpath.py` 做在线抓取（常返回空壳）。统一走 field-verify 的 playwright 脚本，**URL 只用 CSV `fullPath`**：

```bash
# 从 CSV 取 fullPath / id 后：
FULL_PATH="$(python3 -c "
import csv
with open('api_list_export.csv', encoding='utf-8-sig') as f:
    for row in csv.DictReader(f):
        if row['id'] == '<API_ID>':
            print(row['fullPath']); break
")"

node .agents/skills/openlark-api-field-verify/scripts/fetch_doc.js \
  "https://open.feishu.cn${FULL_PATH}" \
  "/tmp/doc_<API_ID>.txt"

# 或按 api-id 直接抓（脚本内读 CSV）：
node .agents/skills/openlark-api-field-verify/scripts/fetch_doc.js \
  --from-csv <API_ID> --out /tmp/doc_<API_ID>.txt
```

抓取后按 `Skill(openlark-api-field-verify)` 解析 Request/Response 字段再写代码。离线已有 HTML 时，才可用 `fetch_docpath.py --html-file` 作解析兜底。

## 6. References

- 目录规范与反查：`references/file-layout.md`
- CSV 映射规则：`references/csv-mapping.md`
- 标准示例（照抄结构）：`references/standard-example.md`
- 字段核对与文档抓取：`Skill(openlark-api-field-verify)`
