> ## 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.

# App 开发指南

> 构建一个集成袋袋身份与计费的外部应用

App 是通过 OAuth + Events API 接入袋袋身份与计费体系的外部应用。作为 App 开发者，你可以利用袋袋的用户系统和积分计费能力，快速实现产品商业化。

## 什么是袋袋 App

| 对比项      | 专家               | App                       |
| -------- | ---------------- | ------------------------- |
| **运行方式** | 在袋袋平台内运行         | 开发者自行部署                   |
| **用户交互** | 通过袋袋对话界面         | 自定义界面                     |
| **接入方式** | 配置人格/工具/技能       | OAuth + API 集成            |
| **计费**   | METERED（按 token） | METERED + PER\_USE（自定义事件） |
| **市场分发** | 袋袋专家市场           | 袋袋 App 市场                 |

## 开发流程

<Steps>
  <Step title="创建 App">
    在袋袋 Studio 中创建一个新的 App。你会获得：

    * **App UUID**（即 `client_id`）
    * **App Secret**（即 `client_secret`，仅展示一次）
    * **OAuth 配置入口**

    在 App 设置中配置 OAuth 相关信息：

    * 注册一个或多个 **Redirect URI**（回调地址）
    * 设置 App 的名称、描述、图标
  </Step>

  <Step title="实现 OAuth 流程">
    在你的应用中实现标准 OAuth 2.0 Authorization Code 流程：

    ```typescript theme={null}
    // 1. 引导用户授权
    const authUrl = new URL("https://api.profy.cn/oauth/authorize");
    authUrl.searchParams.set("client_id", APP_UUID);
    authUrl.searchParams.set("redirect_uri", "https://your-app.com/callback");
    authUrl.searchParams.set("response_type", "code");
    authUrl.searchParams.set("scope", "events:write");
    authUrl.searchParams.set("state", generateRandomState());
    // 重定向用户到 authUrl

    // 2. 在回调中换取 Token
    const tokenResp = await fetch("https://api.profy.cn/oauth/token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        grant_type: "authorization_code",
        code: callbackCode,
        redirect_uri: "https://your-app.com/callback",
        client_id: APP_UUID,
        client_secret: APP_SECRET,
      }),
    });
    const { access_token, refresh_token } = await tokenResp.json();
    ```

    详细流程参阅 [OAuth 集成](/zh/developers/oauth-integration)。
  </Step>

  <Step title="配置计费 Meter">
    如果你的 App 需要按次计费（PER\_USE），需要配置 Meter：

    1. 在 App 设置中添加 Meter
    2. 定义事件名称（如 `image_generation`）
    3. 设置每次事件的积分价格
    4. 保存配置

    Meter 配置可通过 `GET /v1/meters` 端点查询。
  </Step>

  <Step title="集成 API">
    使用 OAuth Token 调用袋袋 API：

    **调用 AI 模型**（METERED 计费）：

    ```typescript theme={null}
    const resp = await fetch("https://api.profy.cn/v1/chat/completions", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${access_token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        model: "deepseek-v3",
        messages: [{ role: "user", content: userMessage }],
      }),
    });
    ```

    **上报自定义事件**（PER\_USE 计费）：

    ```typescript theme={null}
    const resp = await fetch("https://api.profy.cn/v1/events", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${access_token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        event: "image_generation",
        idempotency_key: `${userId}_${taskId}`,
      }),
    });
    ```
  </Step>

  <Step title="提交审核">
    App 开发完成后，提交审核以发布到袋袋 App 市场：

    1. 在 Studio 中完善 App 信息（名称、描述、图标、截图）
    2. 点击「发布」按钮提交审核
    3. 审核团队会检查 App 的功能完整性和安全性
    4. 审核通过后 App 上线市场
  </Step>

  <Step title="发布到市场">
    审核通过后，你的 App 将出现在袋袋 App 市场中。用户可以发现、授权和使用你的 App。

    你的 App 通过使用获得的积分收入，会按照创作者激励体系进行分成。
  </Step>
</Steps>

## 计费模型

App 支持两种计费模型，可组合使用：

### METERED（按 token 计费）

适用于 AI 模型调用场景。用户使用 Chat Completions API 时，按实际 token 消耗从用户积分扣除。

```typescript theme={null}
// 积分自动根据 token 用量扣除
const resp = await fetch("https://api.profy.cn/v1/chat/completions", {
  method: "POST",
  headers: { Authorization: `Bearer ${access_token}`, ...},
  body: JSON.stringify({ model: "deepseek-v3", messages: [...] }),
});
```

### PER\_USE（按次计费）

适用于自定义功能场景。通过 Events API 上报事件，按 Meter 配置的固定积分价格扣除。

```typescript theme={null}
// 按 Meter 配置的固定价格扣除
const resp = await fetch("https://api.profy.cn/v1/events", {
  method: "POST",
  headers: { Authorization: `Bearer ${access_token}`, ...},
  body: JSON.stringify({
    event: "premium_feature",
    idempotency_key: uniqueKey,
  }),
});
```

## App 审核流程

App 发布采用审核双行机制（与专家审核一致）：

| 场景        | 行为                                 |
| --------- | ---------------------------------- |
| **首次发布**  | App 进入 `PENDING_REVIEW` 状态，审核通过后上线 |
| **更新版本**  | 旧版本保持在线，新版本进入审核                    |
| **审核拒绝**  | 可修改后重新提交                           |
| **创作者撤回** | 可撤回待审核的提交                          |

审核重点：

* App 功能是否完整可用
* OAuth 流程是否符合安全规范
* 用户体验是否合理
* 计费是否透明

## OAuth 与 App 状态

OAuth 授权流程在 App 创建后即可使用，**不受 `application.status` 限制**。`status` 仅控制 App 在市场中的可发现性：

| 状态               | OAuth | 市场可见  |
| ---------------- | ----- | ----- |
| `OFFLINE`        | ✅ 可用  | ❌ 不可见 |
| `PENDING_REVIEW` | ✅ 可用  | ❌ 不可见 |
| `ONLINE`         | ✅ 可用  | ✅ 可见  |

这意味着你可以在 App 上线前就完成 OAuth 集成和测试。

## 完整集成示例

以下是一个使用 Express + 袋袋 SDK 构建的完整 App 后端示例：

```typescript theme={null}
import express from "express";
import { Profy } from "@profy-ai/sdk";

const app = express();
app.use(express.json());

const APP_UUID = process.env.PROFY_APP_UUID!;
const APP_SECRET = process.env.PROFY_APP_SECRET!;

// 1. 发起 OAuth 授权
app.get("/auth/profy", (req, res) => {
  const state = crypto.randomUUID();
  // 保存 state 到 session
  const url = new URL("https://api.profy.cn/oauth/authorize");
  url.searchParams.set("client_id", APP_UUID);
  url.searchParams.set("redirect_uri", "https://your-app.com/callback");
  url.searchParams.set("response_type", "code");
  url.searchParams.set("scope", "events:write");
  url.searchParams.set("state", state);
  res.redirect(url.toString());
});

// 2. OAuth 回调
app.get("/callback", async (req, res) => {
  const { code, state } = req.query;
  // 验证 state

  const tokenResp = await fetch("https://api.profy.cn/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      grant_type: "authorization_code",
      code,
      redirect_uri: "https://your-app.com/callback",
      client_id: APP_UUID,
      client_secret: APP_SECRET,
    }),
  });
  const tokens = await tokenResp.json();
  // 保存 tokens 到数据库
  res.redirect("/dashboard");
});

// 3. 使用 OAuth Token 调用 API
app.post("/api/generate", async (req, res) => {
  const userToken = getUserToken(req); // 从数据库获取
  const client = new Profy({ apiKey: userToken.access_token });

  try {
    const result = await client.chat.completions.create({
      model: "deepseek-v3",
      messages: [{ role: "user", content: req.body.prompt }],
    });
    res.json({ text: result.text });
  } catch (e) {
    res.status(500).json({ error: "Generation failed" });
  }
});

app.listen(3000);
```

## 最佳实践

<AccordionGroup>
  <Accordion title="安全存储 Token">
    OAuth Token 应加密存储在服务端数据库中，不要暴露在前端。使用 httpOnly cookie 或 session 传递用户身份。
  </Accordion>

  <Accordion title="实现 Token 自动刷新">
    Access Token 仅有 1 小时有效期。在 API 调用返回 401 时，自动使用 Refresh Token 获取新 Token，对用户透明。
  </Accordion>

  <Accordion title="使用幂等键防止重复扣费">
    Events API 的幂等键应与业务操作唯一绑定（如 `{userId}_{taskId}`），确保网络重试不会导致重复扣费。
  </Accordion>

  <Accordion title="向用户展示积分价格">
    使用 Meters API 获取功能价格，在用户使用付费功能前展示将要扣除的积分数，提升信任感。
  </Accordion>

  <Accordion title="处理积分不足">
    当 Events API 返回扣费失败时，向用户展示充值引导，而不是静默失败。
  </Accordion>
</AccordionGroup>

## 下一步

<CardGroup cols={2}>
  <Card title="OAuth 集成" icon="shield" href="/zh/developers/oauth-integration">
    完整的 OAuth 2.0 集成流程
  </Card>

  <Card title="Events API" icon="bolt" href="/zh/developers/api/events">
    自定义计费事件上报
  </Card>
</CardGroup>
