# API 接入文档（v1 · 已实现版）

> 2026-11-04 · 本文与线上实际接口一致，可直接照着写代码。

---

## 一、基础

| 项 | 值 |
|---|---|
| Base URL | `http://<你的域名>/v1` |
| 鉴权 | `Authorization: Bearer sk-live-xxxx` |
| 内容类型 | `application/json`（上传资产支持 multipart） |
| 可选头 | `X-Sub-User: 你的客户标识`（用量归集用） |
| 可选头 | `Idempotency-Key: 任意字符串`（24 小时内重放返回同一任务，不重复扣点） |

**怎么拿 key**：登录网页 → 点「充值」→ 底部「API 接入」→ 新建 key（明文只显示一次）。

---

## 二、9 个功能（kind）

| kind | 说明 | 同步 | 参数 |
|---|---|---|---|
| `script` | 写剧本 | ✅ | `input.text` |
| `video` | 视频提示词 | ✅ | `input.text` |
| `storyboard` | 分镜提示词 | ✅ | `input.text` |
| `drama` | 文戏提示词 | ✅ | `input.text` |
| `fight` | 武戏提示词 | ✅ | `input.text` |
| `edit` | 改图提示词 | ✅ | `input.text`、`input.asset`（**必填**）、`input.model`（**建议指定**） |
| `wan3_prompt` | 全能视频短剧·写提示词 | ✅ | `input.text`、`input.sec`、`input.resolution` |
| `t2i` | 文生图 | ❌ 异步 | `input.text` |
| `reverse` | 图片反推 | ❌ 异步 | `input.asset`（**必填**） |
| `drama_gen` | 短剧生成 | ❌ 异步 | `input.text`、`input.target_sec` |
| `wan3` | 全能视频短剧·出片 | ❌ 异步 | `input.prompt`、`input.prompt_job`（**必填**）、`input.sec`、`input.resolution` |

**实时单价一律从 `GET /v1/caps` 读，不要硬编码。**

### 关于 `input.model`（改图 / 文生图）

两个模型的**提示词格式不一样**，必须告诉接口你打算用哪个：

| model | 应用 | 图片引用格式 | 说明 |
|---|---|---|---|
| `模型B` | 模型B-Image 2.1 | `<image1>` `<image2>` | 尖括号标记（官方规范） |
| `模型A` | 模型A（改图） | 「图1」「图2」 | K2 **不认**尖括号，写了也无效 |
| `模型C` | 模型C | 「图1」 | 同 模型A 口径 |
| `模型A`（t2i） | 模型A 文生图 | — | 文生图默认就是这个 |

不传 `model` 就走站内默认；**传了但匹配不到应用会返回 400 并列出可用值**。

```json
POST /v1/jobs
{ "kind": "edit",
  "input": { "text": "把背景换成雪夜街道，人物不动",
             "asset": "ast_xxx",
             "model": "模型A" } }
```

返回（模型A 口径，注意是「图1」不是 `<image1>`）：

```
保持图1中人物的身份、五官、发型、姿势、表情、视线、服装、肢体动作和画面位置完全不变，
只替换人物身后的背景。把原背景整体换成雪夜街道……
```


---

## 三、接口

### 1. 创建任务

```http
POST /v1/jobs
```

```json
{
  "kind": "script",
  "input": { "text": "一个外卖骑手在暴雨里接单，到了地址发现客户是前女友" },
  "wait": 45
}
```

| 字段 | 说明 |
|---|---|
| `kind` | 见上表 |
| `input` | 按 kind 不同 |
| `wait` | 0（默认，立即返回）/ 1~60（最多等这么多秒） |

**响应（已完成）**

```json
{
  "id": "20261104_180521_747680",
  "kind": "script",
  "status": "done",
  "points": 17,
  "points_raw": 18,
  "created": 1793786721.4,
  "elapsed_ms": 40120,
  "result": { "text": "【场景1】……" }
}
```

**响应（异步，wait=0）**

```json
{
  "id": "20261104_180742_0783c6",
  "kind": "wan3",
  "status": "queued",
  "points": 100,
  "points_raw": 100,
}
```

`points_raw` 是估点数，`points` 是实际要扣的点数（已经算好，直接按它扣）。

---

### 2. 查询任务

```http
GET /v1/jobs/{id}
```

```json
{
  "id": "20261104_180742_0783c6",
  "kind": "wan3",
  "status": "done",
  "points": 100,
  "elapsed_ms": 188230,
  "result": {
    "text": "已出片：1 个文件 · 1 段 · 扣 100 点",
    "videos": [
      {
        "url": "/v1/files/20261104_180742_0783c6/0",
        "type": "video",
        "name": "20261104_180742_0783c6_seg01.mp4",
        "bytes": 1162369
      }
    ]
  }
}
```

**status**：`queued` / `running` / `done` / `failed` / `canceled`

**result 结构**
- 提示词类：`{"text": "..."}`
- 出图类：`{"images": [{url,type,name,bytes}]}`
- 出片类：`{"videos": [{url,type,name,bytes}]}` + 可能带 `text` 说明

---

### 3. 取消任务

```http
POST /v1/jobs/{id}/cancel
```

只有 `queued` 能取消（`running` 返回 409）。

---

### 4. 下载产物

```http
GET /v1/files/{job_id}/{index}?key=sk-live-xxxx
```

`index` 就是 `result.videos[0]` / `result.images[0]` 里的序号（从 0 开始）。

**为什么要 `?key=`**：`<video src>` / `<img src>` 带不了 Authorization 头，所以这个接口额外接受 query 里的 key。

---

### 5. 能力清单

```http
GET /v1/caps
```

```json
{
  "kinds": {
    "script": { "group": "prompt", "sync": true, "available": true, "points": 18 },
    "wan3": {
      "group": "video", "sync": false, "available": true,
      "sec_range": [2, 30],
      "resolutions": {
        "480P": { "points_per_sec": 20 },
        "720P": { "points_per_sec": 34 },
        "1080P": { "points_per_sec": 65 }
      },
      "require": "prompt_job"
    }
  },
  "is_agent": false,
  "balance": 2900,
  "user": { "id": 1, "name": "admin" }
}
```

`points` / `points_per_sec` 就是最终要扣的点数，直接按它扣，不用自己再换算。

---

### 6. 余额

```http
GET /v1/balance
→ { "points": 2900, "today_used": 0, "day_limit": 0, "is_agent": false }
```

---

### 7. 上传资产

```http
POST /v1/assets
```

**方式 A：JSON base64**

```json
{ "data": "data:image/png;base64,iVBORw0...", "name": "参考图", "media": "image" }
```

**方式 B：multipart**

```bash
curl -X POST http://<域名>/v1/assets \
  -H "Authorization: Bearer sk-live-xxxx" \
  -F "file=@photo.jpg" -F "media=image"
```

```json
{ "id": "ast_dd0ff6857e", "name": "参考图", "media_type": "image", "bytes": 139982,
  "url": "/api/asset-image/ast_dd0ff6857e" }
```

拿到 `id` 后，`reverse` / `t2i` / `wan3` 的 `input.asset` 传它。

---

### 8. 用量

```http
GET /v1/usage?from=2026-11-01&to=2026-11-04&group=kind
```

```json
{
  "from": "2026-11-01", "to": "2026-11-04", "group": "kind",
  "total_jobs": 8, "total_points": 285,
  "groups": [
    { "key": "wan3", "jobs": 2, "points": 200 },
    { "key": "script", "jobs": 1, "points": 18 }
  ]
}
```

`group` 可选 `day` / `kind` / `sub_user`。

---

### 9. 流水

```http
GET /v1/ledger?limit=50
```

```json
{
  "items": [
    { "kind": "consume", "points": -100, "job_id": "20261104_180742_0783c6",
      "api_kind": "wan3", "status": "done", "created": 1793786862.4 }
  ],
  "limit": 50, "offset": 0
}
```

`kind` = `consume`（消耗）/ `refund`（失败退回，`points` 为 0 表示已退回）。

---

### 10. 自己管理 key

```http
GET    /v1/keys            列 key
POST   /v1/keys            新建（{"name":"生产"}）→ 返回明文一次
DELETE /v1/keys/{kid}      吊销
```

> 第一个 key 必须在网页上建（「充值」→「API 接入」），之后可以用 API 管理。

---

## 四、完整示例：全能视频短剧（两步）

```python
import requests, time

BASE = "http://your-domain/v1"
H = {"Authorization": "Bearer sk-live-xxxx"}

# 第①步：写 视频提示词（这一步扣提示词费）
p = requests.post(f"{BASE}/jobs", headers=H, json={
    "kind": "wan3_prompt",
    "input": {"text": "一个女孩站在暴雨的街口，抬头看路灯", "sec": 5, "resolution": "480P"},
    "wait": 60,
}, timeout=120).json()

print(p["status"], p["points"])          # done 15
prompt = p["result"]["text"]

# 客户可以随便改这段提示词
prompt = prompt.replace("路灯", "钠灯")

# 第②步：出片（必须带 prompt_job，扣出片费）
v = requests.post(f"{BASE}/jobs", headers=H, json={
    "kind": "wan3",
    "input": {
        "prompt": prompt,
        "prompt_job": p["id"],
        "sec": 5,
        "resolution": "480P",
    },
    "wait": 0,
}).json()

# 轮询
while True:
    j = requests.get(f"{BASE}/jobs/{v['id']}", headers=H).json()
    if j["status"] in ("done", "failed", "canceled"):
        break
    time.sleep(5)

print(j["status"], j["points"])
video = j["result"]["videos"][0]
url = f"http://your-domain{video['url']}?key=sk-live-xxxx"
open("out.mp4", "wb").write(requests.get(url).content)
```

---

## 五、限流与错误

| 项 | 默认 |
|---|---|
| QPS | 10 次/秒 |
| 并发任务 | 5 个 |
| 单文件 | ≤ 20MB |

**错误结构**

```json
{ "detail": { "code": "insufficient_points", "message": "需要 100 点，当前 40 点" } }
```

| code | HTTP | 含义 |
|---|---|---|
| `invalid_api_key` | 401 | key 无效/吊销 |
| `account_blocked` | 403 | 账号被封 |
| `insufficient_points` | 402 | 点数不足 |
| `rate_limited` | 429 | 超出并发/QPS |
| `invalid_input` | 400 | 参数错误 |
| `not_ready` | 501 | 该 kind 还没开放 |
| `job_not_found` | 404 | 任务/产物不存在 |
| `job_not_cancelable` | 409 | 已开始跑，不能取消 |

---

---

## 六、数据模型

### 6.1 Job 对象

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 任务 ID，形如 `20261104_180521_747680` |
| `kind` | string | 功能名，见第二节 |
| `status` | string | `queued` / `running` / `done` / `failed` / `canceled` |
| `points` | integer | **实扣点数**（直接按它扣） |
| `points_raw` | integer | 创建时的估点数，仅创建时返回 |

| `created` | number | 创建时间，Unix 秒（带小数） |
| `elapsed_ms` | integer | 从开始执行到结束的毫秒数，未完成时为 `null` |
| `result` | object | 见 6.2，仅 `done` 时有 |
| `error` | string | 失败原因，仅 `failed` 时有 |

### 6.2 Result 对象

| 字段 | 类型 | 说明 |
|---|---|---|
| `text` | string | 提示词 / 剧本内容。出片类也可能有一句说明 |
| `videos` | array | 视频产物，见 6.3 |
| `images` | array | 图片产物，见 6.3 |

三个字段**至少有一个**。

### 6.3 Media 对象

| 字段 | 类型 | 说明 |
|---|---|---|
| `url` | string | **相对路径**，如 `/v1/files/xxx/0`，必须拼域名 + `?key=` |
| `type` | string | `video` / `image` |
| `name` | string | 原始文件名 |
| `bytes` | integer | 文件大小 |
| `sec` | number\|null | 视频时长（秒），图片为 `null` |

### 6.4 Kind 枚举

| 值 | 中文 | 分组 | 同步 |
|---|---|---|---|
| `script` | 写剧本 | prompt | ✅ |
| `video` | 视频提示词 | prompt | ✅ |
| `storyboard` | 分镜提示词 | prompt | ✅ |
| `drama` | 文戏提示词 | prompt | ✅ |
| `fight` | 武戏提示词 | prompt | ✅ |
| `edit` | 改图提示词 | prompt | ✅ |
| `wan3_prompt` | 全能视频短剧·写提示词 | prompt | ✅ |
| `t2i` | 文生图 | image | ❌ |
| `reverse` | 图片反推 | image | ❌ |
| `drama_gen` | 短剧生成 | video | ❌ |
| `wan3` | 全能视频短剧·出片 | video | ❌ |

### 6.5 错误对象

```json
{ "detail": { "code": "insufficient_points", "message": "需要 100 点，当前 40 点" } }
```

⚠️ **注意**：错误体是 `detail` 包裹的。你的后端要给前端用，记得先解包
（`body.detail.code` / `body.detail.message`）。

---

## 七、幂等与重试

### 7.1 幂等

```
POST /v1/jobs
Idempotency-Key: <你生成的唯一字符串，建议 UUID>
```

- 同一 Key 在 **24 小时**内重复提交 → 返回**同一个** job，**不重复扣点**
- 只对 `POST /jobs` 生效
- 建议在「网络超时 / 连接中断」时用同一个 Key 重试

### 7.2 什么情况可以重试

| 情况 | 能不能重试 | 怎么做 |
|---|---|---|
| 网络超时、连接被重置 | ✅ | 带**同一个** `Idempotency-Key` 重发 |
| 429 rate_limited | ✅ | 按 `Retry-After` 退避（没这个头就等 5 秒） |
| 502 upstream_error | ✅ | 等 10 秒重试，最多 3 次 |
| 504 timeout | ✅ | 任务还在跑，转成轮询 `GET /jobs/{id}` |
| 401 / 402 / 403 / 400 | ❌ | 改配置或充值，重试无用 |

### 7.3 轮询建议

```
间隔：5 秒
上限：10 分钟（出片类一般 3~6 分钟能出）
超过上限：判超时，不要再等，去后台看任务状态
```

**不要**用 1 秒间隔轮询——会撞 QPS 限流（429）。

---

## 八、变更日志

| 日期 | 变更 |
|---|---|
| 2026-11-04 | 首次发布 v1。11 个 kind，6 个接口。新增 `wan3` 两步流程（`prompt_job` 校验）、`edit` 的 `model` 参数、`/v1/files` 下载 |
| — | 后续变更按「日期 + 改了什么 + 影不影响调用方」补在这里 |
