# Turbo Agent Auth 使用与设计手册（系统级）

本文是上游系统对接 Turbo Agent Auth 的完整手册，按"先概念、后落地"的顺序讲清楚设计方法、配置步骤与执行效果。目标是让你在不写一行业务代码的情况下，让授权像拼积木一样可编排、可观测、可复用。

---

## 架构说明

turbo-agent-auth 采用三层架构设计，实现了业务逻辑与核心机制的解耦：

1. **核心层 (auth_core.py)**: 纯粹的认证流程执行引擎，无数据库依赖
   - `AuthFlowExecutor`: 基于生成器的多阶段流程编排器
   - `parse_response_data`: 统一的响应解析引擎（支持 JSON/Headers/Cookies/HTML）
   - `prepare_login_request_config`: 请求配置构建器（模板渲染、参数注入）

2. **服务层 (services/auth_service.py)**: 数据库驱动的业务服务
   - 平台/授权方式/秘钥的 CRUD 操作
   - 集成 `auth_core` 执行登录/刷新流程
   - 持久化认证结果与执行轨迹

3. **客户端层 (client_helper.py)**: 独立的 HTTP 客户端工具
   - `AuthClient` / `AsyncAuthClient`: 同步/异步认证客户端
   - 支持 `login()` / `refresh()` / `get/post/put/patch/delete` 等完整 HTTP 方法
   - 无需数据库连接，可在脚本/服务间复用

详细架构设计请参考 `docs/design.md`。

---

## 0. 总览：这套系统解决什么问题？

- 统一模型：把不同平台的授权统一抽象为 `Platform`、`AuthMethod`、`Secret` 三层。
- 配置驱动：不硬编码平台细节，用“表单 Schema + 请求模板（Flow）+ 响应解析（Mapping）+ 放置策略（Placements）”描述登录和使用方式。
- 两阶段放置：
  - 登录阶段：步骤间通过 `requestPlacements` 把上一步解析出的值（HTML/Cookies/Headers/Body）注入下一步请求。
  - 业务阶段：通过 `authFieldPlacements` 把最终凭证（保存在 `Secret.data`）注入到真实业务请求。
- 可观测：自动记录每个字段的来源（`authDataSources`），自动推导有效期（`expiresAt`）。

你可以把它理解为“低代码登录机器人”：给它一份“如何登录”和“如何把结果塞回请求/凭证”的说明，它就能完成登录、保存凭证，并把凭证投放到后续 API 调用里。

---

## 1. 第一步：创建平台 Platform（你要对接谁）

平台描述的是“目标系统”的静态信息，用于分组与人类可读。

- 输入（你提供）
  - `id`、`orgId`、`nameId`、`name`、`code`、`baseUrl`（可选）。
- 动作（系统做什么）
  - 在数据库中创建一条平台记录，作为后续授权方式的归属。
- 输出（你可以拿到什么）
  - 一条 `Platform` 记录，供 `AuthMethod.platformId` 引用。

示例命令：
```bash
curl -X POST http://127.0.0.1:8000/v1/platforms \
  -H 'content-type: application/json' \
  -d '{
    "id":"plat_demo",
    "orgId":"org_demo",
    "nameId":"demo",
    "name":"Demo Platform",
    "code":"demo",
    "baseUrl":"https://api.demo.com/"
  }'
```

---

## 2. 第二步：定义授权的“存储与获取方法” AuthMethod（怎么登录、怎么解析、怎么投放）

`AuthMethod` 是核心：它告诉系统“登录需要哪些表单字段（`loginFieldsSchema`）”“如何发请求（`loginFlow`）”“如何从响应里采集值（`responseMapping`）”“最终凭证该如何放到业务请求里（`authFieldPlacements`）”。

### 2.1 形态与字段
- `authType`：授权类型（JWT / OAuth1 / OAuth2 / APIKey / Cookies / SMTP / POP3）。影响默认模板与常见字段。
- `loginFieldsSchema`：登录时需要的输入字段（由前端渲染登录表单）。
- `loginFlow`：登录请求模板，支持两种形态：
  - 单步：直接提供 `url/method/headers/query/body_template` → 发一次请求；
  - 多步：`steps: []`，每步可以有自己的 `responseMapping`，并通过 `requestPlacements` 把上一步的结果注入下一步请求（headers/query/cookies/body）。
- `responseMapping`：对“最终响应”的解析规则（JSON/Headers/Cookies/HTML），产出写进 `Secret.data` 的字段和到期时间。
- `authFieldPlacements`：最终业务请求的注入位置（headers/query/cookies/body/metadata）。
- `defaultValiditySeconds` / `refreshBeforeSeconds`：默认有效期与刷新提前量（当响应未提供过期信息时兜底）。

### 2.2 关键概念：requestPlacements（登录阶段步骤间注入）
- 作用：把“上下文里的值”（来自 loginPayload + 步骤解析产物）写入当前步骤请求：
  - `headers` / `query` / `cookies` / `body`
- 取值写法：
  - 简写路径：`"session": "cookies.session"` → 从上下文取值；
  - 对象写法：`{"sourceField": "cookies.session", "format": "session={{value}}"}` → 支持简单格式拼接；
  - 字面量：`{"value": "abc"}`。
- 上下文演化：
  1) 初始为 `loginPayload`（前端按 `loginFieldsSchema` 采集）+ `runtime.*` 自动注入；
  2) 步骤响应按 `responseMapping` 解析后合并回上下文（得到 `cookies.*`、`csrf_token` 等）；
  3) 下一步的 `body_template` 与 `requestPlacements` 均可引用这些值。

### 2.3 关键概念：responseMapping（统一解析器）
- 支持分区：`body.fields`（json path）、`headers.fields`、`cookies`（含 Set-Cookie 聚合）、`html.fields`（selector 或 regex）。
- 支持步骤级与顶层：
  - 步骤级：解析中间结果，供下一步 `requestPlacements` 使用；
  - 顶层：解析最终响应，产出写入 `Secret.data` 的凭证与有效期。
- 有效期推导：综合 `expires_in`/`expires_at` 与 cookie 的 `max-age`/`expires`，取最早者为 `Secret.expiresAt`。

### 2.4 创建/更新 AuthMethod 示例
（单步：微信 `stable_token`；多步：Superset 表单登录），完整示例可参考 `docs/auth_method.md`。

---

## 3. 第三步：提交并存储具体秘钥 Secret（凭证实体）

`Secret` 保存的是“某个授权方式下某个账号”的最终凭证，以及登录表单等上下文信息。

- 输入（你提供）
  - `id`、`name`、`tags`（可选）、`credential`（可手动直接填最终凭证，或留空由自动登录写入）。
  - `loginPayload`：按 `loginFieldsSchema` 提供，作为登录所需的表单数据。
  - `autoLoginEnabled`：
    - `true`（默认）：首次创建时会使用 `loginPayload` 立即执行登录并持久化登录表单（后续可自动登录/刷新）。
    - `false`：**不校验也不持久化** `loginPayload`。适用于隐私场景（不存账号密码）或仅存储结果场景（通过预授权拿到 Token 后直接存入 `data`）。
  - 注意：若 `AuthMethod` 定义了 `loginFieldsSchema`，通常要求提供 `loginPayload`；但当 `autoLoginEnabled=false` 时，系统允许不提供 `loginPayload`（或提供不完整的），此时仅保存 `data` 中的凭证。
- 动作（系统做什么）
  - 校验 `data`；若 `autoLoginEnabled` 不为 `false`，则校验 `loginPayload` 是否满足对应 Schema；
  - 首次创建：如提供了 `loginPayload` 且定义了 `loginFlow`，会执行登录（多步/单步均可）：
    1) 发起请求，按步骤 `responseMapping` 解析，
    2) 通过 `requestPlacements` 将解析产物注入下一步，
    3) 最终用顶层 `responseMapping` 生成 `Secret.data`，推导 `expiresAt`；
  - 记录字段来源 `authDataSources`（如 `headers.set-cookie`、`html.selector`、`body.path`、`manual-input`）。
- 输出（你可以拿到什么）
  - 创建接口返回「秘钥 + 执行详情」的复合体，用于一次性观察首次登录轨迹：
    - `secret`：一条 `Secret` 记录，包含 `data`、`authDataSources`、`expiresAt`、`lastRefreshed`、`status` 等；
    - `execution`：本次登录的步骤、请求快照、解析结果、是否成功、错误信息等。

示例命令（创建并尝试首次登录，返回执行轨迹）：
```bash
curl -X POST http://127.0.0.1:8000/v1/auth-methods/<auth_method_id>/secret \
  -H 'content-type: application/json' \
  -d '{
    "id":"sec_demo",
    "name":"demo-account",
    "tags":["prod"],
    "credential":{},
    "loginPayload": { ... 与 loginFieldsSchema 对应 ... },
    "autoLoginEnabled": true
  }'
```

对隐私更严格的场景（不持久化登录表单，仅存储 Token）：
```bash
curl -X POST http://127.0.0.1:8000/v1/auth-methods/<auth_method_id>/secret \
  -H 'content-type: application/json' \
  -d '{
    "id":"sec_demo_privacy",
    "name":"demo-privacy",
    "credential": {"access_token": "ey..."},
    "autoLoginEnabled": false
  }'
```
此时服务不会保存 `loginPayload`，仅保存 `credential` 中的凭证；后续每次登录都需在调用登录接口时再提交账号密码。

---

## 4. 秘钥生命周期与接口选择（创建/更新/登录/失效/通知）

下面给出 Secret 的常见操作及“何时用哪个接口”的建议。

### 4.0 预授权验证（不创建秘钥）
- 接口：`POST /v1/auth-methods/{auth_method_id}/pre-auth`
- 何时使用：
  - 用户在前端输入账号密码，希望先验证是否正确，或获取 Token/Cookies，但暂时不想保存到数据库；
  - 获取到的 Token 可用于后续创建 `autoLoginEnabled=false` 的 Secret。
- 行为：执行登录流程，返回执行轨迹（含提取到的 Token/Cookies），但不创建 Secret 记录。

示例：
```bash
curl -X POST http://127.0.0.1:8000/v1/auth-methods/<auth_method_id>/pre-auth \
  -H 'content-type: application/json' \
  -d '{
    "loginPayload": { "username": "...", "password": "..." },
    "proxy": "http://127.0.0.1:7890"
  }'
```

### 4.1 创建或替换（首次上线/更换账号）
- 接口：`POST /v1/auth-methods/{auth_method_id}/secret`
- 何时使用：
  - 首次为某个 `AuthMethod` 建立秘钥；
  - 或同名账号需要替换（系统会以“同名最新一条”作为更新目标）。
- 行为与返回：
  - 若提供 `loginPayload` 且配置了 `loginFlow`，会立即执行登录并返回 `execution` 轨迹；
  - `autoLoginEnabled=true` 时会持久化 `loginPayload`，便于后续自动登录；
  - `autoLoginEnabled=false` 时不会持久化登录表单，也不强制校验 `loginPayload`。

### 4.2 更新（元数据/表单/策略）
- 接口 A：`PATCH /v1/secrets/{secret_id}`（仅更新，不返回执行轨迹）
  - 适合：修改 `name`/`tags`/`data`/`expiresAt`/`autoLoginEnabled` 等元数据；
  - 若本次希望触发登录：
    - 当 `autoLoginEnabled=true` 或已存储过 `loginPayload` 时，可只更新表单字段；
    - 或转用接口 B 以拿到执行轨迹。
- 接口 B：`PATCH /v1/secrets/{secret_id}/with-execution`（更新并返回登录轨迹）
  - 适合：需要确认本次登录每个步骤状态（调试/变更验证/联调）；
  - 可在 body 里携带 `loginPayload`，并在 `autoLoginEnabled=true` 时自动持久化。

示例（更新并返回执行详情）：
```bash
curl -X PATCH http://127.0.0.1:8000/v1/secrets/<secret_id>/with-execution \
  -H 'content-type: application/json' \
  -d '{
    "loginPayload": { ... 如需覆盖本次登录表单 ... }
  }'
```

### 4.3 手动触发登录
- 接口 A：`POST /v1/secrets/{secret_id}/login`（快速，无轨迹）
- 接口 B：`POST /v1/secrets/{secret_id}/login/trace`（返回执行轨迹）
- 何时使用：
  - 周期性刷新场景（A）；
  - 联调/回归/排障需要观察每一步（B）。
- autoLogin 策略与 `loginPayload`：
  - 若 `autoLoginEnabled=false` 且未持久化过 `loginPayload`，调用登录接口时必须在请求体提供 `loginPayload`，否则会报错：`本次登录需要提供 loginPayload 字段`；
  - `storeLoginPayload=true` 可在本次登录后写入/覆盖持久化的 `loginPayload`（适用于你决定由服务端托管登录表单的场景）。

可选代理执行（仅本次生效，不会持久化）：

```bash
# 方式1：在 Body 中传入 proxy（仅 /login 与 /login/trace 支持）
curl -X POST http://127.0.0.1:8000/v1/secrets/<secret_id>/login/trace \
  -H 'content-type: application/json' \
  -d '{
    "loginPayload": { ... },
    "storeLoginPayload": false,
    "proxy": "http://127.0.0.1:7890"
  }'

# 方式2：在创建/更新并执行的接口上通过 query 传入（仅当该接口触发了登录时生效）
curl -X POST "http://127.0.0.1:8000/v1/auth-methods/<auth_method_id>/secret?proxy=http://127.0.0.1:7890" \
  -H 'content-type: application/json' \
  -d '{
    "id":"sec_demo",
    "name":"demo",
    "data":{},
    "loginPayload": { ... },
    "autoLoginEnabled": true
  }'

curl -X PATCH "http://127.0.0.1:8000/v1/secrets/<secret_id>/with-execution?proxy=socks5://127.0.0.1:1080" \
  -H 'content-type: application/json' \
  -d '{
    "loginPayload": { ... }
  }'
```

说明：`proxy` 支持 `http://`、`https://`、`socks5://` 等常见代理协议（使用 `socks5://` 需安装依赖 `httpx[socks]`，本项目已在依赖中启用），作用范围仅限本次登录请求链路；不会被保存到任何配置或数据库。

示例（隐私模式下的手动登录并返回轨迹，不持久化表单）：
```bash
curl -X POST http://127.0.0.1:8000/v1/secrets/<secret_id>/login/trace \
  -H 'content-type: application/json' \
  -d '{
    "loginPayload": { ... 按照 loginFieldsSchema 提供 ... },
    "storeLoginPayload": false
  }'
```

### 4.4 标记失效（外部事件/自检失败）
- 接口：`POST /v1/secrets/{secret_id}/invalidate`
- 何时使用：
  - 第三方系统报 token/cookie 已被吊销；
  - 检测到异常（疑似泄露/风控拦截），希望立即阻断使用；
  - 登录失败需要进入“人工介入”流程。
- 行为与效果：将设置 `isValid=false`、`status=invalid`，并将 `invalidNotified=false`；可选填写 `reason` 记录到 `lastLoginError`。

示例：
```bash
curl -X POST http://127.0.0.1:8000/v1/secrets/<secret_id>/invalidate \
  -H 'content-type: application/json' \
  -d '{"reason":"token revoked by upstream"}'
```

### 4.5 标记“已通知用户”（去重告警/工单闭环）
- 接口：`POST /v1/secrets/{secret_id}/invalid-notified`
- 何时使用：
  - 在检测到 `isValid=false` 后，你的通知/工单系统完成了“联系用户/负责人”的动作；
  - 为避免重复通知，调用该接口将 `invalidNotified` 置为 `true`。

示例：
```bash
curl -X POST http://127.0.0.1:8000/v1/secrets/<secret_id>/invalid-notified \
  -H 'content-type: application/json' \
  -d '{"notified": true}'
```

### 4.6 删除秘钥（不再使用）
- 接口：`DELETE /v1/secrets/{secret_id}`
- 何时使用：账号迁移完成/不再对接该系统/误建条目。

```bash
curl -X DELETE http://127.0.0.1:8000/v1/secrets/<secret_id>
```

### 4.7 刷新策略（如定义了 refreshFlow）
- 若定义了 `refreshFlow`/`refreshFieldsSchema`，可用与登录类似的方式执行刷新；
- 可在你的调度器里按 `refreshBeforeSeconds` 与 `expiresAt` 判断时机，调用登录接口达成刷新（多数平台“刷新”与“登录”同形）。

---

## 5. 获取与使用：如何把凭证带到业务请求里

- 获取：
  - `GET /v1/auth-methods/{auth_method_id}/secret?name=...` 或 `GET /v1/secrets/{secret_id}`。
  - 读取 `Secret.credential`（最终凭证）与 `expiresAt`（到期时间）。
- 使用：
  - 按 `authFieldPlacements` 把 `Secret.credential` 注入业务请求：
    - `headers`：如 `Authorization: Bearer {{credential.access_token}}`；
    - `query`：如 `?access_token={{credential.access_token}}`；
    - `cookies`：如 `Cookie: session={{credential.cookies.session}}`；
    - `body`：在 JSON 或表单中追加字段；
    - `metadata`：非直接发送的附加信息（供你方执行器使用）。
  - 注意：`authFieldPlacements` 只用于“最终业务请求”，不参与登录阶段；登录阶段靠 `requestPlacements` 串起步骤。

输出：
- 正确注入的业务请求；
- 上游可根据 `expiresAt` 决定是否先刷新再调用。

---

## 6. 解析调试：快速校验 responseMapping

在对接新平台、或遇到响应变化时，可单独调用解析接口校验你的 `responseMapping`。

```bash
curl -X POST http://127.0.0.1:8000/v1/auth-methods/<auth_method_id>/parse \
  -H 'content-type: application/json' \
  -d '{
    "body": { ... 模拟服务返回的 JSON ... },
    "headers": { ... 原样填写多值头或单值头 ... },
    "cookies": { ... 客户端读到的 cookie ... },
    "rawBody": "... 原始文本/HTML（可选，用于 html.selector/regex） ..."
  }'
```

输出：
- `extracted`：解析出的字段；
- `fieldSources`：每个字段对应的来源标签；
- `expiresAt`：系统推导的到期时间（若可计算）。

---

## 7. 心智模型与最佳实践

- 一切皆上下文：从 `loginPayload` 起步，解析产物不断累加，`requestPlacements` 只是“把上下文里的值搬到合适的位置”。
- 职责清晰：登录阶段注入（`requestPlacements`）和业务阶段注入（`authFieldPlacements`）不要混用。
- 解析留证据：开启 `responseMapping` 的 `source`/路径标记，让 `authDataSources` 可读，排查更轻松。
- 过期策略优先保守：token/cookie 取最早；必要时用 `refreshBeforeSeconds` 提前刷新。
- 最小可用：能用单步就不做多步；只有在确实需要（如 Superset 的 csrf+cookie）时才写 `steps`。

---

## 8. 常见场景一览

- 微信 stable_token（单步）：
  - `loginFlow` 只有一跳；`responseMapping` 从 JSON 中取 `access_token`、`expires_in`；
  - `authFieldPlacements.query.access_token -> data.access_token`；
  - 输出：`Secret.data.access_token`、`expiresAt`。

- Superset Cookies（两步）：
  - 第 1 步：POST `/login/`，解析 HTML `#csrf_token`、聚合 `Set-Cookie` 得到 `cookies.session`；
  - 第 2 步：`requestPlacements.cookies.session <- cookies.session`，`requestPlacements.body.csrf_token <- csrf_token`；
  - 顶层 `responseMapping.cookies.collectAll -> data.cookies` 并推导 `expiresAt`；
  - 输出：`Secret.data.cookies.session`、`expiresAt`、字段来源（cookies/html）。

- X 平台（手填）：
  - 无 `loginFlow`，由 `authFieldsSchema` 直接定义最终凭证；
  - 你方把用户在后台拿到的 token 手工录入到 `Secret.credential`；
  - 输出：`Secret.credential` 与 `authDataSources=manual-input`。

---

## 9. 参考与附录

- 默认模板：`src/turbo_agent_auth/config/auth_schemas.yaml`
- 核心服务：`src/turbo_agent_auth/services/auth_service.py`
- 路由接口：`src/turbo_agent_auth/api/routes.py`
- 详尽配置说明：`docs/auth_method.md`
- 快速示例：`weixin.md`、`x.md`

如需我为你的环境直接生成 Superset 的两步 `AuthMethod` 与 `Secret` 演示请求，请告知 baseUrl/用户名/密码（可发占位，我将生成带占位的 curl，便于你替换后直接运行）。

---

## 10. 统一：获取授权并按类型投放到业务请求（代码示例）

下面示例演示一个“客户端侧统一适配器”的心智模型：

1. 调用服务端获取 `Secret`（含 `data`、`usageMapping`、`type`）。
2. 按 `type`（Cookies / JWT / OAuth1 / OAuth2 / APIKey / SMTP / POP3 / Login 泛指可产生 token 的流程）决定默认放置策略。
3. 使用 `usageMapping`（即 `AuthMethod.authFieldPlacements`）解析需要注入的 headers/query/cookies/body/metadata。
4. 构建最终业务请求（对 SMTP/POP3 这类非 HTTP 协议，使用 metadata 来指导连接）。

> 说明：
> - 若存在 `usageMapping`，它是首选；否则可根据内置类型做保守的默认推断（例如 `JWT` 默认尝试 `Authorization: Bearer <access_token>`，`APIKey` 尝试 `X-API-Key` 或 query）。
> - `metadata` 部分不直接发给远端，而是交给你方的执行器（如邮件客户端、OAuth 签名器）。
> - 路径解析：本示例用“`.` 分隔”的简易深度访问（data.access_token / data.cookies.session）。

### 10.1 Python 统一适配器示例

```python
import requests
from typing import Any, Dict

BASE = "http://127.0.0.1:8000"

def get_secret(secret_id: str) -> Dict[str, Any]:
    r = requests.get(f"{BASE}/v1/secrets/{secret_id}")
    r.raise_for_status()
    return r.json()

def get_by_path(obj: Any, path: str):
    cur = obj
    for p in path.split('.'):
        if isinstance(cur, dict) and p in cur:
            cur = cur[p]
        else:
            return None
    return cur

def resolve_spec(spec: Any, data: Dict[str, Any]):
    # 支持几种形式：字符串路径 / {sourceField, format} / {tokenField, type} / {value}
    if isinstance(spec, str):
        return get_by_path(data, spec) if ('.' in spec or spec in data) else spec
    if isinstance(spec, dict):
        if 'value' in spec:
            return spec['value']
        src_field = spec.get('sourceField')
        token_field = spec.get('tokenField') or spec.get('valueField')
        fmt = spec.get('format') or spec.get('template')
        token_type = spec.get('type')
        field = src_field or token_field
        val = get_by_path(data, field) if field else None
        if val is None:
            return None
        if token_type and token_type.lower() == 'bearer':
            return f"Bearer {val}"
        if fmt:
            return fmt.replace('{{value}}', str(val))
        return val
    return spec

def apply_usage_mapping(usage_mapping: Dict[str, Any], credential: Dict[str, Any]):
    result = {"headers": {}, "query": {}, "cookies": {}, "body": {}, "metadata": {}}
    for section in result.keys():
        spec_section = usage_mapping.get(section)
        if isinstance(spec_section, dict):
            for k, v in spec_section.items():
                rv = resolve_spec(v, credential)
                if rv is not None:
                    result[section][k] = rv
    return result

def infer_defaults(secret_type: str, credential: Dict[str, Any]):
    # 当 usageMapping 为空时的兜底策略
    headers = {}
    query = {}
    cookies = {}
    metadata = {}
    at = data.get('access_token') or data.get('token')
    if secret_type in ('JWT', 'OAuth2') and at:
        headers['Authorization'] = f'Bearer {at}'
    elif secret_type == 'APIKey':
        key = data.get('api_key')
        if key:
            headers['X-API-Key'] = key
    elif secret_type == 'Cookies':
        # 聚合所有 cookies.*
        if 'cookies' in data and isinstance(data['cookies'], dict):
            for ck, cv in data['cookies'].items():
                cookies[ck] = cv
    elif secret_type == 'OAuth1':
        # 仅保留原始字段到 metadata，具体签名由上层执行器处理
        for f in ['oauth_token', 'oauth_token_secret', 'access_token', 'access_token_secret']:
            if f in data:
                metadata[f] = data[f]
    elif secret_type in ('SMTP', 'POP3'):
        # 非 HTTP 协议，全部注入 metadata
        for f in data.keys():
            metadata[f] = data[f]
    return {"headers": headers, "query": query, "cookies": cookies, "body": {}, "metadata": metadata}

def build_request(secret_id: str, url: str):
    secret = get_secret(secret_id)
    credential = secret.get('credential', {})
    usage_mapping = secret.get('usageMapping') or {}
    stype = secret.get('type')
    if usage_mapping:
        applied = apply_usage_mapping(usage_mapping, credential)
    else:
        applied = infer_defaults(stype, credential)

    # 对于 SMTP/POP3 等，applied['metadata'] 由你自定义的连接层使用；此处仅演示 HTTP 请求。
    resp = requests.get(url, headers=applied['headers'], params=applied['query'], cookies=applied['cookies'])
    return resp, applied

if __name__ == '__main__':
    # 示例：假设 secret 类型可能是 JWT/OAuth2/Cookies...
    r, applied = build_request('<your-secret-id>', 'https://httpbin.org/get')
    print('status=', r.status_code)
    print('applied=', applied)
```

### 10.2 Node.js 统一适配器示例

```js
async function getSecret(secretId) {
  const res = await fetch(`http://127.0.0.1:8000/v1/secrets/${secretId}`);
  if (!res.ok) throw new Error('secret fetch failed');
  return res.json();
}

function getByPath(obj, path) {
  return path.split('.').reduce((cur, p) => (cur && typeof cur === 'object' ? cur[p] : undefined), obj);
}

function resolveSpec(spec, data) {
  if (typeof spec === 'string') {
    return spec.includes('.') || data[spec] !== undefined ? (getByPath(data, spec) ?? data[spec]) : spec;
  }
  if (spec && typeof spec === 'object') {
    if (spec.value !== undefined) return spec.value;
    const field = spec.sourceField || spec.tokenField || spec.valueField;
    let val = field ? (getByPath(data, field) ?? data[field]) : undefined;
    if (val == null) return undefined;
    if (spec.type && spec.type.toLowerCase() === 'bearer') return `Bearer ${val}`;
    if (spec.format || spec.template) return (spec.format || spec.template).replace('{{value}}', String(val));
    return val;
  }
  return spec;
}

function applyUsageMapping(mapping = {}, credential = {}) {
  const sections = { headers: {}, query: {}, cookies: {}, body: {}, metadata: {} };
  for (const sec of Object.keys(sections)) {
    const conf = mapping[sec];
    if (conf && typeof conf === 'object') {
      for (const [k, v] of Object.entries(conf)) {
        const rv = resolveSpec(v, credential);
        if (rv !== undefined) sections[sec][k] = rv;
      }
    }
  }
  return sections;
}

function inferDefaults(type, credential) {
  const applied = { headers: {}, query: {}, cookies: {}, body: {}, metadata: {} };
  const at = credential.access_token || credential.token;
  if ((type === 'JWT' || type === 'OAuth2') && at) {
    applied.headers.Authorization = `Bearer ${at}`;
  } else if (type === 'APIKey' && credential.api_key) {
    applied.headers['X-API-Key'] = credential.api_key;
  } else if (type === 'Cookies' && credential.cookies) {
    Object.assign(applied.cookies, credential.cookies);
  } else if (type === 'OAuth1') {
    ['oauth_token','oauth_token_secret','access_token','access_token_secret'].forEach(f => { if (credential[f]) applied.metadata[f] = credential[f]; });
  } else if (type === 'SMTP' || type === 'POP3') {
    Object.assign(applied.metadata, credential);
  }
  return applied;
}

async function buildRequest(secretId, url) {
  const secret = await getSecret(secretId);
  const credential = secret.credential || {};
  const stype = secret.type;
  const usage = secret.usageMapping || {};
  const applied = Object.keys(usage).length ? applyUsageMapping(usage, credential) : inferDefaults(stype, credential);
  const u = new URL(url);
  Object.entries(applied.query).forEach(([k, v]) => u.searchParams.set(k, v));
  const headers = applied.headers;
  const res = await fetch(u.toString(), { headers, credentials: 'include' });
  return { res, applied };
}

// 演示调用
buildRequest('<your-secret-id>', 'https://httpbin.org/get').then(({ res, applied }) => {
  console.log('status', res.status);
  res.text().then(t => console.log('body', t));
  console.log('applied', applied);
});
```

### 10.3 类型要点速览

- Cookies：通常在 `usageMapping.cookies` 下列出需要放置的 cookie；兜底策略即全部复制。
- JWT / OAuth2：优先使用配置的 `Authorization` 头；若无映射则尝试 `Bearer access_token`。
- OAuth1：`usageMapping.metadata.oauth1` 指导签名；若无则把核心字段放入 metadata。
- APIKey：可配置在 `headers` 或 `query`；若无映射则尝试 `X-API-Key` 头。
- SMTP / POP3：不走 HTTP 请求注入；将凭证放入 metadata，由邮件客户端使用。
- Login（泛指有 loginFlow 的其它类型）：按 `responseMapping` 产出的字段看作 `data`，再走上述分发策略。

> 小技巧：若你希望彻底覆盖默认逻辑，确保在创建 `AuthMethod` 时把所有需要的投放位置都写进 `authFieldPlacements`，这样客户端永远只依赖 `usageMapping`。

---

## 11. 全局 API Key 鉴权与部署说明（新增）

### 11.1 API Key 鉴权（Authorization Bearer）
系统启动时可通过环境变量或 CLI 参数配置一个全局访问密钥：

- 环境变量：`AUTH_API_KEY=<your_key>`
- CLI 参数：`uv run turbo-agent-auth serve run --api-key <your_key>`

一旦配置，所有 `/v1/*` 路由必须在请求头附加：

```
Authorization: Bearer <your_key>
```

未配置时（即没有设置 `AUTH_API_KEY` 且未传 CLI 参数）服务保持原行为，不对 `/v1/*` 做密钥校验；`/health` 始终开放。失败返回：`401 {"detail": "Invalid API key"}`。

示例：
```bash
AUTH_API_KEY=abc123 uv run turbo-agent-auth serve run --host 0.0.0.0 --port 8000
curl -H 'Authorization: Bearer abc123' http://127.0.0.1:8000/v1/ping
```

### 11.2 Docker 单镜像构建
项目已提供单阶段 `Dockerfile`（使用 uv 官方基础镜像），核心步骤：
1. 复制 `pyproject.toml`（与可选 `uv.lock`）并执行 `uv sync --no-dev` 安装生产依赖；
2. 复制源码与 `README.md`；
3. 生成 Prisma Client（若构建阶段已设置 `DATABASE_URL`）；
4. 运行 Typer CLI：`uv run turbo-agent-auth serve run`。

构建与运行：
```bash
docker build -t turbo-agent-auth:latest .
docker run --rm -p 8000:8000 \
  -e DATABASE_URL='postgresql://user:pass@host:5432/dbname' \
  -e AUTH_API_KEY=abc123 \
  turbo-agent-auth:latest
curl -H 'Authorization: Bearer abc123' http://127.0.0.1:8000/v1/ping
```

### 11.3 docker-compose 编排
示例 `docker-compose.yml` 提供 Postgres + 应用：
```bash
docker compose up -d --build
curl -H 'Authorization: Bearer changeme' http://127.0.0.1:8000/v1/ping
```
初始化数据库迁移：
```bash
docker compose exec auth uv run prisma migrate deploy
docker compose exec auth uv run prisma generate
```
清理：
```bash
docker compose down -v
```

### 11.4 代理执行（回顾）
登录/首次创建执行/更新并执行/轨迹登录均支持一次性 `proxy` 参数：
- Query：`?proxy=http://127.0.0.1:7890`
- Body：`{"proxy": "socks5://127.0.0.1:1080"}`
不持久化、不记录到数据库；支持 `http`/`https`/`socks5`。

### 11.5 安全建议
- 将 `AUTH_API_KEY` 置于容器编排的密钥管理或 CI/CD Secret Store；
- 若需要逐调用级别鉴权（多租户），可后续扩展为数据库表/签名方案（当前实现是“单实例静态密钥”）。
- 若需双兼容旧 `X-API-Key` 头，可在 `_require_api_key` 中增加分支：优先检查 Bearer，不存在则尝试旧头；默认未启用，以免放宽规则。

---

## 12. 调试与执行结果详解

在调用 `/auth-methods/{id}/pre-auth`（预授权）或 `perform_login_with_trace`（带轨迹登录）接口时，系统会返回详细的执行报告 `LoginExecutionResult`。理解这些字段有助于排查登录流程配置是否正确。

### 12.1 核心字段说明

- **finalExtracted** (`Dict[str, Any]`)
  - **含义**：这是登录流程结束后，系统从所有响应（Body/Headers/Cookies）中提取到的**最终凭证数据**。
  - **作用**：如果这是正式登录，这些数据会被直接保存到 `Secret.data` 中。
  - **示例**：`{"access_token": "eyJ...", "csrf_token": "abc", "cookies": {"session_id": "123"}}`

- **finalFieldSources** (`Dict[str, str]`)
  - **含义**：记录了 `finalExtracted` 中每个字段的数据来源路径。
  - **作用**：帮助你确认某个字段是来自 Body 的哪个 JSON 路径，还是来自 Set-Cookie 头。
  - **示例**：`{"access_token": "body.data.token", "csrf_token": "headers.x-csrf-token"}`

- **finalExpiresAt** (`datetime | null`)
  - **含义**：系统推导出的凭证过期时间。
  - **来源**：可能来自 `expires_in` 字段计算，也可能来自 Cookie 的过期时间，或者 AuthMethod 的默认有效期配置。

- **steps** (`List[LoginStepResult]`)
  - **含义**：记录了登录流程中每一步的执行详情。
  - **包含内容**：
    - `request`: 发出的请求快照（URL, Method, Headers 等）。
    - `statusCode`: 响应状态码。
    - `durationMs`: 耗时（毫秒）。
    - `extracted`: 该步骤提取到的中间变量（会合并到后续步骤的上下文中）。
    - `error`: 如果该步骤失败，记录错误信息。

### 12.2 常见问题排查

1. **finalExtracted 为空**
   - 检查 `responseMapping` 配置是否正确
   - 查看 `steps[*].extracted` 确认中间步骤是否有数据
   - 使用 `/parse` 接口验证解析规则

2. **多步骤登录失败**
   - 检查 `requestPlacements` 是否正确引用了上一步的字段
   - 确认步骤的 `responseMapping` 是否解析到了所需的中间变量
   - 查看 `steps[*].error` 定位具体失败步骤

3. **Token 过期时间不正确**
   - 确认响应中是否包含 `expires_in` 或 `expires_at` 字段
   - 检查 Cookie 是否设置了 `Max-Age` 或 `Expires`
   - 验证 `AuthMethod.defaultValiditySeconds` 配置

---

## 13. 客户端工具使用指南

### 13.1 AuthClient 基本用法

`AuthClient` 是一个独立的 HTTP 客户端，无需数据库连接即可使用。适用于：
- 脚本化任务
- 微服务间调用
- 测试和开发

**重要**：`AuthClient` 和 `AsyncAuthClient` 现在要求传入的参数必须是 `turbo-agent-core` 中的 Pydantic 模型（`Platform`、`AuthMethod`、`Secret`），不再接受普通字典。这确保了类型安全和与 Core 模块的完全对齐。

```python
from turbo_agent_auth.client_helper import AuthClient
from turbo_agent_core.schema.external import Platform, AuthMethod, Secret
from turbo_agent_core.schema.basic import Endpoint
from turbo_agent_core.schema.common import AuthType

# 方式1: 从 API 获取完整对象
import requests
platform_data = requests.get("http://127.0.0.1:8000/v1/platforms/plat_x").json()
auth_method_data = requests.get("http://127.0.0.1:8000/v1/auth-methods/am_x").json()
secret_data = requests.get("http://127.0.0.1:8000/v1/secrets/sec_x").json()

client = AuthClient(
    platform=Platform(**platform_data),
    auth_method=AuthMethod(**auth_method_data),
    secret=Secret(**secret_data)
)

# 方式2: 手动构造（用于测试或独立脚本）
client = AuthClient(
    platform=Platform(
        id="plat_x",
        name="Example Platform",
        name_en="example",
        image_url="",
        officialUrl="https://api.example.com",
        description="",
        endpoint=Endpoint(id="ep_1", baseURL="https://api.example.com")
    ),
    auth_method=AuthMethod(
        id="am_jwt",
        name="JWT Login",
        authType=AuthType.JWT,
        loginFlow={
            "url": "https://api.example.com/auth/login",
            "method": "POST",
            "headers": {"Content-Type": "application/json"},
            "body_template": {
                "username": "{{username}}",
                "password": "{{password}}"
            }
        },
        responseMapping={
            "access_token": "access_token",
            "expires_in": "expires_in"
        },
        authFieldPlacements={
            "headers": {
                "Authorization": {"type": "Bearer", "tokenField": "access_token"}
            }
        }
    ),
    secret=Secret(
        id="sec_demo",
        name="demo-account",
        identifier="demo-account",
        credential={},
        loginPayload={"username": "admin", "password": "secret"}
    )
)

# 执行登录
result = client.login()
print(f"Access Token: {result['access_token']}")

# 发起认证请求（自动注入 Authorization 头）
response = client.get("/api/protected/resource")
print(response.json())

# 刷新 Token
refreshed = client.refresh()
```

### 13.2 AsyncAuthClient 异步用法

```python
import asyncio
from turbo_agent_auth.client_helper import AsyncAuthClient

# 从 API 获取配置对象（与同步版本相同）
import httpx
async with httpx.AsyncClient() as http:
    platform_data = (await http.get("http://127.0.0.1:8000/v1/platforms/plat_x")).json()
    auth_method_data = (await http.get("http://127.0.0.1:8000/v1/auth-methods/am_x")).json()
    secret_data = (await http.get("http://127.0.0.1:8000/v1/secrets/sec_x")).json()

from turbo_agent_core.schema.external import Platform, AuthMethod, Secret

async def main():
    client = AsyncAuthClient(
        platform=Platform(**platform_data),
        auth_method=AuthMethod(**auth_method_data),
        secret=Secret(**secret_data)
    )
    
    # 异步登录
    result = await client.login()
    
    # 并发请求
    tasks = [
        client.get(f"/api/item/{i}")
        for i in range(10)
    ]
    responses = await asyncio.gather(*tasks)
    
    # 异步刷新
    await client.refresh()

asyncio.run(main())
```

### 13.3 多协议支持

#### OAuth1 签名

```python
def custom_oauth1_signer(method, url, headers, params, body, oauth1_meta, data):
    """自定义 OAuth1 签名逻辑"""
    consumer_key = data.get("consumer_key")
    consumer_secret = data.get("consumer_secret")
    # ... 实现签名算法
    return {"Authorization": f"OAuth {signature}"}

response = client.get(
    "/api/resource",
    oauth1_signer=custom_oauth1_signer
)
```

#### SMTP 发送邮件

```python
# Secret 配置 SMTP 连接信息
from turbo_agent_core.schema.external import Secret

secret = Secret(
    id="sec_smtp",
    name="smtp-account",
    identifier="smtp-account",
    credential={
        "smtp_host": "smtp.gmail.com",
        "smtp_port": 587,
        "username": "user@gmail.com",
        "password": "app_password"
    }
)

client = AuthClient(platform, auth_method, secret)

result = client.smtp_send(
    from_addr="sender@example.com",
    to_addrs=["recipient1@example.com", "recipient2@example.com"],
    subject="测试邮件",
    text="纯文本内容",
    html="<h1>HTML 内容</h1>"
)
print(f"发送结果: {result}")
```

#### POP3 读取邮件

```python
client = AuthClient(platform, auth_method, secret)

# 列出邮件
messages = client.pop3_list()
for msg in messages:
    print(f"邮件 {msg['index']}: {msg['size']} 字节, UID: {msg['uid']}")

# 获取邮件内容
raw_email = client.pop3_fetch(1)
print(raw_email)
```

### 13.4 辅助功能

#### 检查是否需要用户协助

```python
from turbo_agent_auth.client_helper import AuthClient

# 创建客户端
client = AuthClient(platform, auth_method, secret)

# 检查当前状态是否需要用户手动介入
if client.needs_user_assistance():
    print("需要用户提供登录凭证或秘钥已过期")
    # 提示用户输入账号密码或联系管理员
else:
    print("可以自动登录或使用现有凭证")
    # 继续执行业务逻辑
```

**使用场景**：
- 在执行自动化任务前检查授权状态
- 判断是否需要向用户请求登录表单
- 避免因缺少凭证导致的失败

### 13.5 高级特性

#### 自定义会话

```python
import httpx

# 自定义连接池配置
session = httpx.Client(
    limits=httpx.Limits(
        max_keepalive_connections=20,
        max_connections=100
    ),
    timeout=httpx.Timeout(30.0)
)

client = AuthClient(platform, auth_method, secret, session=session)
```

#### 代理支持

```python
session = httpx.Client(
    proxy="http://proxy.example.com:8080"
)
client = AuthClient(platform, auth_method, secret, session=session)
```

---

## 14. 参考资料

- **设计文档**: `docs/design.md` - 详细的架构设计与实现原理
- **API 参考**: `docs/api.md` - 完整的 REST API 文档
- **配置示例**: `docs/auth_method.md` - 各种授权方式的配置示例
- **GitHub 仓库**: https://github.com/your-org/turbo-agent-auth

---

**文档版本**: v2.0  
**最后更新**: 2025-12-04  
**维护者**: turbo-agent-auth 团队
   - 检查 `responseMapping` 是否配置正确。
   - 检查接口响应是否符合预期（查看 `steps` 中的 `statusCode` 和 `request`）。

2. **字段值不对**
   - 查看 `finalFieldSources` 确认系统是从哪里提取的该值。
   - 检查 JSONPath 或 Regex 配置是否匹配目标响应结构。

3. **预授权成功但正式使用失败**
   - 预授权 (`pre-auth`) 仅验证登录流程能否跑通并提取数据。
   - 正式使用还依赖 `authFieldPlacements`（投放策略），请检查是否正确配置了如何将 `Secret.data` 注入到业务请求中。
