Files
cmhub/docs/api.md
T
2026-07-02 14:47:22 +08:00

12 KiB
Raw Blame History

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 账本主体;注册邮箱必须验证且唯一,避免同邮箱对应多个点数账户。

通用错误响应:

{
  "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,但只允许 Provider 白名单内的安全参数透传;model、n、size、resolution、messages、image* 等核心/计费字段由服务端固定,调用方传入时忽略。

POST /api/v1/generate/title

生成标题。请求:

{
  "prompt": "为这件女装生成5个吸睛标题",
  "model": "title-standard",      // 能力别名,非具体模型名;可选,缺省用该操作的默认别名
  "image_url": "https://...",     // 可选,看图生成时传商品图(或 image_base64)
  "resolution": "1K",             // 可选,影响计费规则匹配
  "parameters": {}                // 可选,安全供应商参数;未知字段/核心字段忽略
}

成功响应:

{
  "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

生成图片(同步等待)。请求:

{
  "prompt": "把这件衣服换成模特上身的穿搭图",
  "model": "image-hd",            // 能力别名,非具体模型名
  "image_base64": "data:image/png;base64,...",  // 或 image_url;改图类模型(如映射到 gpt-image-2)原图必传
  "resolution": "1K",
  "aspect_ratio": "1:1",
  "parameters": {}                // 可选,安全供应商参数;未知字段/核心字段忽略
}

成功响应:

{
  "image_url": "https://.../result.jpg",   // 默认返回 URL(结果存对象存储);大 base64 不进同步响应体
  "alias": "image-hd",
  "model_used": "nano_banana_2",
  "points_cost": 10,
  "points_balance": 88,
  "call_id": 12346
}

要点:改图类模型缺原图返回 bad_request,不落 500;上游失败返回 upstream_error 且不扣点(已预扣则退回)。结果默认存对象存储返回 image_url,避免同步响应体过大。

GET /api/v1/balance

查询当前账号点数余额。成功响应:

{
  "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) 时锁定当前汇率并计算预计到账点数,再向支付平台下单,返回支付二维码。请求:

{
  "amount": "100.00",
  "pay_method": "weixin"          // weixin | alipay
}

成功响应:

{
  "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 层只传别名,由本模块解析到具体模型并选适配器。

别名解析

# alias -> 具体 AiModel(含 capabilities、解密后的 key);找不到/能力不符抛业务异常
resolve_alias(operation_type: str, alias: str | None) -> ResolvedModel

T-102 后,别名解析已接入数据库表 AiModel / ModelAlias:每次调用读取当前 active 记录,缺省 alias=None 时取该 operation_type 的默认别名;标题要求 text 能力,图片要求 image 能力。密钥以 AiModel.api_key_encrypted 存储,使用 Fernet 解密后进入 ResolvedModel。

从 cmbot 形状导入配置的管理命令:

py -3.12 manage.py import_ai_models path\to\ai_models.json --create-default-aliases

导入前必须配置有效 AI_KEY_ENCRYPTION_KEY。不要把真实 ai_models.json 或真实上游 key 提交到 git。

Provider 适配器接口

每个供应商一个适配器,按 api_type 注册;移植自 cmbot 的调用逻辑落到各适配器内:

class Provider(Protocol):
    def capabilities(self) -> set[str]: ...           # {"text","image","vision"}
    def generate_text(self, prompt: str, model: ResolvedModel,
                      image: bytes | None = None,
                      image_mime_type: str = "image/png",
                      resolution: str = "1K",
                      parameters: dict | None = None) -> TextGenerationResult: ...
    def generate_image(self, prompt: str, model: ResolvedModel,
                       image: bytes | None = None,
                       image_mime_type: str = "image/png",
                       image_filename: str = "image.png",
                       resolution: str = "1K", aspect_ratio: str = "1:1",
                       parameters: dict | None = None) -> ImageGenerationResult: ...

要点:

  • ResolvedModel 来自数据库 AiModel(形状同 cmbot/config/ai_models.json,外加 capabilities;api_key 在库中以 Fernet 加密,使用时解密,不落明文)。
  • TextGenerationResult 包含 text、清洗后的 titles、model_used、raw;ImageGenerationResult 包含图片 bytes、model_used、raw。raw 仅供本次解析/排障摘要使用,调用记录不得整包保存或打印,避免 base64 大图和上游敏感字段入库;图片落对象存储并返回 URL 属 T-302 之后的 API 编排职责。
  • 适配器按 api_type(chat/gemini/images/images_edits/auto)从注册表选取,新增供应商 = 新增一个适配器,不改对外接口。
  • parameters 为供应商特有参数的安全子集;适配器负责白名单过滤并把统一入参翻译成各家上游格式,核心字段不可被 parameters 或 extra_body 覆盖。
  • 配置热生效:每次调用读当前 AiModel/ModelAlias,后台改动及时反映(或带缓存失效)。
  • 配置审计:后台保存/删除 AiModel、ModelAlias 时写 AiConfigAuditLog;记录 actor/action/target/changed_fields/changes/created_at,admin 只读查看;密钥变更只记录 empty/set 状态。
  • 区分异常:别名/能力不匹配 → 业务错误(400 类,如 model_not_allowed);上游网络/超时/服务错误 → upstream_error(502,触发退点)。
  • 不在本模块写点数逻辑,只负责解析、调上游与解析返回。

待实现时确认

  • 支付商户真实配置:微信 appid/mchid/apiv3_key/证书/notify_url,支付宝 appid/应用私钥/支付宝公钥/notify_url。协议字段、验签方式与成功应答已按同系统实现明确;缺真实配置时用 mock。
  • 汇率与各操作/别名(+ 分辨率档)的点数单价。
  • 图片结果存储:对象存储选型与 image_url 生成(默认走存储返回 URL)。
  • 图片模型调用机制:gpt-image-2 走 images/edits(改图,原图必传)、nano-banana2 走 chat/completions 多模态返图(需自定义解析),两者非标准 images/generations。图片返回结构(URL / base64 / 位置)首次对接抓真实响应确认后再定适配器解析。
  • 别名命名规范、默认别名、是否按账号授权可用别名(account_alias_permission)。
  • 参数校验细则、限流策略、API Key 轮换机制。