﻿# IP 中台开发者安全接入指南

## 面向 LLM 与代码代理的读取入口

实现对接前按以下顺序读取，避免从可视化页面、过期模型快照或字段名称猜测接口：

1. `/llms.txt`：机器文档导航、真源顺序与安全边界。
2. `/llms-full.txt`：本指南、完整 OpenAPI 和全部示例源码合并后的单文件上下文。
3. `/openapi.json`：路径、方法、请求/响应字段与鉴权方式的唯一机器可读契约。
4. `/v1/platform/models/documentation`：从后台当前配置实时生成的模型 Markdown 索引。
5. `/v1/platform/models/{model_key}/documentation`：逐模型调用体及 cURL、Node.js、Python 示例。
6. `/v1/market/documentation`：**数据 API（API 广场）** 的 Markdown 总索引——抖音、小红书、B 站、快手、微博、微信、知乎、头条等第三方数据接口的全部清单、每个接口的方法与调用路径、积分单价，以及通用调用方法；逐接口文档在 `/v1/market/endpoints/{supplier_slug}/{endpoint_key}/documentation`（参数表 + 请求体结构 + cURL）。
7. `/v1/market/categories`、`/v1/market/endpoints`、`/v1/market/endpoints/{supplier_slug}/{endpoint_key}`：数据 API 的 JSON 索引，供程序消费。

字段或示例出现冲突时以 OpenAPI 为准；模型可用性、`model_key`、分类与介绍以实时模型 Markdown 为准。**数据 API 是原样透传的独立网关，OpenAPI 里只有一条通用路径 `/v1/data/{supplierSlug}/{upstreamPath}`，逐个接口的清单与参数以 `/v1/market/documentation` 为准**，不要因为某个数据接口没出现在 OpenAPI 的逐路径清单里就判定它不存在。不要自造接口，也不要保存永久模型或接口白名单。

## 1. Base URL

生产环境使用平台部署域名，所有业务接口位于 `/v1`：

```text
https://<平台域名>/v1
```

本文示例使用 `https://idp.example.com` 作为占位域名。生产请求必须使用 HTTPS，不能把 API Key、Client Secret、Access Token 放在 URL 查询参数中。

### 1.1 在开发者控制台配置应用

进入开发者控制台的“应用管理”，应用以表格展示；点击某一行的“详情”后，弹窗只展示该应用。点击右下角“修改”可在原位置编辑允许变更的字段。应用配置与接口字段的对应关系如下：

| 控制台字段 | API 字段 | 是否可修改 | 接入用途与规则 |
|---|---|---|---|
| 名称 | `name` | 是 | 授权页展示给用户的应用名称，建议与实际产品名称一致。 |
| 应用首页/介绍页 | `homepage_url` | 是 | AI 超市“打开应用”的公开跳转地址。仅允许无账号密码信息的完整 HTTP(S) 地址；建议生产使用 HTTPS。 |
| Client ID | `client_id` | 否 | 应用的公开唯一标识，用于 OAuth 授权请求、Token 兑换和按应用隔离用户 `sub`；可以出现在浏览器中。 |
| 创建时间 | `created_at` | 否 | Client 创建时间，ISO 8601 格式。 |
| 最低会员等级 | `min_member_level` | 是 | OAuth 授权时实时校验当前用户等级；低于门槛时拒绝授权。L0-L4 为平台统一数值空间。 |
| 授权范围 | `allowed_scopes` | 是 | 该 Client 最多允许申请的 Scope。实际授权请求的 `scope` 必须是其子集。 |
| 回调白名单 | `redirect_uris` | 是 | OAuth 授权码返回地址。必须登记完整 URI，并与授权请求中的 `redirect_uri` 精确匹配。 |
| 支付回跳白名单 | `billing_return_uris` | 是 | 会员购买或积分充值完成后的应用回跳地址；创建支付入口时的 `return_uri` 必须精确匹配。 |
| 事件 Webhook | 独立 Webhook 配置 | 是 | 每个 Client 一个 HTTPS 服务端接收地址，用于接收支付、会员和积分事件。不能填写前端页面地址。 |
| Secret | `client_secret_masked` | 仅轮换 | 列表和详情只返回固定掩码 `****************` 表示已经签发，不返回明文、哈希或尾号；明文仅在首次领取或轮换响应中出现一次。 |

授权范围在控制台使用中文显示，协议请求仍使用右侧固定值：

| 控制台名称 | Scope | 何时申请 |
|---|---|---|
| OIDC 登录身份 | `openid` | 需要标准 OIDC 登录和 `id_token` 时。 |
| 基础身份 | `identity` | 需要调用 `/v1/oauth/userinfo` 获取应用专属 `sub` 和脱敏资料时。 |
| 会员等级 | `membership.read` | 需要读取当前授权用户实时会员等级或套餐详情时。 |
| 积分余额 | `points.read` | 需要读取当前授权用户钱包摘要时。 |
| 积分消费 | `points.spend` | 需要以当前授权用户积分调用模型时。 |
| 会员与充值入口 | `billing.initiate` | 需要创建当前用户的一次性会员购买或积分充值入口时。 |

首次创建应用时直接提交 Sandbox 配置。`request_id` 必须是本次创建意图唯一的 UUID；请求超时、断连或响应丢失后的重试必须复用同一 UUID 和完全相同的配置：

```http
POST /v1/developer/sandboxes
Cookie: <平台首方会话>
Content-Type: application/json

{
  "request_id": "6eec6a0e-3af5-4f32-9a25-ff84a50468e9",
  "configuration": {
    "name": "示例应用",
    "homepage_url": "https://app.example.com",
    "redirect_uris": ["https://app.example.com/oauth/callback"],
    "billing_return_uris": ["https://app.example.com/payment-complete"],
    "allowed_scopes": ["openid", "identity", "membership.read", "points.read", "points.spend", "billing.initiate"],
    "min_member_level": 1
  }
}
```

创建后在应用详情读取当前有效测试凭据，并成功完成至少一次 Sandbox OAuth 兑换。当前凭据在 Sandbox 为 Active 时可重复读取；暂停会隐藏并禁用但保留加密副本，恢复成功后重新显示，重生成会使原 Secret、测试密码和 Token 失效。凭据不得写入浏览器持久存储、正式库或日志。状态返回 `basic_integration_completed=true` 后，才能调用 `POST /v1/developer/sandboxes/{sandboxClientId}/production-applications` 提交 `{request_id}` 申请正式上线。该操作只创建 `pending` Client，仍需 Admin 审核；任何测试凭据、用户、Webhook、支付和日志都不会迁移。直接调用 `POST /v1/developer/clients` 固定返回 `409 sandbox_required`。

修改应用使用 `PATCH /v1/developer/clients/{clientId}`，请求体至少包含一个可修改字段。名称和最低会员等级直接生效；`homepage_url` 可传完整地址，显式传 `null` 表示清空。已启用应用若修改首页地址、授权范围、OAuth 回调白名单或支付回跳白名单，将立即从 AI 超市下架，撤销该 Client 的 Refresh Token、清除旧 Client Secret，并转为 `pending` 重新审核。审核通过后应用自动重新上架，开发者必须领取并部署新 Secret，用户也需要重新授权。Sandbox 来源的正式申请被拒后，不得用 PATCH 改写旧 rejected Client；应先在原 Sandbox 配置中调整，再重新申请正式上线，系统会创建新的 pending Client 并保留旧拒审记录。

应用状态含义：`pending` 为等待审核，`active` 为可授权和调用，`disabled` 为平台停用，`rejected` 为审核拒绝。`active` 应用可以领取审核签发的一次性 Client Secret；`active` 或 `disabled` 应用可以主动轮换。首次领取使用 `GET /v1/developer/clients/{clientId}/secret`；主动轮换使用 `POST /v1/developer/clients/{clientId}/rotate-secret`。轮换成功后旧 Secret 立即失效，应先准备安全的部署切换流程，收到新明文后存入服务端秘密管理系统，禁止放入浏览器、移动端包、源码或日志。

AI 超市读取公开接口 `GET /v1/platform/apps?page=1&page_size=20`。平台只列出当前 `active` 的应用；`pending`、`rejected`、`disabled` 不会出现。公开项固定为 `client_id`、`name`、`homepage_url`、`min_member_level`、`created_at`，不会泄露开发者资料、回调地址、支付回跳地址、授权范围、Secret 或会员数据。历史应用尚未配置 `homepage_url` 时仍可展示，但入口保持不可点击；补充地址并重新审核后即可跳转。

上述 Client 管理和 Webhook 配置端点依赖平台首方 Session，只供登录后的开发者控制台调用。第三方应用运行时不应调用这些端点；运行时只使用已配置的 `client_id`、服务端安全保存的 Secret，以及对应的 OAuth/API Key 业务端点。

上线前逐项确认：授权请求只申请已登记 Scope；开发、测试、生产回调地址分别完整登记；支付回跳不携带可信归因参数；应用后端校验 OAuth `state` 和 `nonce`；Token 兑换只在服务端完成；Webhook 使用原始请求体验签并按 `event_id` 去重；不得尝试拉取、搜索或关联其他会员。

## Sandbox 联调

新应用默认在控制台创建 Sandbox，随后在应用详情持续显示 `sbx_` Client ID、当前有效 Client Secret 和虚构测试账号，直至凭据失效或重新生成。测试用户通过 `POST /v1/auth/login` 传入 `sandbox_client_id` 登录；服务端只写入独立的 `idp_sbx_session`，不会读取正式 Cookie 或正式用户。

Sandbox 继续使用 OAuth Authorization Code + PKCE，但 code、refresh token、issuer、签名 key、JWKS、Cookie、Redis 和数据库均独立。Sandbox 不签发 Developer API Key，Sandbox OAuth Access Token 也不具备模型调用能力；请求 `/v1/models/{modelId}/invoke` 固定返回 `403 production_environment_required`，不会访问供应商或扣减积分。模型调用与应用环境切换相互独立：开发者服务端使用账号级 Developer API Key 调用 `POST /v1/developer/models/{modelKey}/invoke`，平台直接扣减 Key 所属开发者的正式积分。历史 `idp_sbx_` Key 固定返回 `401 invalid_api_key`，不会查库或回退正式环境。测试凭据、Webhook、日志、测试数据和支付状态绝不迁移到正式 Client。

Sandbox 复用现有 OAuth、用户资料、会员、钱包、充值和内容投诉契约，但所有记录只写入 Sandbox 数据库。充值响应的 `payment_url` 使用 `mock-pay:` 测试 URL，其中包含该订单的测试回调字段与单次签名；订单号固定为 `sbx_pay_` 前缀，回调入口命中该前缀后只查询 Sandbox，绝不调用微信、支付宝或正式商户。Sandbox Webhook 仍执行本文的 HTTPS、DNS 绑定、HMAC、300 秒时间窗、`event_id` 去重和密钥轮换规则，并限制为每 Client 每分钟 20 个测试事件、最多 100 个待投递事件。

## AI 开发助手：Sandbox → 正式环境切换契约

本节是给 LLM、代码代理和 SDK 生成器的实现约束。OAuth 登录、会员、钱包和应用内支付必须共用一套业务代码，通过选择环境配置切换 Sandbox 与正式环境。**“无缝切换”不表示只替换两个字符串**：支付主链路的 HTTP 契约不变，但身份校验和 Webhook 仍必须使用对应环境的材料；模型调用使用独立的账号级 Developer API Key。

### AI 必须建立的环境配置

以下名称是开发者应用自己的配置字段，不是 IdP API 请求字段。除 `client_id` 外，Secret、Token 和 Webhook 密钥只能保存在服务端秘密管理系统。

| 应用配置字段 | 必填条件 | Sandbox 值 | 正式值 | 使用位置 |
|---|---|---|---|---|
| `environment` | 必填 | `sandbox` | `production` | 选择完整配置对象；不得作为业务 API 参数发送。 |
| `api_origin` | 必填 | 平台提供的 API Origin | 平台提供的 API Origin | 拼接 `/v1/oauth/token`、`/v1/user/*` 和 JWKS 路径；不得从 Token `iss` 猜测。 |
| `authorization_ui_origin` | 必填 | 平台提供的 Web Origin | 平台提供的 Web Origin | 浏览器跳转 `/oauth/authorize`；第三方不得直接调用平台首方登录接口。 |
| `client_id` | 必填 | `sbx_` 前缀的 Sandbox Client ID | 审核通过的正式 Client ID | OAuth authorize、Token 请求体和 `id_token.aud` 校验。 |
| `client_secret` | 必填 | Sandbox Client Secret | 正式 Client Secret | 仅服务端调用 `/v1/oauth/token` 时用于 HTTP Basic；它不是 Developer API Key。 |
| `expected_issuer` | 必填 | Sandbox issuer | 正式 issuer | 校验 Token 的 `iss`；两套 issuer 必须不同。 |
| `redirect_uri` | 必填 | Sandbox Client 已登记的精确 URI | 正式 Client 已登记的精确 URI | OAuth authorize 与 Token 兑换必须使用同一个值。 |
| `billing_return_uri` | 使用支付时必填 | Sandbox Client 已登记的精确 URI | 正式 Client 已登记的精确 URI | Billing Entry 请求的 `return_uri`。 |
| `webhook_secret` | 接收 Webhook 时必填 | Sandbox Webhook Secret | 正式 Webhook Secret | 对原始请求体验签；两套 Secret 不得混用。 |

每个环境应保存一个完整、不可拆分的配置对象，避免把 Sandbox `client_id` 与正式 Secret、issuer 或 Webhook Secret 交叉组合：

模型调用不属于这个环境配置对象。开发者后端应单独保存账号级 Developer API Key，并始终使用实时模型文档返回的正式 `model_key`；请求体不得携带 `client_id` 或 `sandbox_client_id`。

```ts
type IdpEnvironmentProfile = {
  environment: "sandbox" | "production";
  api_origin: string;
  authorization_ui_origin: string;
  client_id: string;
  client_secret: string;
  expected_issuer: string;
  redirect_uri: string;
  billing_return_uri?: string;
  webhook_secret?: string;
  model_key?: string;
};
```

### AI 必须复用的函数与字段

AI 应实现下列职责，不得为 Sandbox 复制一套带不同字段名的业务函数：

```ts
type OAuthToken = {
  access_token: string;
  token_type: "Bearer";
  expires_in: number;
  refresh_token: string;
  scope: string;
  id_token?: string;
};

type BillingIntent = "membership" | "recharge";
type CreateBillingEntryRequest = { intent: BillingIntent; return_uri: string };
type BillingEntryCreateResult = { entry_id: string; billing_url: string; expires_at: string };
type BillingEntryStatus = {
  id: string;
  intent: BillingIntent;
  status: "pending" | "consumed" | "expired";
  expires_at: string;
  payment_order: null | {
    status: string;
    paid_at: string | null;
    refunded_at: string | null;
  };
};

interface IdpIntegration {
  createAuthorizationRequest(profile: IdpEnvironmentProfile): URL;
  exchangeAuthorizationCode(profile: IdpEnvironmentProfile, code: string, codeVerifier: string): Promise<OAuthToken>;
  createBillingEntry(accessToken: string, input: CreateBillingEntryRequest): Promise<BillingEntryCreateResult>;
  getBillingEntryStatus(accessToken: string, entryId: string): Promise<BillingEntryStatus>;
  verifyIdToken(profile: IdpEnvironmentProfile, idToken: string, expectedNonce: string): Promise<void>;
  verifyWebhook(profile: IdpEnvironmentProfile, rawBody: Buffer, headers: Headers): Promise<void>;
}
```

`verifyIdToken` 必须从 `<api_origin>/v1/.well-known/jwks.json` 按 `kid` 选择 RS256 公钥，并同时校验 `iss === expected_issuer`、`aud === client_id`、`nonce === expectedNonce`、过期时间和 `token_use === "id"`。JWKS 可能同时包含 Sandbox 与正式公钥，因此只校验签名或 `kid` 不足以证明环境。Access Token 只作为 Bearer 凭证调用 IdP API；应用读取用户身份应调用 `userinfo`，不要把未验证的 JWT payload 当作用户资料。

| 功能 | Sandbox 与正式共用的方法和路径 | 必须发送 | 禁止发送或依赖 |
|---|---|---|---|
| 发起授权 | `GET <authorization_ui_origin>/oauth/authorize` | `response_type=code`、`client_id`、`redirect_uri`、`scope`、`code_challenge`、`code_challenge_method=S256`、`state`，含 `openid` 时带 `nonce` | 不直接调用 `/v1/auth/login`，不收集测试账号或正式账号密码。 |
| 兑换 Token | `POST <api_origin>/v1/oauth/token` | HTTP Basic `client_id:client_secret`；请求体为 `grant_type`、`code`、`code_verifier`、`redirect_uri`、`client_id` | 不把 Secret 放入 URL、浏览器或日志。 |
| 创建支付入口 | `POST <api_origin>/v1/user/billing-entries` | Bearer Access Token；请求体只能是 `intent`、`return_uri` | 不发送 `client_id`、`sandbox_client_id`、`user_id`、手机号、金额、套餐、归因或返佣字段。 |
| 查询支付入口 | `GET <api_origin>/v1/user/billing-entries/{entry_id}` | 创建入口的同一用户当前 Access Token | 不信任浏览器回跳的 `status`，不使用其他用户 Token 查询。 |
| 读取到账结果 | `GET /v1/user/wallet` 或 `GET /v1/user/membership/details` | 分别需要 `points.read` 或 `membership.read` | 不把 Token 内的余额或会员等级快照当成最终结果。 |
| 验证 Webhook | 开发者配置的同一 HTTPS 接收端点 | 原始正文、`X-IdP-Event-Id`、`X-IdP-Timestamp`、`X-IdP-Signature` | 不先解析 JSON 再验签；不跨环境复用 Secret。 |

`billing_url` 是短期不透明地址。应用只能把浏览器跳转到该值，不得解析协议、追加 `sandbox_client_id`、缓存为固定地址或根据 `mock-pay:` 编写业务分支。Sandbox 页面中的模拟成功/失败控件和 `/mock-result` 路径属于平台首方内部能力，开发者服务端和开发者 AI **不得调用**。

支付状态判断必须遵循以下顺序：

1. 浏览器回跳参数仅用于提示用户，不能作为支付证据。
2. 使用创建入口的同一用户 Access Token 调用 `getBillingEntryStatus`。
3. `status="consumed"` 只表示入口已经绑定订单，不表示已经付款。
4. 只有查询结果中的 `payment_order.status="paid"` 才可视为订单支付成功；随后读取钱包或会员详情确认最终业务状态。
5. Webhook 必须验签、检查负载中的 `environment` 和 `client_id` 与当前配置一致，并按 `event_id` 去重；最终状态仍以查询接口为准。

| 保持同一套代码 | 必须按环境切换或重新取得 |
|---|---|
| OAuth PKCE 流程、Scope、HTTP 方法和路径 | `client_id`、`client_secret`、`expected_issuer` |
| Billing Entry 字段、浏览器跳转和状态查询 | Webhook Secret、测试/正式用户和全部 Token |
| Webhook HMAC 算法、时间窗和 `event_id` 去重 | 账号级 Developer API Key 与应用 OAuth 凭据 |
| `issuer + sub` 的本地用户关联方式 | 正式 Client 状态、回调白名单、支付回跳白名单和真实支付渠道 |

如果“Key”指 Developer API Key，则不能按上述方式切换：Sandbox 明确不签发 Developer API Key，且 API Key 不能用于用户登录、会员、钱包或支付。应用内支付只能使用带 `billing.initiate` 的 OAuth Access Token；测试用户、Token、订单、Webhook 和支付结果不会迁移到正式 Client。

AI 完成实现后至少验证：Sandbox OAuth 登录与 Token 刷新、两种 Billing Entry、Mock 成功和失败、回跳后查询、钱包/会员最终状态、Webhook 验签与重复事件。切换正式配置前还要验证正式 Client 为 `active`、两个白名单精确匹配、issuer/audience/nonce 校验已启用，并确保应用代码中不存在 `/mock-result` 或对 `mock-pay:` 的判断。

## 2. 使用开发者 API Key 调用模型

适用于开发者自己的服务端直接调用，不代表任何终端用户身份。

1. 登录开发者中心直接创建账号级密钥，不需要先创建 Sandbox、OAuth Client 或正式应用。明文只展示一次。
2. 保存明文后确认交付，Key 才会从 `delivery_pending` 激活。
3. 控制台通过 `GET /v1/developer/models` 读取账号级模型广场、`eligible` 和实际 `points_per_call`；公开文档仍可从[模型调用文档索引](/docs/models)或 `/v1/platform/models/documentation` 读取。
4. 使用唯一 `X-API-Key` 请求头调用模型。

创建字段固定如下，开发者 AI 不得添加应用选择字段：

```http
POST /v1/developer/api-keys
Cookie: <平台首方会话>
Content-Type: application/json

{
  "name": "生产模型服务",
  "create_request_id": "<UUIDv4，同一创建意图重试复用>",
  "expires_in_days": 90
}
```

`expires_in_days` 只能取 `30|90|180|365`。首次 201 的 `api_key` 只出现一次；同一创建意图重放返回 200 且不再返回明文。保存后调用 `POST /v1/developer/api-keys/{id}/confirm-delivery` 并发送严格空对象 `{}` 激活。

文档索引由公开只读接口 `GET /v1/platform/models` 动态生成。该接口只返回当前可用模型的文档字段，不返回供应商、成本、应用定价、会员门槛或任何凭证。字段不要混用：

| 字段 | 用途 |
|---|---|
| `model_key` | 平台稳定调用标识，写入 `/v1/developer/models/{modelKey}/invoke`；同一模型改展示标题或底层模型名时不变。 |
| `display_name` | 文档索引展示标题，仅用于界面。 |
| `model_name` | 平台模型名称兼容字段，不作为调用 URL。 |
| `model_description` | 后台维护的模型能力介绍，可为空。 |
| `model_categories` | 多能力分类数组，可同时包含对话、生图、视频、语音、嵌入、重排序等。 |
| `model_category` | `model_categories` 首项；多能力模型应读取完整数组。 |
| `points_per_call` | 当前开发者账号每次调用实际扣除的积分；目前等于 `default_points_per_call`。 |
| `eligible` | 当前账号等级是否满足模型 `min_developer_level`；`false` 时不要发起调用。 |

当前默认价格位于[模型价格](/docs/models/pricing)，机器可读接口为 `GET /v1/platform/models/pricing`。响应中的 `points_to_cny_rate` 表示 1 积分对应的人民币分值，`default_points_per_call` 表示平台默认单次调用积分价；账号级调用以 `GET /v1/developer/models` 返回的 `points_per_call` 为准。公开价格接口不会返回供应商成本或供应商身份。

三档图片模型 `ultimate-image-2`、`ultimate-image-2.5-flare`、`ultimate-image-2.5-sunburst` 使用独立的图片输入上限：完整 `input` 最多 32MB，支持 1～16 张参考图和可选蒙版，参考图与蒙版合计最多 20MB。平台按最长边选择档位：1K（最长边不超过 1,536）、2K（1,537～2,048）、4K（2,049～3,840），最终扣减为档位单价乘以 `numberResults`。4K 档耗时更长、时限单独放宽，客户端超时与失败处理见 §6.2。当前默认档位单价：`ultimate-image-2` 为 25 / 40 / 60 积分/张，两个 2.5 模型为 30 / 48 / 72 积分/张，以 `GET /v1/developer/models` 和模型价格页为准。`outputType=URL` 返回的 `/v1/generated-images/{token}` 是临时地址，按请求 `ttl` 到期后返回 404，应及时转存。平台返回的图片已去除上游元数据，并按 GB 45438-2025 在 XMP 中写入 AI 生成合成内容隐式标识（`TC260:AIGC`）；转存或二次处理时请保留该标识。

默认按次价格的统一输入包络为：`input` JSON 序列化后最多 32,000 个 UTF-8 字节；超限返回 HTTP 413 `content_too_large`，且不会预扣积分或调用模型。

**整个 HTTP 请求体的上限是另一层**：模型类路由（`/v1/developer/models/{key}/invoke`、`/v1/models/{key}/invoke`、`/v1/chat/completions`、`/v1/embeddings`、`/v1/images/*`）接受最多 **32MB** 的 JSON 请求体，足够容纳 base64 参考图；其他管理类 `/v1/*` 接口的请求体上限为 100KB。超过时返回 HTTP 413 `payload_too_large`（`{"error":"payload_too_large"}`，与上面的 `content_too_large` 不同：前者是请求体整体超限、请求根本没进业务层，后者是 `input` 包络超限）。这个 413 由平台自身返回，不是模型供应商的错误。文本/代码模型的后台硬上限为 8,000 个输出单位；调用方可以在 `input` 中提供更小的正整数 `max_tokens` 或 `max_completion_tokens`，平台取调用方上限与后台硬上限中的最小值。非推理模型只需短回答时可以主动设置较小上限，避免等待不必要的完整生成。

**推理模型的输出上限（重要）**：带 `reasoning_content` 的推理模型（当前 `glm-5.3`、`deepseek-v4-pro`、`deepseek-v4-flash`、`minimax-m3` 等）会**先用 `max_tokens` 做推理**，剩余预算才用于正文。预算不足时模型返回 HTTP 200、`finish_reason: "length"`、`content` 为空字符串且 `reasoning_content` 非空，`usage.completion_tokens_details.reasoning_tokens` 接近全部预算——这是合法完成体，**照常按 Token 计费**，平台不会重试、不会补预算。对这类模型建议 `max_tokens ≥ 4096`，最稳妥是**不传**（直接走后台 8,000 上限）；收到 `finish_reason: "length"` 且 `content` 为空时，应提高上限或简化提示后用新的幂等键重试，而不是当作平台故障。

```http
POST /v1/developer/models/{modelKey}/invoke
X-API-Key: <YOUR_API_KEY>
Content-Type: application/json

{
  "idempotency_key": "order-20260819-001",
  "input": { "messages": [{ "role": "user", "content": "请回复 OK" }], "max_tokens": 256 }
}
```

模型结果仍采用同步 JSON 返回。成功响应包含 `Server-Timing`（`provider`、`pre_moderation`、`reserve`、`materialize`、`post_moderation`、`settlement`、`platform`、`total`）和 `X-Request-Id` 响应头，可用于区分模型生成、平台审查、积分预扣、图片落地和结算耗时；这些诊断头不改变响应 JSON。

- 创建 Key 和调用模型都不接受 `client_id` 或 `sandbox_client_id`；Key 只归属当前开发者账号。
- API Key 调用使用 API Key 所属开发者本人的积分钱包。
- 余额不足固定返回 HTTP 402 `insufficient_balance`；不调用模型，也不使用信用额度先用后结。
- API Key 只能用于 `/v1/developer/models/*`，不能用于用户登录、会员查询或 OAuth 接口。
- API Key 只保存在服务端秘密管理系统，不得进入源码、浏览器、日志或错误上报。

#### OpenAI 兼容入口：`/v1/chat/completions`、`/v1/embeddings`、`/v1/images/generations`、`/v1/images/edits`、`/v1/audio/transcriptions`

任何 OpenAI SDK 或兼容客户端（Cursor、Claude Code、LangChain、OpenAI Python/Node SDK 等）只需把 `base_url` 指到 `https://<平台域名>/v1`、`api_key` 填 Developer API Key、`model` 填平台 `model_key`，即可直接调用，不必改写成上面的平台契约：

```http
POST /v1/chat/completions
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json

{
  "model": "doubao-seed-2.0-lite",
  "messages": [{ "role": "user", "content": "请回复 OK" }],
  "max_tokens": 256,
  "stream": true
}
```

- 鉴权同时接受 `Authorization: Bearer <API Key>` 与 `X-API-Key`，两者只能带其一；扣费、审查、限流、幂等与 `/v1/developer/models/{modelKey}/invoke` 完全相同，只是响应直接返回 OpenAI 对象，错误返回 OpenAI 的 `{"error":{"message","type","code"}}` 形状并保持相同 HTTP 状态码（402 余额不足、422 内容拦截、429 限流、502 上游失败）。
- 每次请求默认是一次新调用；需要幂等重试时带标准 `Idempotency-Key` 请求头（≤128 字符），语义与 `idempotency_key` 相同。
- **`stream: true` 是真流式**：模型的每个 SSE 块经平台增量安全审查后逐块转发，最后以 `data: [DONE]` 结束；平台审查一旦命中，后续块不再发送，改为发出一个 `{"error":{"code":"content_blocked"}}` 事件并退款。流式过程中无法再改变 HTTP 状态码，所以块已经发出之后的错误一律以事件形式出现。模型忽略 `stream` 直接返回完整 JSON 时，平台退回单块 SSE 形式。
- **`tools` / function calling 原样透传**：模型返回 `tool_calls`（`content` 为 `null`）时平台按有效结果处理，流式时 `tool_calls` 的增量同样逐块转发。
- 支持 OpenAI Chat 协议的文本/代码模型；图片、Embeddings 与语音识别模型请分别使用下面的同形入口，误用会返回 400 `unsupported_model_format`。
- **客户端超时**：文本 ≥ 120 秒、图片 ≥ 300 秒（推荐 600 秒；4K 档 ≥ 920 秒，推荐 960 秒）、长语音识别 ≥ 180 秒（推荐 300 秒），SDK 默认值请显式覆盖，详见 §6.1、§6.2。

```http
POST /v1/embeddings
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json

{ "model": "doubao-embedding-vision", "input": ["用户希望找回项目中的支付回调设计"], "encoding_format": "float" }
```

响应即模型的 OpenAI Embeddings 对象（`data[].embedding`、`usage`）。

```http
POST /v1/images/generations
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json

{ "model": "ultimate-image-2", "prompt": "一只在窗台晒太阳的橘猫", "n": 1, "size": "1024x1024", "quality": "high", "response_format": "url" }
```

`size` 为 `WIDTHxHEIGHT`（宽高均为 16 的倍数，缺省或 `auto` 即 `1024x1024`），`quality` 接受 `auto|low|medium|high|standard|hd`，`response_format` 为 `url`（返回平台临时地址 `/v1/generated-images/{token}`，按默认 TTL 到期）或 `b64_json`，`output_format` 为 `png|jpeg|webp`，`output_compression` 为 `0-100`（OpenAI 语义，越大越压，仅 jpeg/webp 生效），`background` 为 `auto|opaque|transparent`；计费按平台图片档位（见上文）乘以 `n`。响应为 `{ "created", "data": [{ "url" | "b64_json", "revised_prompt"? }] }`。

**`/v1/images/generations` 只做文生图，和 OpenAI 一样不接受 `image`/`mask` 字段**：把参考图放进这个请求会得到 `400 invalid_request`（`param` 为你实际传入的字段名——`image`、`images`、`image_url`、`reference_images`、`mask` 等以 `image` / `reference` / `mask` 开头的键都会命中，`message` 明确指向 `/v1/images/edits`），不会扣费。图生图请改用下面的 `/v1/images/edits`。

**图生图 / 蒙版局部重绘使用 OpenAI 标准的 `/v1/images/edits`**，OpenAI 官方 SDK 的 `images.edit()` 直接可用（multipart/form-data，`image` 或 `image[]` 传 1–16 张参考图文件，`mask` 传可选蒙版文件，白色为重绘区域）：

```python
from openai import OpenAI
client = OpenAI(base_url="https://<平台域名>/v1", api_key="<YOUR_API_KEY>", timeout=600)
result = client.images.edit(
    model="ultimate-image-2",
    image=[open("cat.png", "rb")],
    prompt="给这只猫戴上一顶红色圣诞帽，其余画面保持不变",
    size="1024x1024",
    quality="high",
)
```

平台额外接受 JSON 形式，`image`/`mask` 可以直接给 **HTTPS URL、Data URI 或 Base64** 字符串（单个或数组），不必先下载再上传：

```http
POST /v1/images/edits
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json

{
  "model": "ultimate-image-2",
  "prompt": "给这只狗戴上一顶红色圣诞帽，保持其余画面不变",
  "image": ["https://example.com/dog.jpg"],
  "size": "1024x1024",
  "quality": "high",
  "output_format": "png"
}
```

规则：参考图 PNG/JPEG/WEBP（Data URI 声明接受 `image/png`、`image/jpeg`、`image/jpg`、`image/webp`，真实类型以字节为准；不接受 base64url），每张 ≤ 10MB、合计 ≤ 20MB；URL 必须是公网 HTTPS 地址（平台会跟随最多 3 次跳转，内网地址一律拒绝）；`mask` 必须与 `image` 一起提供；`background: transparent` 当前不支持；其余参数与 `/v1/images/generations` 相同，计费按图片档位乘以 `n`。模型返回的图片格式可能与 `output_format` 不完全一致（以实际字节为准）。同样的结构化输入也可以通过 `/v1/developer/models/ultimate-image-2/invoke` 的 `inputs.referenceImages` / `inputs.maskImage` 提交。

**语音识别使用 OpenAI 标准的 `/v1/audio/transcriptions`**（腾讯云语音识别；OpenAI 官方 SDK 的 `audio.transcriptions.create()` 直接可用，multipart/form-data 上传 `file`）：

```python
from openai import OpenAI
client = OpenAI(base_url="https://<平台域名>/v1", api_key="<YOUR_API_KEY>", timeout=300)
with open("meeting.mp3", "rb") as audio:
    result = client.audio.transcriptions.create(model="tencent-asr-large", file=audio, response_format="verbose_json")
print(result.text)
```

选型：**≤ 60 秒**的语音消息 / 语音输入 / 单句跟读用 `tencent-asr-sentence`（短语音识别，同步秒回、最便宜）；**1–20 分钟**的会议 / 访谈 / 课程 / 播客录音用 `tencent-asr-large`（大模型长语音识别 2.0，方言与嘈杂环境更准，按 20 分钟上限计费）；不确定长度选后者（短音频给它也能识别，只是多花积分；超过 60 秒给短语音识别会在提交前被拒绝）；> 20 分钟请在客户端切段。同价同上限的语种 / 场景变体：短语音 `tencent-asr-sentence`（中英粤混说）、`tencent-asr-sentence-zh`（普通话专用）、`tencent-asr-sentence-en`（英文专用）；长语音 `tencent-asr-large`（中英 + 31 种方言）、`tencent-asr-large-meeting`（多人会议对话）。实时可用列表以 `GET /v1/platform/models`「语音」分类为准。`response_format` 为 `json`（默认，`{ "text" }`）、`text`（纯文本）或 `verbose_json`（含 `duration` 秒数与 `segments` 时间轴）；`language` / `prompt` / `temperature` 接受但忽略。音频格式按文件字节识别（wav、mp3、m4a、aac、ogg-opus、amr），不看扩展名；平台在提交前解析实际时长并硬校验，超时长 / 超大小 / 无法识别的文件返回 `400 invalid_request`（`message` 以 `audio_too_long:`、`audio_too_large:`、`unsupported_audio_format:`、`invalid_audio_format:` 开头）或 `413 audio_too_large`（multipart 文件 > 3.7MB），**都不扣积分**；识别文本命中内容审查返回 `422 content_blocked` 并退款。同样的音频也可以通过 `/v1/developer/models/{modelKey}/invoke` 的 `input.audio`（Base64 或 Data URI）提交，原生入口恒返回 `verbose_json` 结构。

`doubao-embedding-vision` 使用 OpenAI 兼容的 Embeddings 协议。平台统一调用地址不变，待向量化文本放在外层 `input` 对象的内层 `input` 数组中：

```http
POST /v1/developer/models/doubao-embedding-vision/invoke
X-API-Key: <YOUR_API_KEY>
Content-Type: application/json

{
  "idempotency_key": "embedding-20260822-001",
  "input": {
    "input": ["用户希望找回项目中的支付回调设计"],
    "encoding_format": "float"
  }
}
```

成功响应的 `output.data[].embedding` 为稠密向量，`output.data[].index` 对应原输入数组顺序。知识库内容和查询文本必须使用同一模型与相同向量维度；入库时保存原文 ID 与向量的对应关系，查询时使用余弦相似度或向量数据库取得 Top K。网络重试必须复用原幂等键和原请求体。不要把聊天模型的 `messages` 请求体直接用于该模型；完整示例见文档中心的对应模型页面。

### 开发者双账户钱包与线下结算

登录开发者中心后，`GET /v1/developer/wallet` 返回权威双账户摘要。`points_account` 是 API Key 调用消费的本人积分；`commission_account` 是按人民币分核算的返佣。页面绝不能把两者相加，积分汇率只用于显示模型人民币参考价，不代表积分可兑换现金。响应禁止缓存。

返佣只支持选择整张已确认月结单提交线下结算申请，不提供自动提现、自动分账或支付渠道付款：

```http
POST /v1/developer/offline-settlement-requests
Cookie: <平台首方会话>
Content-Type: application/json

{
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "settlement_ids": ["123", "124"]
}
```

同一业务意图重试必须复用规范小写 UUID。首次创建返回 201；同 UUID、同账单集合重放返回 200，不同集合返回 409 `request_id_conflict`。全部账单必须属于当前 active Developer、状态为 `confirmed`、未被占用且合计达到最低门槛。`submitted` 可由本人通过 `POST /v1/developer/offline-settlement-requests/{requestNo}/cancel` 和严格空对象 `{}` 取消；取消或 Admin 驳回会原子释放账单。`paid` 只表示运营已在平台外付款并登记证据。

使用 `GET /v1/developer/offline-settlement-requests?page=1&page_size=20` 读取本人历史。页面重新获得焦点、主动刷新或状态动作完成后重新读取钱包与申请，无需固定轮询。`sla_due_at` 是申请时固化的平台处理目标，不是法定到账承诺。Sandbox 写操作固定拒绝为 403 `production_environment_required`。

## 3. 接入平台用户登录

第三方应用必须使用 OAuth2 Authorization Code + PKCE，不得收集、代理或保存用户的平台密码。

接入顺序：

1. 在开发者中心创建 Sandbox 应用，登记精确回调地址、允许 Scope 和最低会员等级，并完成一次 OAuth 兑换。
2. 提交正式申请并等待管理员审核；通过后由 Client 所有者领取一次性 Client Secret。
3. 生成 `state`、`nonce`、`code_verifier` 和 S256 `code_challenge`。
4. 把用户跳转到 IdP Web 的 `GET /oauth/authorize`，由平台完成登录、展示当前隐私政策、记录同意和授权。第三方应用不得自行收集平台隐私同意，也不得调用同意记录接口。
5. 回调后由应用服务端调用 `POST /v1/oauth/token` 兑换 Token。
6. 使用 Access Token 调用用户资料和会员接口；开发者自己的模型调用使用账号级 Developer API Key。

推荐 Scope：

```text
openid identity membership.read points.read points.spend billing.initiate
```

Token 端点要求 HTTP Basic `client_id:client_secret`，JSON 请求体仍携带同一 `client_id`。`id_token` 只用于验证登录结果，不能作为 Bearer Token 调用 API。

### 平台首方登录接口边界

`POST /v1/auth/login` 和 `POST /v1/auth/privacy-consents` 只用于平台自己的同源前端。它们通过 HttpOnly Cookie 建立或使用首方 Session，并受 Origin / Fetch Metadata 校验；第三方应用不要直接调用，应跳转 `/oauth/authorize`。该 Web 页面记录同意后会内部调用 `/v1/oauth/authorize` 完成授权码签发。

平台首方前端可用 `GET /v1/auth/session` 检查登录状态，并用 `GET /v1/auth/account-summary` 读取本人会员和钱包摘要。

## 4. 读取用户与会员

```http
GET /v1/oauth/userinfo
Authorization: Bearer <ACCESS_TOKEN>
```

需要 `identity` Scope。应用必须使用 `issuer + sub` 关联自己的本地账号，不得用手机号、昵称或邮箱作为唯一键。

```http
GET /v1/user/membership
Authorization: Bearer <ACCESS_TOKEN>
```

需要 `membership.read` Scope，仅返回实时 `member_level`。

```http
GET /v1/user/membership/details
Authorization: Bearer <ACCESS_TOKEN>
```

需要 `membership.read` Scope，返回当前等级、套餐权益、当前购买周期和预约周期。会员结果来自当前数据库状态，不使用 Access Token 内的等级快照；响应禁止缓存。

第三方应用只能读取当前授权用户的会员状态。会员升级、续费和降级由平台会员中心处理，当前不开放修改会员等级的开发者 API。

### 主体隔离与钱包

`sub` 是按 `client_id` 隔离的随机主体，同一用户在不同应用得到不同的值。应用只能使用 `issuer + sub` 关联自己的账号；它不是平台用户 ID。平台不提供会员列表、导出、按手机号/邮箱/用户名/用户 ID 查询或跨应用同步能力。

```http
GET /v1/user/wallet
Authorization: Bearer <ACCESS_TOKEN>
```

该接口需要 `points.read`，只返回当前令牌用户的 `sub`、`member_level`、`wallet.balance`、`wallet.frozen`、`wallet.available` 和更新时间。`balance` 已是可用积分，因此 `available` 始终等于 `balance`，`frozen` 只展示冻结额，接入方不得再次相减；响应禁止缓存。

## 5. 应用支付入口与归因

先在开发者中心把精确回跳地址配置到 Client 的 `billing_return_uris`，再由当前用户令牌创建一次性入口。应用不得传递或伪造 `user_id`、手机号、Client 归属或返佣参数。

### 5.1 充值积分

创建 `recharge` 入口，平台页面负责展示充值套餐、自定义金额和支付渠道：

```http
POST /v1/user/billing-entries
Authorization: Bearer <ACCESS_TOKEN>
Content-Type: application/json

{
  "intent": "recharge",
  "return_uri": "https://app.example.com/payment-complete"
}
```

支付完成并核验入口状态后，调用 `GET /v1/user/wallet` 读取实际到账余额。详细说明见 `/docs/billing/recharge`。

### 5.2 升级会员

创建 `membership` 入口，平台页面负责展示可购买会员方案并处理升级或续费：

```http
POST /v1/user/billing-entries
Authorization: Bearer <ACCESS_TOKEN>
Content-Type: application/json

{
  "intent": "membership",
  "return_uri": "https://app.example.com/payment-complete"
}
```

支付完成并核验入口状态后，调用 `GET /v1/user/membership/details` 读取实时会员等级和周期。详细说明见 `/docs/billing/membership`。

### 5.3 通用回跳与归因流程

需要 `billing.initiate`。响应给出 `entry_id`、短时单次 `billing_url` 与过期时间。把浏览器跳到该 URL，用户在平台首方页面完成支付。

平台支付成功后仅回跳已白名单的地址，并带 `billing_entry` 和 `status=paid`。应用用同一当前用户的 Access Token 查询单个入口：

```http
GET /v1/user/billing-entries/{entry_id}
Authorization: Bearer <ACCESS_TOKEN>
```

不支持会员或支付入口的列表、筛选和跨用户查询。平台将归因写入会员实收、充值实收、确认积分消费和退款分项，第一阶段仅供线下结算核对，不实现自动提现、分账或返佣。

```text
应用后端 --当前用户 Access Token--> POST /v1/user/billing-entries
应用前端 --浏览器跳转 billing_url--> IdP 登录/会员购买/积分充值页
IdP --仅回跳白名单 return_uri--> 应用前端（billing_entry + status 提示）
应用后端 --同一用户 Access Token--> GET /v1/user/billing-entries/{entry_id}
IdP --HMAC Webhook--> 应用服务端（通知；最终状态仍以上述查询为准）
```

### 5.4 应用一次性购买（AI 超市）

用户付一次钱、永久使用你的应用；调用模型产生的 Token 消耗不包含在价格里，仍按公开模型价格另计。购买、收款、退款全部由平台完成，你的应用不需要接支付。

**设置价格**：在开发者中心的应用详情里提交一次性价格（接口 `POST /v1/developer/clients/{client_id}/commercial-offer`），平台审核通过后生效，页面会给出同分类的建议价。同一时间只能有一条待审报价；涨价只影响之后的新购买，已购用户不受影响。没有设价的应用在商店显示「报价配置中」，用户免费使用。

**公开详情**：商店与任何人都可以读取价格和说明，不需要登录：

```http
GET /v1/platform/apps/{client_id}
```

返回应用名称、分类、`commercial_offer`（一次性价格，单位人民币分）、`token_billing_note`（Token 另计说明）、`token_pricing_path`（平台模型价格页路径）和 `refund_policy`（公开退款说明）。不返回任何用户的购买状态。

**平台开启购买强制后，你的应用会看到这些行为**（只对已设价的应用生效；应用所有者本人始终放行）：

1. **授权时**：未购买的用户跳到 `/oauth/authorize` 后不会回跳你的回调地址，而是停留在平台授权页并看到购买入口（地址带 `purchase_required=1`）；付款成功后继续授权，再回跳你的回调地址。你按原来的 OAuth 流程接入即可。
2. **刷新令牌时**：用户退款或被撤销权益后，`POST /v1/oauth/token`（`grant_type=refresh_token`）返回 `400 invalid_grant`，`error_description` 为「需要先购买该应用」。按常规处理：清掉本地登录态，引导用户重新走授权，用户会在授权页看到购买入口。访问令牌有效期 1 小时，所以退款后最迟 1 小时内失去访问。
3. **服务端判断是否已购**：用当前用户的访问令牌查询，不需要额外 Scope，只能查询本应用：

```http
GET /v1/user/app-entitlement
Authorization: Bearer <ACCESS_TOKEN>
```

```json
{
  "sub": "usr_...",
  "access": "granted",
  "entitled": true,
  "acquired_at": "2026-09-29T08:00:00.000Z",
  "expires_at": null
}
```

- `access`：`granted` 表示可以使用（已购、你本人、应用未设价，或平台尚未开启购买强制）；`purchase_required` 表示需要先在平台购买。以它决定是否放行。
- `entitled`：用户是否有有效的购买 / 平台发放的测试或补偿权益，适合在你的页面上展示「已购买」。
- `expires_at`：平台发放的测试权益可能有到期时间；`null` 表示永久。
- 响应禁止缓存。Sandbox 没有购买，恒为 `granted`。

**退款**：用户向平台客服申请，平台审核后按原支付渠道原路全额退回；退款成功后该用户的权益立即撤销，访问按上面第 2 条失效。你的应用不需要处理退款。

**应用停止服务**：应用被停用（不是 active）期间，已购用户保留权益但不能授权，个人中心「已购应用」显示「应用暂停服务」和处置进度。请在开发者中心提交恢复计划（`/v1/developer/clients/{client_id}/service-termination`）；停服满 30 天或你确认永久下线后，由平台决定是否全额原路退款给已购用户。

**收入**：购买收入按平台公布的分成比例在平台、你和推广者之间核算（有无推广者两套比例），按月线下结算；平台不做自动分账或提现。

## 6. 调用模型与积分扣减

终端用户场景使用带 `points.spend` Scope 的 Access Token：

```http
POST /v1/models/{modelId}/invoke
Authorization: Bearer <ACCESS_TOKEN>
Content-Type: application/json

{
  "idempotency_key": "user-request-20260819-001",
  "input": { "messages": [{ "role": "user", "content": "概括这段内容" }] }
}
```

模型网关会自动完成积分结算，调用方不需要手工扣分：

1. 账号级 API Key 按 `GET /v1/developer/models` 的 `points_per_call` 从开发者本人钱包预扣；余额不足返回 `402 insufficient_balance`。
2. 校验开发者等级、内容安全和限流后调用模型；账号级调用不读取或创建信用额度。
3. 成功时确认消费；明确失败时释放并退回积分。
4. 模型结果未知时释放终端用户积分并返回 `provider_unknown`，平台进入人工核对。
5. 后置内容安全拦截时自动退款。

不要为模型调用自行编排 `/v1/points/reserve`、`/confirm`、`/release`。这些是通用积分原语，模型网关已在内部完成全部步骤。

每次新业务调用生成唯一 `idempotency_key`；同一次业务重试必须复用原键和完全相同的请求体。

### 6.0 OAuth 终端用户调用：只扣终端用户积分

2026-10-04 起预付扣费下线。终端用户通过你的应用调用模型时，**只扣终端用户的积分余额**，开发者不承担供应商成本——既不从你的积分钱包预付，也不占用信用额度、不做月底对账赊账。规则：

- **谁付**：每次 `POST /v1/models/{modelId}/invoke` 只按应用售价预扣终端用户积分（预扣 → 成功确认；明确失败、后置内容拦截或结果未知 `provider_unknown` 时原路退还）。开发者钱包和信用额度一分不动；开发者余额为 0 或从未充值，也不影响终端用户调用。
- **售价**：默认按模型默认积分（`default_points_per_call`）扣费。你可以通过 `PUT /v1/developer/clients/{clientId}/model-pricing` 为应用设置自己的单价，但**不能低于模型默认积分**，低于时返回 **HTTP 400** `price_below_model_default`。平台日后若上调默认积分，已设置但低于新默认值的单价按默认积分扣费。
- **余额不足**：只有终端用户积分不足这一种情况，返回 `402 insufficient_balance`，应引导终端用户充值。`402 developer_prepaid_insufficient` 已停用，不会再返回；旧客户端保留这条映射也无妨。
- **历史流水**：下线前产生的预付流水（钱包流水说明形如「应用「X」终端用户调用成本预付 / 核销 / 退还」）保留可查。

账号级 Developer API Key 调用不受影响：它本来就是直接扣开发者本人积分。

### 6.1 超时、耗时与客户端超时设置

模型调用是**同步阻塞**的：HTTP 请求会一直挂着，直到模型生成完成、平台完成安全审查与结算才返回。平台自身的超时如下，客户端超时必须**大于**对应的平台模型超时，否则会在结果出来之前主动断线：

| 调用类型 | 平台对模型的超时 | 实测耗时（2026-09） | **建议客户端总超时** |
|---|---|---|---|
| 文本/代码模型，非流式（`/v1/developer/models/{key}/invoke`、`/v1/chat/completions`） | 60 秒 | 3–15 秒；思考型模型可达 30–60 秒 | **≥ 120 秒** |
| 文本/代码模型，流式（`stream: true`） | 60 秒（自模型首块起按块刷新） | 首块 1–30 秒，之后逐块到达 | 连接/读取超时 **≥ 120 秒**，整体不设或 ≥ 600 秒 |
| Embeddings（`/v1/embeddings`） | 60 秒 | 1–5 秒 | **≥ 90 秒** |
| 图片生成 1K / 2K（`ultimate-image-2` 与两个 2.5 模型：文生图 `/v1/images/generations`；图生图优先 `/v1/images/edits`，带 `inputs.referenceImages` 的 `/invoke` 为备选） | 600 秒 | **30–150 秒/张** | **≥ 300 秒，推荐 600 秒** |
| 图片生成 4K（最长边 2,049～3,840，同上三个模型） | 生成 600 秒；整单（生成 + 取回 + 清洗 + 结算）**885 秒** | 生成 35–330 秒/张；取回视上游线路，见 §6.2 | **≥ 920 秒，推荐 960 秒** |
| 数据 API（API 广场 `/v1/data/*`） | 30 秒 | 0.5–5 秒 | **≥ 45 秒** |

平台入口层（Nginx）的读写超时：图片相关路径（`/v1/images/*`、`/v1/developer/models/*`、`/v1/models/*`）为 920 秒，文本、Embeddings、语音路径为 620 秒，都不会先于平台时限掐断连接；其他 `/v1/*` 管理类接口为 75 秒。

**为什么必须设够**：客户端一旦超时断线，平台会立即中止对模型的请求。此时模型可能已经生成完成并产生计费，但平台没拿到结果——这一单会落成 `provider_unknown` 进入人工核对，结果作废（终端用户的预扣积分会退回，不扣费）。图片调用尤其如此：60 秒的默认超时几乎必然在结果返回前断线。

各 SDK 的设置方法：

- OpenAI Python SDK：`OpenAI(base_url=..., api_key=..., timeout=600)`；Node SDK：`new OpenAI({ baseURL, apiKey, timeout: 600_000 })`。
- `requests`：`requests.post(url, json=body, timeout=(10, 600))`（连接 10 秒、读取 600 秒）。
- `fetch`（Node/浏览器）：`fetch(url, { signal: AbortSignal.timeout(600_000) })`。
- `curl`：`--max-time 600`。

超时后**不要更换幂等键重试**：先按 `request_id` 查询或等待平台核对结果，重复发起会产生第二笔扣费。

### 6.2 4K 图片：耗时、时限与失败处理

三档图片模型的 4K 档（最长边 2,049～3,840）比 1K / 2K 慢得多，接入前请先看完本节。

**为什么 4K 慢、而且慢得不稳定**

- 生成本身更慢：2026-10 实测 4K 单张生成 35–330 秒（1K / 2K 经平台约 30 秒）。
- 文件大：4K PNG 通常 11–19MB。上游对 4K 大图有时不直接返回图片数据，而只给一个下载链接；平台必须先把图片取回，去除上游元数据、写入 AI 生成标识（见 §2 三档图片模型说明），再转存成平台临时地址交给你——**平台从不把上游链接直接交给调用方**。
- 取回速度取决于上游图片服务器到平台的线路，目前这条线路偶尔非常慢，大图可能要十几分钟才能取回。平台正在推动上游改善。
- 上游对 4K 有时不按请求的 `outputFormat` / `output_format` 返回（例如请求 JPEG / WEBP 仍返回 PNG），不能靠换格式来缩小文件；请以返回图片的实际文件头为准。

**平台的时限**

| 阶段 | 1K / 2K | 4K |
|---|---|---|
| 等待上游生成 | 最长 600 秒 | 最长 600 秒 |
| 取回图片的截止时间（从开始调用算起，之后还要清洗、转存、结算） | 585 秒 | **885 秒** |
| 入口层（Nginx）读超时 | 920 秒 | 920 秒 |
| **建议客户端总超时** | ≥ 300 秒，推荐 600 秒 | **≥ 920 秒，推荐 960 秒** |

**取回赶不上时平台怎么处理**

上游返回了文件大小时，平台在开始取回十几秒内按实测下载速度估算剩余时间，预计赶不上截止时间就立即停止，不会让你一直等到超时；上游没返回文件大小时只能等到截止时间（4K 约 885 秒）才判定。两个入口的返回形状不同：

`/v1/developer/models/{key}/invoke`、`/v1/models/{key}/invoke`（HTTP 200）：

```json
{
  "status": "provider_unknown",
  "request_id": "…",
  "error_message": "图片已生成，但平台未能在交付时限内取回（4K 大图较常见），本次不扣积分；可换新幂等键稍后重试，或改用 2K 尺寸"
}
```

OpenAI 兼容入口 `/v1/images/generations`、`/v1/images/edits`（HTTP 502，OpenAI SDK 会抛出 `APIStatusError` / `InternalServerError`）：

```json
{
  "error": {
    "message": "图片已生成，但平台未能在交付时限内取回（4K 大图较常见），本次不扣积分；可换新幂等键稍后重试，或改用 2K 尺寸",
    "type": "server_error",
    "param": null,
    "code": "provider_unknown",
    "request_id": "…"
  }
}
```

- 1K / 2K 极少触发，提示为「图片已生成，但平台未能在交付时限内取回，本次不扣积分；可换新幂等键稍后重试」。
- **只有提示里写明「未能在交付时限内取回……本次不扣积分」的 `provider_unknown`** 表示平台确定没有交付图片、未扣积分，可以**换新的幂等键**重试；其他 `provider_unknown` 仍按 §6.1 处理。
- 重试会重新生成并按正常价格计费，不是续传上一次的结果。
- 平台取回图片期间如果你的客户端先断开，平台会立即停止取回，这一单同样落成 `provider_unknown`、不扣积分——所以客户端超时必须按上面的时限表设够，否则 4K 会白白生成。

**接入建议**

1. 4K 调用把客户端总超时设到 960 秒（OpenAI SDK `timeout=960`、Node SDK `timeout: 960_000`、`requests.post(..., timeout=(10, 960))`、`fetch(..., { signal: AbortSignal.timeout(960_000) })`、`curl --max-time 960`）。
2. 不要在用户页面的同步请求里直接调用 4K：放进后台任务或队列，先给用户一个「生成中」状态，完成后再通知或轮询展示。
3. 不需要 4K 时优先用 2K（最长边 2,048），速度和稳定性都明显更好；需要大图时也可以先出 2K 预览，用户确认后再出 4K。
4. 收到上面的「交付时限」提示时，提示用户稍后再试或改用 2K，而不是立即自动重试多次。

## 7. 签名 Webhook

每个 Client 可以在开发者中心配置一个 HTTPS Webhook：

```http
PUT /v1/developer/clients/{client_id}/webhook
Cookie: <平台首方会话>
Content-Type: application/json

{
  "url": "https://app.example.com/idp/webhook",
  "enabled": true,
  "rotate_secret": false
}
```

新建或主动轮换时返回一次 `webhook_secret`，只能保存在应用服务端。事件包括 `billing.payment.paid`、`billing.payment.refunded`、`membership.changed`、`points.confirmed`、`points.released`。

平台发送 `X-IdP-Event-Id`、`X-IdP-Timestamp`、`X-IdP-Signature: v1=<hex>`。应用按 `HMAC-SHA256(webhook_secret, "<timestamp>.<raw request body>")` 验签，校验时间窗口，并以 `event_id` 持久化去重。2xx 视为成功；其他响应或超时最多重试 8 次。

```js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyIdpWebhook({ rawBody, timestamp, signature, secret }) {
  if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  if (!/^v1=[0-9a-f]{64}$/.test(signature)) return false;
  const expected = Buffer.from(createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex"), "hex");
  const received = Buffer.from(signature.slice(3), "hex");
  return timingSafeEqual(expected, received);
}
```

必须对 HTTP 原始正文验签，成功后再解析 JSON，并以 `event_id` 唯一约束去重。

负载仅含当前 `client_id`、应用专属 `sub`、业务金额/积分/状态和事件元数据，不含内部用户 ID、手机号、邮箱、Token、密码、Secret 或完整钱包流水。

## 8. Token、Secret 与重试安全

- Client Secret 和 API Key 明文都只展示一次，只保存在服务端秘密管理系统。
- `state`、`nonce` 和 `code_verifier` 保存在服务端会话，完成或失败后立即删除。
- Refresh Token 每次使用都会轮转；调用方必须原子替换旧值。
- `409 in_progress`：稍后复用同一幂等键和相同请求体。
- `409 idempotency_key_conflict`：同一键对应了不同请求体，属于调用方错误。
- `200 status=provider_unknown`：保存 `request_id`，不要更换幂等键自动重试。
- `401 invalid_api_key`：停止重试并检查密钥是否未确认交付、已过期或已吊销。
- `402 insufficient_balance`：开发者本人余额不足（API Key 调用）或终端用户售价积分不足（OAuth 调用）；充值后复用原幂等键和完全相同的请求体重试。
- `402 developer_prepaid_insufficient`：已停用（2026-10-04 预付扣费下线，见 §6.0），不会再返回。
- `422 content_blocked`：根据 `details.stage` 判断前置拦截或后置退款。
- `429 rate_limited`：遵循 `Retry-After`，使用有上限的指数退避。
- `503 model_providers_disabled`：模型总开关关闭，不会预扣积分或调用模型。

## 9. 金额与货币单位

- 平台全站统一使用人民币（`CNY` / `¥`）元。
- API 和账本中的 `cny_cents`、`*_cents` 使用人民币分，并以十进制字符串传输；例如 `"1999"` 表示 `¥19.99`。
- 接入方不得使用 JavaScript `number` 做金额运算。

## 10. 应用托管：部署与数据库连接

开发者在「应用管理 → 创建应用」中选择自行部署、平台托管或稍后决定。自行部署表示使用自己的服务器和域名，平台不托管。选择平台托管时在同一个表单填写套餐、模板、应用标识和用途，并确认费用；点击「创建应用并提交托管审核」一次提交应用及待审托管申请，无需再提交另一份申请。创建失败保留材料，同内容重试复用请求标识，任一校验失败都不会留下孤立应用或托管申请。选择和提交不扣费、不部署、不轮换密钥。稍后决定可在「应用详情 → 部署与托管」继续配置。应用名称与接入配置复用当前应用，用途草稿保存后可以继续编辑；原「应用托管」页面仍用于全部实例和历史实例管理，历史实例须由本人显式关联，不按名称自动绑定。

「应用管理」的同一张列表同时展示平台应用和本人托管申请：已关联申请显示在所属应用下，未关联档案的历史申请单独展示，并可直接打开托管详情。可筛选全部、平台应用或托管申请；审核结论与运行状态分别显示。刷新列表不会创建申请、扣费或自动绑定；选择某个实例进入详情后，刷新页面保持该实例。

1. 创建时已选择托管的应用直接等待审核；旧应用或稍后决定的应用可在当前详情中补交托管申请：确认套餐费用，选择模板（`static_site` / `nodejs` / `python`），补充不可更改的应用标识与用途说明。
   套餐选择区展示当前开放套餐的积分月费、CPU、内存、磁盘、月流量、空闲策略和支持的模板。三种模板均有说明；不受所选套餐支持的模板会标为不可选。更换套餐或模板后需重新确认费用，提交申请本身不会扣费。价格与模板支持范围以当前后台配置为准。
2. 等待 Admin 托管审核；通过后按既有规则预扣积分，等待上传。**预扣不等于正式扣款，也不代表正式应用审批通过。**
3. 审批通过后配置环境变量、上传 `.zip`（不超过 20 MB）并部署。平台构建镜像、启动容器和检查健康；首次部署成功后按原规则确认扣款并开始计费周期。网络异常先刷新实例及部署记录，核对结果再操作，不能假定关页或服务重启后任务必定继续。
4. 后续可重新上传代码。构建失败时保留旧版本，切换时可能短暂中断，启动失败会尝试恢复上一个版本。日志入口提供构建摘要，资源入口提供已有采样。主动下线仍须单独确认代码和数据库清理，改部署意向不会销毁资源。

创建应用仍先创建 Sandbox。测试联调、正式应用审批和托管部署分别展示；选择托管不跳过成功 OAuth 兑换等正式申请门槛。尚无正式 Client 的应用不能通过配置导入入口领取或轮换正式凭据，测试凭据不自动导入托管。此入口不提供独立测试环境托管，测试与正式数据库仍分别管理。

整合入口默认全部账号可用；管理员可临时关闭或限制范围，此时仍可使用原创建/托管入口，已有实例和数据库管理继续保留。用途草稿只更新控制面材料，不改写已提交的审核快照。

详情分别显示容器运行与公网配置状态。访问地址由节点域名配置生成，实例运行成功不等于公网域名已验证可达；反代失败显示“公网访问未就绪”，反代成功显示“公网访问与 HTTPS 尚未验证”，历史无证据也显示未验证。出站白名单的实际隔离与放行效果依赖节点网络配置，不能把界面白名单记录当作已经验证的网络隔离；第三方 API 出站需求仍交管理员处理。

### 数据库连接：两条独立路径

当前三种模板的实例部署流程均创建独立的 MySQL 数据库和专属账号，权限锁定在自己的库里。`nodejs` / `python` 业务代码可读取注入的连接串；`static_site` 只提供静态文件，不执行服务端数据库连接，也不能把数据库凭据放入浏览器代码。已有数据库的静态实例同样可在详情管理；入口以实际资源与实例状态为准，选择部署方式或打开数据库页不会自动建库或恢复已清理的数据。有两条独立的连接路径，指向同一个专属库与账号：

| 场景 | 怎么连 | 谁在用 |
|---|---|---|
| 应用代码本身要读写数据库 | 读环境变量 `DATABASE_URL`（标准 `mysql://` 连接串，部署时自动注入，零接线） | 容器内的服务端业务代码（`nodejs`/`python`）；静态页面不能直接连接 MySQL，不能把数据库凭据放进前端代码 |
| 让自己的 AI 工具管理/查询数据库 | 调用 `POST /v1/developer/hosting-databases/{instanceId}/sql-query`（`X-API-Key` 鉴权，跟模型调用共用同一把账号级 Key） | 本机运行的 AI 工具（Claude Code、Cursor 等），不在平台容器网络里，连不上内部数据库地址 |

应用代码读取注入的连接串（Node.js 示例）：

```js
import mysql from "mysql2/promise";

const connection = await mysql.createConnection(process.env.DATABASE_URL);
const [rows] = await connection.execute("SELECT 1");
```

让本机 AI 工具执行 SQL：

```http
POST /v1/developer/hosting-databases/{instanceId}/sql-query
X-API-Key: <YOUR_API_KEY>
Content-Type: application/json

{ "sql": "SELECT * FROM your_table LIMIT 20" }
```

数据库不对公网开放，没法用 MySQL 客户端从本机直连；`sql-query` 接口需要账号级 API Key，创建入口是否开放依据平台实际开关；未开放时可在应用详情的「数据库」页签使用原会话鉴权 SQL 控制台。单次查询超过 10 秒会被中止；结果最多返回 200 行。`SELECT`/`SHOW` 类语句返回 `columns`/`rows`；`INSERT`/`UPDATE`/`DELETE`/`CREATE` 等语句返回 `affected_rows`。这把 Key 目前唯一的 scope 是 `models.invoke`，用来批准访问自己名下的托管数据库不构成越权（Key 本身就代表开发者本人），只是命名尚未细分。

### 10.1 空闲挂起与快速唤醒：让你的应用启动得更快

托管实例长时间没有访问会被平台自动挂起（`docker stop`，释放内存；阈值按套餐，标准档 30 分钟）。挂起后**不需要任何人手动操作**：

- 访客用浏览器打开你的域名，会先看到平台的「应用正在启动」页面（带一个像素小游戏），平台同时在后台唤醒容器；应用一响应，页面自动进入原来的地址（含路径和查询参数）。
- 前端已经打开、此时才发起的 API 请求（`fetch`/`XMLHttpRequest`、`POST` 等非页面导航）会收到 **HTTP 503 JSON**，带 `Retry-After` 头和 `X-Idp-Wake-Page: 1` 头，平台**不会替你重放**这些请求：

```json
{ "error": "instance_unavailable", "state": "waking", "message": "应用正在启动，请稍候", "retry_after_ms": 1000 }
```

  单页应用建议对 503 + `X-Idp-Wake-Page` 做一次简单的重试（按 `retry_after_ms` 等待后重发，幂等请求才重试）。其他 `state` 取值：`failed`（本轮启动失败，30 秒后可再试）、`blocked`（应用被系统停用，需管理员处置）、`suspended`（欠费暂停）、`preparing`（尚未部署完成）。

- 唤醒期间平台会**临时把容器 CPU 配额放宽**（默认放宽到 1 vCPU，最长 60 秒），健康检查通过即收回套餐配额。所以启动耗时几乎只取决于你的应用自己启动得有多快：标准档实测从 40 秒降到约 8 秒。

要让唤醒更快，按下面的约定写启动逻辑（`nodejs` 模板为例，`python` 同理）：

| 建议 | 原因 |
|---|---|
| `package.json` 的 `scripts.start` 写成朴素的 `node <文件>`（如 `node dist/server.js`），不要 `tsx`/`ts-node`/`nodemon`，也不要 `npm run build && node …` | 平台看到这种形式会直接用 `node` 启动，跳过 `npm` 命令行本身的启动开销；运行时编译 TypeScript 在小配额下要多花好几秒 |
| **先 `listen`，再做别的**：HTTP 服务器一起来就监听 `process.env.PORT`（固定 8080）并让 `GET /` 返回 2xx，迁移、预热缓存、连外部服务这些放在监听之后 | 平台的健康检查只看 `GET /` 最终是否返回 2xx（会跟随重定向；4xx/5xx 不算通过）；先监听可以把"访客能进来"的时刻提前，唤醒超时也不会误判失败 |
| 数据库迁移和 seed 放在监听**之后顺序执行**，不要和服务器启动并行；seed 做成幂等且已完成时直接跳过 | 这些进程和你的服务器抢同一份 CPU 配额，并行会让监听时刻明显推后（实测多出十几秒） |
| Prisma 的 `prisma generate` 放在 `scripts.build`，不要放在启动时；seed 脚本预先编译成 JS，不要启动时加载 `tsx` 跑 `.ts` | 构建阶段做一次的事，不要每次唤醒都重做 |
| 启动路径上只加载必需的模块，重依赖（大型 SDK、图表库、Puppeteer 之类）按需懒加载 | 启动时的模块加载是 CPU 密集操作，配额越小放大得越明显 |
| 应用要能被 `SIGTERM` 干净关闭（10 秒内退出） | 挂起是 `docker stop`，宽限期 10 秒，超时会被强杀 |

一个符合约定的最小启动脚本：

```js
// scripts/start.mjs —— package.json: { "scripts": { "start": "node scripts/start.mjs" } }
import { createServer } from "node:http";
import { app } from "../dist/app.js";           // 预先构建好的 JS，不在启动时编译

const server = createServer(app);
server.listen(Number(process.env.PORT ?? 8080), "0.0.0.0", async () => {
  // 先监听、先通过健康检查，再做耗时初始化
  await runMigrationsIfNeeded();                 // 幂等：已是最新则立刻返回
  await seedIfEmpty();                            // 幂等：已有数据则跳过
});

process.on("SIGTERM", () => server.close(() => process.exit(0)));
```

如果你的应用确实不适合被挂起（长连接、定时任务、后台队列），联系管理员把实例换到常驻档套餐（不挂起，按套餐计费）；开发者中心目前没有自助关闭挂起的开关。

### 10.2 上传前检查清单：常见部署失败原因

部署失败时，实例详情页会显示失败原因。下面按模板列出平台固定的构建规则和最常见的失败原因（"失败时看到"一列是失败原因里会出现的原文），上传前逐条核对即可避开大部分重试。首次部署失败后，实例状态会变成"初始化失败"：改好后在实例详情页点「解决问题后点此重试部署」（重新预扣积分，失败时预扣已自动退回），再重新上传 `.zip`。

**所有模板通用**

| 检查项 | 要求 | 失败时看到 |
|---|---|---|
| 压缩包 | 只收 `.zip`，不超过 20 MB | `代码包不能超过 20 MB，请排除 node_modules、.git 等目录后重新打包` |
| 文件放在 zip 最外层 | 打开 zip 第一层就能看到 `package.json`（或 `requirements.txt`、`index.html`），不能再套一层项目文件夹 | `…要求源码根目录包含 …` |
| 不打包本地产物 | 排除 `node_modules/`、`.git/`、`.venv/`、`__pycache__/` 等；`node_modules` 平台会按锁文件重新安装 | 超过 20 MB |
| 不打包密钥 | `.env` 不要放进 zip，改在实例详情页「环境变量」里配置（勾选「密钥」会加密存储）；最多 30 个、单值 4 KB；**改完要重新部署才生效** | — |
| 平台注入的变量 | `PORT`（固定 `8080`）和 `DATABASE_URL` 由平台自动注入，不能自定义 | `PORT 由平台自动注入，不可自定义` |

macOS 在访达里对项目文件夹点「压缩」会多套一层文件夹，这是"根目录找不到 `package.json`"最常见的原因。正确做法是进入项目目录，打包目录里的内容：

```bash
cd my-app
zip -r ../my-app.zip . -x "node_modules/*" ".git/*" ".venv/*" ".env"
unzip -l ../my-app.zip | head   # 第一层应直接是 package.json，而不是 my-app/package.json
```

**`nodejs` 模板**：在 `node:20-alpine` 上依次执行 `npm ci --ignore-scripts` → `npm run build --if-present`，启动用 `package.json` 的 `scripts.start`。

| 检查项 | 要求 | 失败时看到 |
|---|---|---|
| `package.json` | 在 zip 最外层 | `Node.js 模板要求源码根目录包含 package.json` |
| `package-lock.json` | 在 zip 最外层，并且和 `package.json` 一致（`npm ci` 强制要求）。没有第三方依赖也要有；用 pnpm/yarn 的项目执行 `npm install --package-lock-only` 生成一份；改过依赖后记得重新生成 | `Node.js 模板要求源码根目录包含 package-lock.json（npm ci 强制要求）`；不一致时构建日志出现 `npm ci can only install packages when your package.json and package-lock.json … are in sync` |
| 锁文件的下载地址 | 构建时只能访问 npm 官方源（`registry.npmjs.org`）。如果锁文件是在国内镜像源（如 `registry.npmmirror.com`）下生成的，里面记录的下载地址会被拦截：删掉 `package-lock.json`，执行 `npm install --package-lock-only --registry=https://registry.npmjs.org/` 重新生成；也不要在 `.npmrc` 里改 `registry` | 构建日志里依赖下载报 `403` 或超时 |
| 依赖的安装脚本不会执行 | `--ignore-scripts` 会跳过所有依赖的 `postinstall`：需要在安装时编译原生模块或下载二进制的包（如 `bcrypt`、`sqlite3`、`puppeteer`）装上也用不了，换纯 JS 实现（如 `bcryptjs`）；用 Prisma 的项目把 `prisma generate` 写进 `scripts.build` | 运行时报 `Cannot find module …`，或提示 Prisma Client 未生成 |
| 构建时长 | 单次构建最长 10 分钟 | 构建超时 |
| `scripts.start` | 必须有，建议写成 `node dist/server.js` 这种朴素形式（见 §10.1） | 容器起不来，健康检查失败 |

**`python` 模板**：在 `python:3.12-slim` 上执行 `pip install --no-deps -r requirements.txt`，启动用 `Procfile` 里 `web:` 行的命令。

| 检查项 | 要求 | 失败时看到 |
|---|---|---|
| `requirements.txt` | 在 zip 最外层 | `Python 模板要求源码根目录包含 requirements.txt` |
| 依赖要写全 | `--no-deps` 不会自动安装"依赖的依赖"：在干净的虚拟环境里装好并跑通后，用 `pip freeze > requirements.txt` 导出完整清单（含版本号）；不要在文件里写 `-i`/`--index-url` 指向国内镜像，构建时只能访问 PyPI 官方源 | 启动时报 `ModuleNotFoundError` |
| `Procfile` | 在 zip 最外层，包含一行 `web: <启动命令>`，命令监听 `0.0.0.0:$PORT`，例如 `web: gunicorn -b 0.0.0.0:$PORT app:app` 或 `web: uvicorn main:app --host 0.0.0.0 --port $PORT` | ``Python 模板要求源码根目录包含 Procfile，且有一行 `web: <启动命令>` ``、``Procfile 缺少 `web:` 行`` |

**`static_site` 模板**：平台不执行任何构建命令，把 zip 内容原样放进 Nginx 站点根目录。上传的必须是**构建产物**（例如 `npm run build` 之后 `dist/` 目录里的内容），`index.html` 在 zip 最外层；否则访问 `/` 得到 403/404，健康检查失败。

**运行时约束（`nodejs` / `python`）**

| 约束 | 说明 | 违反时 |
|---|---|---|
| 文件系统只读 | 应用目录不可写，只有 `/tmp` 可写，而且是内存盘，重启或空闲挂起后就清空。上传的文件、SQLite、日志文件都不要写本地磁盘，业务数据存进 `DATABASE_URL` 指向的 MySQL | 写文件报 `EROFS: read-only file system` |
| 健康检查 | 容器启动后 60 秒内，在 `0.0.0.0:8080` 上对 `GET /` 最终返回 2xx（详见 §10.1） | 失败原因以"容器启动后健康检查未通过"开头，后面附带容器最近的日志（其中你配置的密钥值和数据库连接串会显示为 `***`） |
| 出站网络 | 默认全部禁止，只自动放行应用自己的子域名；要调第三方 API 需联系管理员放行 | 外部请求超时或被拒 |
| 内存 | 按套餐限额，超出会被系统强制结束进程 | 容器反复退出 |

**平台侧故障，不需要改代码**：失败原因里出现 `Can't reach database server`（托管数据库不可达）或 `托管数据库管理连接未配置` 这类字样时，说明构建已经完成，是平台基础设施的问题。把实例标识和失败原因发给管理员，平台修复后按上面的方式点重试、重新上传同一个 zip 即可。

### 10.3 从应用中心一键导入接入配置与推荐变量名

应用接平台登录、支付或 Webhook 时，需要的 Client ID、回调地址等值平台里本来就有。在实例详情页点「从我的应用导入配置」，选一个自己的应用，勾选要导入的项，平台会直接写进这个实例的环境变量，不用再手抄。变量名预填为下表的推荐名，可以逐项改成你代码里用的名字；**改完环境变量都要重新部署才生效**。

| 导入项 | 推荐变量名 | 存储方式 |
|---|---|---|
| Client ID | `AIGZM_CLIENT_ID` | 普通 |
| Client Secret | `AIGZM_CLIENT_SECRET` | 密钥（加密） |
| 回调白名单 | `AIGZM_REDIRECT_URI` | 普通；多条时可选只取第一条，或全部写成 JSON 数组 |
| 支付回跳白名单 | `AIGZM_BILLING_RETURN_URI` | 普通；同上 |
| 事件 Webhook URL | `AIGZM_WEBHOOK_URL` | 普通 |
| 事件 Webhook Secret | `AIGZM_WEBHOOK_SECRET` | 密钥（加密） |
| API Key | `AIGZM_API_KEY` | 密钥（加密） |
| 平台 OAuth Issuer | `AIGZM_EXPECTED_ISSUER` | 普通 |

**三类密钥是「生成新值」，不是读出旧值**：平台只保存 Client Secret 和 API Key 的哈希，自己也读不出原值，所以导入时会：

- **Client Secret**：立即轮换，旧值马上失效。本地开发或其他服务器里用着旧值的地方会立刻鉴权失败。
- **Webhook Secret**：轮换，旧密钥在 24 小时宽限期内仍会参与签名，请在这段时间内更新所有接收端。
- **API Key**：为你的账号新建一把 Key（已有的 Key 不受影响），占用 1 个名额（每个账号最多 5 把可用 Key）。

新值直接写进实例的加密环境变量，默认不会展示。如果本地开发也要用同一个应用，导入时勾选「同时把新生成的值显示一次」，并在关闭提示前复制保存。

## 11. 数据 API（API 广场）

平台除模型外还提供第三方**数据接口**：抖音、小红书、B 站、快手、微博、微信、知乎、今日头条、西瓜、皮皮虾等平台的作品、用户、搜索、榜单、评论等数据，统一经平台网关调用、按积分计费。接口清单由后台上架配置实时生成，**唯一真源是 `GET /v1/market/documentation`**（Markdown，按分类列出全部接口的方法、调用路径、积分单价与逐接口文档链接）；程序消费用 JSON 索引 `GET /v1/market/categories`、`GET /v1/market/endpoints?supplier=&category=&keyword=&page=`、`GET /v1/market/endpoints/{supplier_slug}/{endpoint_key}`。可视化目录在 `/docs/api`。

调用方法（所有数据接口通用）：

1. 地址：`https://<平台域名>/v1/data/{supplier_slug}/{上游原始路径}`。索引里每个接口的「调用路径」已经拼好，直接用。
2. 鉴权：Developer API Key（创建后必须「确认已保存」激活）。`Authorization: Bearer <api_key>` 或 `X-API-Key: <api_key>` 二选一，同时带会返回 `400 ambiguous_credentials`。scope 与模型调用相同，不需要额外申请。
3. 方法与参数：与接口标注一致——GET 走 query，POST 走 JSON 请求体（≤ 1MB）。参数表、请求体结构与可运行 cURL 见逐接口文档 `GET /v1/market/endpoints/{supplier_slug}/{endpoint_key}/documentation`。
4. 响应：保持上游 JSON 的**结构、字段与数字精度**，平台不包装、不改形；数据源自身的元信息字段（文档链接、缓存链接等）不返回，上游品牌/链接替换为占位文本。业务数据一般在 `data` 字段；平台自身信息只走响应头 `X-IdP-Points-Charged`、`X-IdP-Endpoint-Key`、`X-IdP-Reservation-Id`、`X-IdP-Error-Source`。
5. 计费：上游返回成功（HTTP 2xx 且信封 `code` 为 2xx）即按接口积分单价扣费；上游失败自动全额退还。**「作品已删除 / 权限受限」等情况上游同样返回 200 并收费，平台照常扣积分**，内容是否可用要自行读响应体判断。
6. 错误：平台侧拒绝返回 `{"error","message"}` 且 `X-IdP-Error-Source: platform`，均未扣费——`401` Key 无效、`402 insufficient_balance`、`403 endpoint_forbidden`（等级不够/需人工开通）、`404 endpoint_not_available`（不存在或未上架）、`429 rate_limited`（读 `Retry-After`）、`503`（暂不可用）；`502 supplier_unavailable` 表示上游未返回确定结果，预扣待人工核对，保留 `X-IdP-Reservation-Id` 不要盲目重试。上游失败原样返回并带 `X-IdP-Error-Source: supplier`。
7. 超时与限流：平台等待上游最长 30 秒，客户端超时建议 ≥ 45 秒；每个接口有独立 QPS（索引里标注），另有开发者级 QPS 与日调用量上限。

```bash
curl "https://<平台域名>/v1/data/social-data/api/v1/douyin/web/fetch_one_video?aweme_id=7372484719365098803" \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

不要新建「友好封装」再自定义响应结构：原样透传是数据 API 的核心承诺——从上游直连迁移只换 base 前缀与 Token，业务代码一行不改。

本文件只使用占位配置，不包含真实 Client、Token、API Key、商户或供应商凭证。


## Fish Audio：语音合成与私有音色 API

能力仅在后台已发布模型并完成积分标准及台湾出口配置后开放；没有默认免费额度。TTS 与音色创建分别是模型列表中的独立计价项，实际价格及 UTF-8 文本预算以 `GET /v1/developer/models` 为准。仅使用平台原生 `/v1/developer/models/{model_key}/invoke`（X-API-Key）或 `/v1/models/{model_key}/invoke`（OAuth `points.spend`），不提供 `/v1/audio/speech` 兼容路由。

- 普通合成：`input={"text":"你好","format":"mp3"}`，支持 mp3/wav 与 speed 0.5–2。成功输出 `output.audio.b64_json`、`mime_type`、`format`、`bytes`，调用方自行安全保存音频。
- 即时克隆：在 TTS input 附加 `references:[{audio:"<录音Base64>",text:"录音转录"}]`、`voice_rights_confirmed:true`；不创建可复用音色。
- 可复用克隆：调用后台配置的 `voice-clone-fast` 对应平台模型，传 `title`、`references` 和 `voice_rights_confirmed:true`；保存返回的平台 `output.voice_id`。后续 TTS 传 `voice_id`，不能同时传 references，不接受上游 reference_id。
- 参考录音必须拥有声音使用权，1–3段、每段10–180秒、合计至多8MiB；格式为真实 WAV/MP3/M4A/Ogg Opus。文本与参考转录合计以 UTF-8 字节计算，序列化 input 至多12MiB。
- `GET /v1/developer/voices?page=1&page_size=20` 只查询本人音色；`PATCH /v1/developer/voices/{voice_id}` 携带 `{based_on_version:1,status:"disabled"}` 可停用（或active恢复）。OAuth 管理对应 `/v1/voices`。停用不物理删除供应商录音；声音授权证据、原始录音和归属不允许普通编辑，物理删除需运营专用核验流程。
- 同一 idempotency_key 重放不重复调用或扣费，也不重传旧音频；调用方需保留首次音频。504、超时与不完整响应保留 provider_unknown 等待核对，不能换键重试。
- 平台音色与用户、供应商稳定绑定，原创建调用必须成功结算后才能使用。只有 Fish 使用独立台湾代理，不改变其他模型线路；代理不可用时拒绝，不自动直连或切换别的供应商。
