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

# SDK 指南

> Profy App SDK 完整使用指南：TypeScript 和 Python 双语言支持，OAuth 对接、事件上报、Token 持久化。

# SDK 指南

Profy 提供 TypeScript 和 Python 两套官方 SDK，封装了 OAuth token 交换、自动刷新、Events API 上报等核心能力。

## 安装

<CodeGroup>
  ```bash TypeScript theme={null}
  npm install @profy-ai/sdk
  # or
  bun add @profy-ai/sdk
  ```

  ```bash Python theme={null}
  pip install profy-sdk
  # or
  pip install profy-sdk[sqlalchemy]  # 含可选的 SQLAlchemy 存储层
  ```
</CodeGroup>

## 初始化

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { ProfyApp } from "@profy-ai/sdk";

  const profy = new ProfyApp({
    clientId: process.env.PROFY_APP_ID!,
    clientSecret: process.env.PROFY_APP_SECRET!,
    baseUrl: "https://profy.cn",           // 可选，默认 https://profy.cn
    onTokenRefresh: (token) => {            // 可选，token 刷新后的回调
      // 持久化新 token（存 DB / Redis / 文件）
    },
  });
  ```

  ```python Python (Async) theme={null}
  from profy import ProfyApp

  profy = ProfyApp(
      client_id="your-app-id",
      client_secret="your-app-secret",
      base_url="https://profy.cn",            # 可选
      on_token_refresh=lambda token: ...,     # 可选，token 刷新后持久化
  )
  ```

  ```python Python (Sync) theme={null}
  from profy import ProfyAppSync

  profy = ProfyAppSync(
      client_id="your-app-id",
      client_secret="your-app-secret",
  )
  ```
</CodeGroup>

## 核心 API

### 1. 授权码换 Token

用户在 Profy 授权页面同意后，回调地址收到 `code`，在后端调 SDK 换取 Token：

<CodeGroup>
  ```typescript TypeScript theme={null}
  const token = await profy.exchangeCode(code, redirectUri);
  // token: { accessToken, refreshToken, userId, expiresAt }
  ```

  ```python Python theme={null}
  token = await profy.exchange_code(code, redirect_uri)
  # token: TokenData(access_token, refresh_token, user_id, expires_at)
  ```
</CodeGroup>

<Warning>
  授权码交换必须在后端完成。App Secret 不能暴露给前端。
</Warning>

### 2. 刷新 Token

Token 过期后用 Refresh Token 续期。`reportEvent` 内部自动处理刷新，也可手动调用：

<CodeGroup>
  ```typescript TypeScript theme={null}
  const newToken = await profy.refreshToken(token.refreshToken);
  ```

  ```python Python theme={null}
  new_token = await profy.refresh_token(token.refresh_token)
  ```
</CodeGroup>

### 3. 上报计费事件

用户触发付费操作时，调用 SDK 上报事件。SDK 会自动处理 token 过期重试：

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await profy.reportEvent("generate_report", {
    token,
    idempotencyKey: "unique-key-123",   // 可选，幂等键
    metadata: { source: "web" },         // 可选，附加信息
  });
  ```

  ```python Python theme={null}
  result = await profy.report_event(
      "generate_report",
      token=token,
      idempotency_key="unique-key-123",  # 可选
      metadata={"source": "web"},        # 可选
  )
  ```
</CodeGroup>

### 4. Token 过期检测

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { isTokenExpired } from "@profy-ai/sdk";

  if (isTokenExpired(token)) {
    // token 即将过期（30 秒 buffer）
  }
  ```

  ```python Python theme={null}
  if token.is_expired:
      # token 即将过期（30 秒 buffer）
      pass
  ```
</CodeGroup>

## Token 持久化（Contrib 存储层）

SDK 核心不依赖任何 ORM/数据库。Token 怎么存由你决定。

如果你使用 Drizzle (TS) 或 SQLAlchemy (Python)，SDK 提供了**可选的** contrib 模块，开箱即用。不想用 contrib 也完全可以自建。

### 方案 A：使用 SDK Contrib（推荐快速接入）

<CodeGroup>
  ```typescript TypeScript (Drizzle) theme={null}
  import { ProfyApp } from "@profy-ai/sdk";
  import { profyOAuthToken, DrizzleTokenStore } from "@profy-ai/sdk/contrib/drizzle";

  // 1. 将 profyOAuthToken 加入你的 Drizzle schema
  //    然后执行 drizzle-kit generate && drizzle-kit migrate 建表

  // 2. 创建 Store
  const store = new DrizzleTokenStore(db);

  // 3. 初始化 SDK，绑定 store
  const profy = new ProfyApp({
    clientId: "...",
    clientSecret: "...",
    onTokenRefresh: (token) => store.save(token),
  });

  // 4. 授权后保存 token
  const token = await profy.exchangeCode(code, redirectUri);
  await store.save(token, "events:write");

  // 5. 下次使用时加载 token
  const savedToken = await store.load(userId);
  ```

  ```python Python (SQLAlchemy) theme={null}
  from profy import ProfyApp
  from profy.contrib.sqlalchemy import create_token_model, SQLAlchemyTokenStore

  # 1. 创建 ORM model（跟随 Base.metadata.create_all() 自动建表）
  ProfyOAuthToken = create_token_model(Base)

  # 2. 创建 Store
  store = SQLAlchemyTokenStore(async_session_factory, ProfyOAuthToken)

  # 3. 初始化 SDK，绑定 store
  profy = ProfyApp(
      client_id="...",
      client_secret="...",
      on_token_refresh=store.on_refresh,
  )

  # 4. 授权后保存 token
  token = await profy.exchange_code(code, redirect_uri)
  await store.save(token.user_id, token, scope="events:write")

  # 5. 下次使用时加载 token
  saved_token = await store.load(user_id)
  ```
</CodeGroup>

### 方案 B：自建存储

SDK 只需要一个 `onTokenRefresh` 回调函数，不关心你用什么数据库：

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { ProfyApp, type TokenData } from "@profy-ai/sdk";

  // 用 Redis / Prisma / TypeORM / 文件 / 任何方式存储
  async function saveToken(token: TokenData) {
    await redis.set(`profy:token:${token.userId}`, JSON.stringify(token));
  }

  const profy = new ProfyApp({
    clientId: "...",
    clientSecret: "...",
    onTokenRefresh: saveToken,
  });
  ```

  ```python Python theme={null}
  from profy import ProfyApp, TokenData

  # 用 Redis / MongoDB / 文件 / 任何方式存储
  async def save_token(token: TokenData):
      await redis.set(f"profy:token:{token.user_id}", token.access_token)

  profy = ProfyApp(
      client_id="...",
      client_secret="...",
      on_token_refresh=save_token,
  )
  ```
</CodeGroup>

## Contrib 自动建表说明

| ORM            | 建表方式                                           | 时机                |
| -------------- | ---------------------------------------------- | ----------------- |
| **SQLAlchemy** | `Base.metadata.create_all(engine)`             | 应用启动时自动创建（如果表不存在） |
| **Drizzle**    | `drizzle-kit generate` + `drizzle-kit migrate` | 手动执行迁移命令          |

<Note>
  SQLAlchemy 的 `create_all` 只能建新表，不能给已有表加列。如果你升级了 SDK 且 contrib model 有新字段，需要手动 `ALTER TABLE` 或使用 Alembic 迁移。
</Note>

## Contrib 表结构

`profyOAuthToken` / `profy_oauth_token` 表默认字段：

| 字段              | 类型               | 说明                    |
| --------------- | ---------------- | --------------------- |
| `id`            | serial / integer | 主键                    |
| `profy_user_id` | string(64)       | Profy 用户 ID（唯一索引）     |
| `access_token`  | text             | JWT Access Token (1h) |
| `refresh_token` | text             | Refresh Token (90d)   |
| `expires_at`    | timestamp        | Access Token 过期时间     |
| `scope`         | string           | 授权 scope              |
| `created_at`    | timestamp        | 创建时间                  |
| `updated_at`    | timestamp        | 更新时间                  |

表名可自定义：

```python theme={null}
ProfyOAuthToken = create_token_model(Base, table_name="my_profy_tokens")
```

## 错误处理

SDK 提供结构化异常，方便业务层精确处理：

| 异常                    | HTTP 状态码 | 含义                             |
| --------------------- | -------- | ------------------------------ |
| `AuthExpired`         | 401      | Token 过期且 refresh 也失败，需要用户重新授权 |
| `InsufficientBalance` | 402      | 用户余额不足                         |
| `InvalidEvent`        | 400      | 事件名未在 Profy Studio 配置          |
| `ProfyApiError`       | 其他       | 通用 API 错误（含 429 限频）            |

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { AuthExpired, InsufficientBalance } from "@profy-ai/sdk";

  try {
    await profy.reportEvent("action", { token });
  } catch (e) {
    if (e instanceof AuthExpired) {
      // 引导用户重新授权
    } else if (e instanceof InsufficientBalance) {
      // 提示用户充值
    }
  }
  ```

  ```python Python theme={null}
  from profy import AuthExpired, InsufficientBalance

  try:
      await profy.report_event("action", token=token)
  except AuthExpired:
      # 引导用户重新授权
  except InsufficientBalance:
      # 提示用户充值
  ```
</CodeGroup>

## SDK vs 自建对照

| 维度             | 用 SDK                  | 自建                            |
| -------------- | ---------------------- | ----------------------------- |
| Token Exchange | `profy.exchangeCode()` | 自己调 `POST /oauth/token`       |
| Token Refresh  | SDK 自动续期               | 自己检测过期 + 手动 refresh           |
| 重试 / 幂等        | SDK 内置 401 重试          | 自己实现                          |
| 存储             | contrib 或 自定义回调        | 完全自定义                         |
| 计费上报           | `profy.reportEvent()`  | 自己调 `POST /openapi/v1/events` |
| 错误分类           | 结构化异常                  | 自己解析 HTTP status              |

两种方式完全等价，SDK 只是封装了 HTTP 调用和自动刷新逻辑，不引入任何额外的平台绑定。

## 下一步

<CardGroup cols={2}>
  <Card title="快速开始" icon="rocket" href="/zh/documentation/integration-quickstart">
    5 分钟完成首次接入
  </Card>

  <Card title="API 参考" icon="code" href="/zh/api/post-token">
    查看完整的 OAuth 和 Events API 端点
  </Card>
</CardGroup>
