Files
cmhub/docs/api.md
T

220 lines
10 KiB
Markdown
Raw Normal View History

2026-07-01 17:42:10 +08:00
# 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)。
通用错误响应:
```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 轮换机制。