Files
cmhub/docs/api.md
T

221 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# API / 模块合约
> 本文定义 `cmhub` 对外 HTTP 接口、支付回调合约与 AI 调用模块合约的目标形状。
> 实现前可细化,但不要在代码里另起一套不兼容接口。schema 变化须同步 `04-architecture.md`。
## 通用约定
- 传输:JSON over HTTPS。
- 对外鉴权:请求头 `Authorization: Bearer <API_KEY>`(用户在用户端自助生成的 Key)。
- 后台鉴权:django-admin 用 Django Session 登录。
- 编码:UTF-8 JSON。时间:ISO 8601。
- 未携带有效 API Key 调用生成/余额接口:返回 `401`。
- 用户或 Key 被禁用/吊销(disabled/revoked):返回 `403`。
- **对外 API 只接受 API Key 认证,不接受 Web session**(浏览器带 cookie 也不能调 API,防绕过计费归属)。
- 图片生成为**同步**接口,可能耗时较长,调用方与网关需设置足够超时(≥ 300s)。
- 用户端注册使用同一个 `User` 账本主体;注册邮箱必须验证且唯一,避免同邮箱对应多个点数账户。
通用错误响应:
```json
{
"error": {
"code": "insufficient_points",
"message": "点数不足,请先充值"
}
}
```
错误码枚举(MVP):
| code | 含义 | HTTP |
| --- | --- | --- |
| `unauthorized` | 缺失/无效 API Key | 401 |
| `account_disabled` | 账号被禁用 | 403 |
| `bad_request` | 参数错误 | 400 |
| `insufficient_points` | 点数不足,请先充值 | 402 |
| `no_pricing_rule` | 未配置对应计费规则 | 400 |
| `model_not_allowed` | 该模型不支持此操作(如图片模型生成标题) | 400 |
| `upstream_error` | 上游 AI 失败(已退点) | 502 |
| `signature_invalid` | 支付回调验签失败 | 400 |
| `amount_mismatch` | 支付回调金额与本地订单金额不一致 | 400 |
## 对外接口
> **重要约定**:`model` 字段传的是**能力别名**(如 `title-standard` / `image-hd`),不是具体供应商模型名。后台把别名映射到当前的具体模型,换供应商时调用方零改动。供应商特有参数放 `parameters` 透传对象,核心字段保持稳定。
### `POST /api/v1/generate/title`
生成标题。请求:
```json
{
"prompt": "为这件女装生成5个吸睛标题",
"model": "title-standard", // 能力别名,非具体模型名;可选,缺省用该操作的默认别名
"image_url": "https://...", // 可选,看图生成时传商品图(或 image_base64)
"resolution": "1K", // 可选,影响计费规则匹配
"parameters": {} // 可选,供应商特有参数透传,未知字段忽略
}
```
成功响应:
```json
{
"titles": ["标题一", "标题二", "标题三"],
"alias": "title-standard",
"model_used": "gpt-5.5", // 实际服务的具体模型,便于排障;不构成稳定契约
"points_cost": 2,
"points_balance": 98,
"call_id": 12345
}
```
要点:别名必须映射到声明 `text` 能力的模型;映射到图片模型时返回 `model_not_allowed`;别名无计费规则返回 `no_pricing_rule`。
### `POST /api/v1/generate/image`
生成图片(同步等待)。请求:
```json
{
"prompt": "把这件衣服换成模特上身的穿搭图",
"model": "image-hd", // 能力别名,非具体模型名
"image_base64": "data:image/png;base64,...", // 或 image_url;改图类模型(如映射到 gpt-image-2)原图必传
"resolution": "1K",
"aspect_ratio": "1:1",
"parameters": {} // 可选,供应商特有参数透传
}
```
成功响应:
```json
{
"image_url": "https://.../result.jpg", // 默认返回 URL(结果存对象存储);大 base64 不进同步响应体
"alias": "image-hd",
"model_used": "nano_banana_2",
"points_cost": 10,
"points_balance": 88,
"call_id": 12346
}
```
要点:上游失败返回 `upstream_error` 且**不扣点**(已预扣则退回)。结果默认存对象存储返回 `image_url`,避免同步响应体过大。
### `GET /api/v1/balance`
查询当前账号点数余额。成功响应:
```json
{
"user": "demo-client",
"points_balance": 88
}
```
## 支付充值(自助扫码:微信 V3 native + 支付宝当面付)
> 协议对齐同支付系统的既有 PHP 实现(微信库 `wechatpayv3` / 支付宝库 `python-alipay-sdk`)。二维码是平台不透明票据、**不含业务数据**,靠 `out_trade_no`(=本地 `order_no`) 在回调关联,付款人由平台识别。**协议已明确,仅商户密钥/证书为真实值待提供。**
> 金额单位陷阱:**微信传「分」(元×100 取整),支付宝传「元」(两位小数字符串)**。
### 轮询 / 主动查单
前端展示二维码后每 ~1s 轮询 `GET /api/v1/recharge/status?order_no=...`(session)返回订单 `status`;到账以异步回调为权威。回调可能丢失,服务端应提供**主动查单**兜底(按 `order_no` 向平台查单后补入账)。
### `POST /api/v1/recharge/callback/wechat`
微信 V3 异步回调(支付网关服务端调,**必须 `@csrf_exempt`**)。处理合约:
1. SDK `callback(headers, body)` **验签 + 解密**;失败 → `signature_invalid`,不入账、记可疑回调。
2. `event_type == TRANSACTION.SUCCESS` 且 `resource.trade_state == SUCCESS`:按 `out_trade_no` 定位订单。
3. 幂等:`select_for_update` 锁订单,`status != pending` 直接返回成功、不重复加点。
4. 入账:校验回调金额与本地订单金额一致;使用订单创建时锁定的 `exchange_rate` / `points_granted`,锁 `user_wallet` 加点、写 `points_ledger(recharge)`、订单置 `paid`(记 `transaction_id`、`success_time`)。
5. 返回 `{"code":"SUCCESS","message":"成功"}`(微信要求 HTTP 200)。
### `POST /api/v1/recharge/callback/alipay`
支付宝当面付异步回调(**必须 `@csrf_exempt`**;原 PHP 未实现,本项目须补全)。处理合约:
1. SDK `verify(data, sign)` 验签;失败 → 不入账、记可疑回调。
2. `trade_status ∈ {TRADE_SUCCESS, TRADE_FINISHED}`:按 `out_trade_no` 定位订单,同上幂等 + 入账(记 `trade_no`、`gmt_payment`)。
3. 返回纯文本 `success`(支付宝要求)。
> 订单状态机 `pending → paid`(成功)/ `failed` / `expired`。**待提供的仅是商户配置真实值**:微信 `appid/mchid/apiv3_key/cert_serial/private_key(证书)`、支付宝 `appid/app_private_key/alipay_public_key/RSA2`、各 `notify_url`(公网)。到位前用 mock 联调并标注待替换。
### `POST /api/v1/recharge/create`
由**已登录用户**在用户端发起充值(MVP 必做)。创建 `recharge_order(pending, 绑定 user)` 时锁定当前汇率并计算预计到账点数,再向支付平台下单,返回**支付二维码**。请求:
```json
{
"amount": "100.00",
"pay_method": "weixin" // weixin | alipay
}
```
成功响应:
```json
{
"order_no": "T202606291230001234",
"amount": "100.00",
"exchange_rate": "10.00",
"points_granted": 1000,
"pay_method": "weixin",
"code_url": "weixin://wxpay/bizpayurl?pr=abc123", // 微信 native 返回;支付宝为 qr_code(https://qr.alipay.com/...)
"expires_at": "2026-06-29T12:10:00Z" // 二维码有效期
}
```
- 下单字段:`out_trade_no`(=order_no)、`amount`(微信**分**=元×100 取整 / 支付宝**元**字符串)、`description`、`notify_url`。
- `exchange_rate` 与 `points_granted` 以订单创建时的配置为准;回调入账使用订单值,不因后台后续改汇率而变化。
- 微信走 `pay/transactions/native` 取 `code_url`;支付宝走 `trade.precreate` 取 `qr_code`;前端用 qrcode.js 渲染。
- 走 **Web session** 鉴权(用户端流程),不同于对外 API Key;订单绑定发起用户,防充错账户。
## AI 调用模块合约(`apps/ai`)
分两层:**别名解析** + **Provider 适配器**。API 层只传别名,由本模块解析到具体模型并选适配器。
### 别名解析
```python
# alias -> 具体 AiModel(含 capabilities、解密后的 key);找不到/能力不符抛业务异常
resolve_alias(operation_type: str, alias: str | None) -> ResolvedModel
```
### Provider 适配器接口
每个供应商一个适配器,按 `api_type` 注册;移植自 cmbot 的调用逻辑落到各适配器内:
```python
class Provider(Protocol):
def capabilities(self) -> set[str]: ... # {"text","image","vision"}
def generate_text(self, prompt: str, model: ResolvedModel,
image: bytes | None = None,
resolution: str = "1K",
parameters: dict | None = None) -> list[str]: ...
def generate_image(self, prompt: str, model: ResolvedModel, image: bytes,
resolution: str = "1K", aspect_ratio: str = "1:1",
parameters: dict | None = None) -> bytes | str: ...
```
要点:
- `ResolvedModel` 来自数据库 AiModel(形状同 `cmbot/config/ai_models.json`,外加 `capabilities`;`api_key` 在库中加密,使用时解密,不落明文)。
- 适配器按 `api_type`(`chat`/`gemini`/`images`/`images_edits`/`auto`)从注册表选取,新增供应商 = 新增一个适配器,不改对外接口。
- `parameters` 为供应商特有参数透传;适配器负责把统一入参翻译成各家上游格式。
- 配置热生效:每次调用读当前 AiModel/ModelAlias,后台改动及时反映(或带缓存失效)。
- 区分异常:别名/能力不匹配 → 业务错误(400 类,如 `model_not_allowed`);上游网络/超时/服务错误 → `upstream_error`(502,触发退点)。
- 不在本模块写点数逻辑,只负责解析、调上游与解析返回。
## 待实现时确认
- 支付商户真实配置:微信 `appid/mchid/apiv3_key/证书/notify_url`,支付宝 `appid/应用私钥/支付宝公钥/notify_url`。协议字段、验签方式与成功应答已按同系统实现明确;缺真实配置时用 mock。
- 汇率与各操作/别名(+ 分辨率档)的点数单价。
- `AiModel.api_key` 加密方案(应用层 Fernet / KMS)与后台脱敏展示方式;运行配置见 `env.md`。
- 图片结果存储:对象存储选型与 `image_url` 生成(默认走存储返回 URL)。
- **图片模型调用机制**:`gpt-image-2` 走 `images/edits`(改图,原图必传)、`nano-banana2` 走 `chat/completions` 多模态返图(需自定义解析),两者非标准 `images/generations`。图片返回结构(URL / base64 / 位置)**首次对接抓真实响应确认**后再定适配器解析。
- 别名命名规范、默认别名、是否按账号授权可用别名(`account_alias_permission`)。
- 参数校验细则、限流策略、API Key 轮换机制。