jytest/work.md

588 lines
22 KiB
Markdown
Raw Permalink 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 · Golang 对接最小参考
## 0. 全局须知(对接前必读)
- 生成任务消耗 **token**(仅超级会员限时可用);token 包独立购买,**与即梦网页端积分不通用**。
- 任务提交按能力策略**预扣** token;成功按实际用量结算(多退少补);**失败全额返还**。
- 产物链接**有效期为 24 小时**,生成后请在 24h 内下载保存,逾期失效。
- 限流:任务提交 10 qps(用户级),任务查询 20 qps(用户级)。
- 所有接口均为 `POST`,`Content-Type: application/json`。
---
## 1. 鉴权(AK/SK 签名)★必须完整实现
调用方持一对 `AccessKey(AK)` / `SecretKey(SK)`。**SK 只在创建时明文返回一次,服务端不提供二次查询**,自行妥善保存。
### 1.1 请求头(8 个字段全部必填)
| Header | 必填 | 说明 |
|---|---|---|
| `X-Agent-Access-Key` | 是 | AK,服务端签发字符串 |
| `X-Agent-Timestamp` | 是 | 请求发起 Unix 时间戳(秒)。服务端允许 ±300 秒偏差,超出视为重放 |
| `X-Agent-Nonce` | 是 | 本次随机串(推荐 UUID)。同一 AK+Nonce 在时间窗口内去重,防重放 |
| `X-Agent-Body-SHA256` | 是 | HTTP Body 的 SHA-256 摘要(**小写十六进制**);空 Body 也要传空字符串的 SHA-256 |
| `X-Agent-Signature-Method` | 是 | 固定 `HMAC-SHA256` |
| `X-Agent-Version` | 是 | 固定 `v1` |
| `X-Agent-Signature` | 是 | 签名结果,Base64URL 编码(**不含 padding**),长度 = HMAC-SHA256 输出 32 字节 |
| `Content-Type` | 是 | 固定 `application/json`(**该字段参与签名**) |
### 1.2 签名算法
按顺序拼接 8 个字段,字段间用换行符 `\n` 分隔构造 `StringToSign`,再用 SK 做 HMAC-SHA256:
```
StringToSign = METHOD + "\n"
+ PATH + "\n"
+ CANONICAL_QUERY + "\n"
+ CONTENT_TYPE + "\n"
+ SHA256(RequestBody) + "\n"
+ TIMESTAMP + "\n"
+ NONCE + "\n"
+ ACCESS_KEY
Signature = Base64URL( HMAC-SHA256(SK, StringToSign) )
```
各字段规则:
- `METHOD`:HTTP 方法全大写,本 API 均为 `POST`。
- `PATH`:请求路径含前缀,如 `/agent_openapi/v1/video/submit`。
- `CANONICAL_QUERY`:query string 按参数名 **ASCII 升序**排序,每个 key/value 分别按 **RFC3986** 做 URL Encode(空格编码为 `%20`,`~` 保留不编码),最后用 `&` 连接;无 query 时为空字符串。
- `CONTENT_TYPE`:与请求头完全一致,即 `application/json`。
- `SHA256(RequestBody)`:Body 的**小写** SHA-256 十六进制,与 `X-Agent-Body-SHA256` 头一致。
- `TIMESTAMP / NONCE / ACCESS_KEY`:与对应请求头完全一致。
### 1.3 Golang 签名实现(标准库,无第三方依赖)
```go
package main
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
const (
host = "https://jimeng.jianying.com"
path = "/agent_openapi/v1/video/submit"
method = "POST"
contentType = "application/json"
accessKey = "your_access_key"
secretKey = "your_secret_key"
)
func main() {
body := []byte(`{"capability_key":"pippit_avatar_marketing_agent","action_key":"generate_video","run_id":"biz-avatar-005","avatar_marketing_input":{"product_name":"【高级美容护肤品拍一发三】虾青素抗皱面霜紧致修护补水保湿身体","product_image_url_list":["https://lf3-static.bytednsdoc.com/obj/eden-cn/upseh7fkuhm/product1.jpeg"],"model_image_url_list":["https://lf3-static.bytednsdoc.com/obj/eden-cn/upseh7fkuhm/model.jpeg"]}}`)
query := url.Values{}
bodySHA256 := sha256Hex(body)
canonicalQuery := canonicalQueryString(query)
timestamp := fmt.Sprintf("%d", time.Now().Unix())
nonce, err := randomNonce()
if err != nil {
panic(err)
}
stringToSign := method + "\n" +
path + "\n" +
canonicalQuery + "\n" +
contentType + "\n" +
bodySHA256 + "\n" +
timestamp + "\n" +
nonce + "\n" +
accessKey
signature := hmacSHA256Base64URL(secretKey, stringToSign)
requestURL := host + path
if canonicalQuery != "" {
requestURL += "?" + canonicalQuery
}
req, err := http.NewRequest(method, requestURL, bytes.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", contentType)
req.Header.Set("X-Agent-Access-Key", accessKey)
req.Header.Set("X-Agent-Timestamp", timestamp)
req.Header.Set("X-Agent-Nonce", nonce)
req.Header.Set("X-Agent-Body-SHA256", bodySHA256)
req.Header.Set("X-Agent-Signature-Method", "HMAC-SHA256")
req.Header.Set("X-Agent-Version", "v1")
req.Header.Set("X-Agent-Signature", signature)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
rspBody, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println("status:", resp.StatusCode)
fmt.Println(string(rspBody))
}
func canonicalQueryString(query url.Values) string {
keys := make([]string, 0, len(query))
for key := range query {
keys = append(keys, key)
}
sort.Strings(keys)
parts := make([]string, 0)
for _, key := range keys {
values := append([]string(nil), query[key]...)
sort.Strings(values)
for _, value := range values {
parts = append(parts, urlEncode(key)+"="+urlEncode(value))
}
}
return strings.Join(parts, "&")
}
func urlEncode(value string) string {
encoded := url.QueryEscape(value)
encoded = strings.ReplaceAll(encoded, "+", "%20")
encoded = strings.ReplaceAll(encoded, "%7E", "~")
return encoded
}
func sha256Hex(data []byte) string {
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}
func hmacSHA256Base64URL(secret, payload string) string {
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte(payload))
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
}
func randomNonce() (string, error) {
buf := make([]byte, 16)
_, err := rand.Read(buf)
if err != nil {
return "", err
}
return hex.EncodeToString(buf), nil
}
```
### 1.4 常见签名失败原因(报错 `10006` 时逐条排查)
- 本地时钟偏差超过 300 秒。
- `X-Agent-Body-SHA256` 不是小写十六进制。
- Base64URL 带了 `=` padding。
- 多个同名 query 参数未分别 encode 后再字典序拼接。
- 计算时用的 `Content-Type` 与实际发送的不一致。
- 同一 AK 的 nonce 在 300 秒窗口内重复。
---
## 2. 通用请求 / 响应结构
### 2.1 提交任务请求体通参
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `capability_key` | string | 是 | 能力标识,见「能力对照」 |
| `action_key` | string | 是 | 具体动作标识,须与 capability_key 匹配 |
| `run_id` | string | 否 | 业务侧幂等键。同一 AK 下重复提交同一 run_id 返回同一任务;未传由系统生成 |
| `input` | object | 是 | 能力相关业务入参,不同能力结构不同(见各能力章节) |
> 注:营销视频/剧情营销 使用字段名 `avatar_marketing_input` 而非 `input`;短剧与短片创作使用 `input`。
### 2.2 通用响应结构
```json
{ "code": "0", "message": "success", "request_id": "20260708xxxxxxxx", "data": { ... } }
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | string/int64 | 业务错误码,`0` 表示成功;非零见「错误码」 |
| `message` | string | 错误信息或成功描述 |
| `request_id` | string | 请求追踪 ID(LogID),**排障必须提供该字段** |
| `data` | object | 业务数据,不同接口结构不同 |
提交接口 `data` 通常含:`task_id`(后续查询主键)、`run_id`、`thread_id`(短剧会话 ID,链路串联依赖)、`status`(一般为 `QUEUED`)、`capability_key`、`action_key`、`created_at`(Unix 秒)。
### 2.3 任务状态机
| status | 含义 |
|---|---|
| `CREATED` | 已创建,等待入队 |
| `QUEUED` | 已入队,等待调度 |
| `RUNNING` | Agent 任务执行中 |
| `SUCCESS` | 成功终态,从 `data` 读取产物 |
| `FAILED` | 失败终态,`err_code`/`err_msg` 给出原因 |
| `CANCELED` | 已取消终态 |
### 2.4 计费字段(query 返回 `data.usage`)
| 字段 | 说明 |
|---|---|
| `withhold_token` | 预扣 Token(执行时冻结) |
| `actual_token` | 实际结算 Token(成功终态生效) |
| `refund_token` | 返还 Token(失败或结算预扣差额返还) |
| `withhold_quota` / `actual_quota` | 短片/剧情营销额外返回:视频=预估/实际总时长,图片=张数 |
---
## 3. 接口清单
Base:`https://jimeng.jianying.com`,全部 `POST` + 鉴权头。
| 用途 | 接口 | 能力 |
|---|---|---|
| 短剧提交 | `/agent_openapi/v1/novel/submit` | `pippit_novel_agent` |
| 短剧查询 | `/agent_openapi/v1/novel/query` | `pippit_novel_agent` |
| 视频提交 | `/agent_openapi/v1/video/submit` | 带货营销 / 剧情营销 / 短片创作 |
| 视频查询 | `/agent_openapi/v1/video/query` | 带货营销 / 剧情营销 / 短片创作 |
---
## 4. 能力对照(capability_key / action_key)
| 业务能力 | capability_key | 支持的 action_key |
|---|---|---|
| 短剧/漫剧 | `pippit_novel_agent` | `script_analysis`(剧本解析)、`narration_design`(旁白改编·可选)、`character_generate`(角色生成)、`scene_generate`(场景生成)、`storyboard_design`(分镜设计)、`shot_video_generate`(分镜短片 Seedance2.0)、`shot_video_generate_fast`(Seedance2.0 fast)、`shot_video_compose`(成片合成) |
| 带货营销视频 | `pippit_avatar_marketing_agent` | `generate_video`(另有 `avatar_marketing_agent_v2` 待开放) |
| 剧情营销视频 | `pippit_story_marketing_agent` | `generate_video` |
| 短片创作 | `pippit_video_part_agent` | `generate_video` |
---
## 5. 短剧 Agent(novel,多阶段流水线)
短剧是一串串联的阶段任务,用 `thread_id`(会话)+ `asset_id`(资产)把链路串起来:
```
script_analysis ─→ narration_design(可选) ─→ character_generate / scene_generate ─→ storyboard_design ─→ shot_video_generate(_fast) ─→ shot_video_compose
```
### 5.1 提交任务 `POST /agent_openapi/v1/novel/submit`
Body:
```json
{
"capability_key": "pippit_novel_agent",
"action_key": "<各阶段动作>",
"run_id": "biz-novel-003",
"input": { ... }
}
```
`input` 字段(是否必填取决于 action_key):
| 字段 | 类型 | 必填时机 | 说明 |
|---|---|---|---|
| `thread_id` | string | 除 `script_analysis` 外均需回填 | 剧本分析返回的会话 ID |
| `asset_id` | string | 除 `script_analysis` 外均需回填 | `narration_design/character_generate/scene_generate` 传 **OverviewAssetID**(蓝图资产);`storyboard_design/shot_video_generate/shot_video_generate_fast/shot_video_compose` 传 **StoryboardAssetID**(分镜资产) |
| `visual_style` | string | 是(script_analysis) | 视觉风格,如 `"2D,国风,平涂"`、`"3D,CG动画,写实都市"`、`"真人写实,电影风格,冷色调"` |
| `video_ratio` | string | 是(script_analysis) | `"16:9"` / `"9:16"` |
| `file_url` | string | 是(script_analysis) | 剧本文件 URL,需公网可访问 |
| `file_type` | string | 是(script_analysis) | `"txt"` / `"docx"` |
| `file_name` | string | 是(script_analysis) | 剧本文件名 |
| `shot_ids` | string[] | 是(shot_video_generate / _fast) | 分镜脚本编码列表,如 `["S1","S2"]` |
| `enable_watermark` | bool | 否 | 是否加水印,默认 false |
### 5.2 查询任务 `POST /agent_openapi/v1/novel/query`
请求 Body:`task_id` 或 `run_id` **二选一**。
响应关键字段(`data`):
- `status` / `usage` / `thread_id` / `start_at` / `elapsed_seconds`:同通用结构。
- `novel_data.overview_asset_id`:蓝图资产 ID,后续角色/场景/分镜/视频链路的关键入口。
- `novel_data.resp_data`:**JSON 字符串,需再次反序列化**才能读取内部字段。
`resp_data` 二次反序列化后的关键字段(对接必须会用):
| 字段 | 说明 |
|---|---|
| `OverviewAssetID` | 蓝图资产 ID → 旁白/角色/场景阶段回填为 `asset_id` |
| `StoryboardAssetID` | 分镜资产 ID(在 `StoryboardBriefs[]` 内)→ 分镜/短片/成片阶段回填为 `asset_id` |
| `Settings.VideoRatio` / `Settings.VisualStyle` | 比例与风格(与提交一致) |
| `EpisodeAssets[]` | 每集:`EpisodeID`、`EpisodeAssetID`、`StoryboardAssetID`、`CharacterAssetIDs`、`SceneAssetIDs` |
| `CharacterAssets[]` | 角色:`CharacterID/Name`、`CharacterAssetID`、`IsMainCharacter` |
| `SceneAssets[]` | 场景:`SceneAssetID`、`EpisodeAssetIDs` |
| `ScriptAssetID` / `CoreElement` | 剧本资产与核心要素 |
> 链路取 `asset_id` 的规则:`narration_design`、`character_generate`、`scene_generate` 取 **OverviewAssetID**;`storyboard_design`、`shot_video_generate(_fast)`、`shot_video_compose` 取对应集的 **StoryboardAssetID**。
---
## 6. 视频类能力(video:带货营销 / 剧情营销 / 短片创作)
三类共用 `POST /agent_openapi/v1/video/submit` 与 `/agent_openapi/v1/video/query`,仅 `capability_key` 与入参对象不同。
### 6.1 带货营销 `pippit_avatar_marketing_agent`
Body 使用 `avatar_marketing_input`:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `prompt` | string | 否 | 自定义营销 prompt |
| `product_name` | string | 是 | 商品名称,用于文案/素材语义理解 |
| `product_image_url_list` | array<string> | 是 | 商品图 1~3 张 |
| `model_image_url_list` | array<string> | 否 | 模特图,最多 1 张 |
| `enable_watermark` | bool | 否 | 默认无水印 |
### 6.2 剧情营销 `pippit_story_marketing_agent`
Body 使用 `avatar_marketing_input`:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `prompt` | string | 否 | 自定义剧情营销 prompt |
| `model` | string | 否 | 默认 `seedance20_fast`;可选 `seedance25` / `seedance20` / `seedance20_fast` / `seedance20_mini` |
| `duration` | int | 否 | 秒,默认 15s;`seedance25` 最长 30s |
| `product_name` | string | 是 | 商品名称 |
| `product_image_url_list` | array<string> | 是 | 商品图 1~9 张 |
| `model_image_url_list` | array<string> | 否 | 模特图,最多 1 张 |
| `enable_watermark` | bool | 否 | 默认无水印 |
### 6.3 短片创作 `pippit_video_part_agent`
Body 使用 `input`:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `prompt` | string | 是 | 提示词 |
| `image_url_list` | string[] | 否 | 参考图,最多 9 张,公网可访问 |
| `video_url_list` | string[] | 否 | 参考视频,最多 3 条;`seedance25` 总时长 ≤30s,其他 ≤15s |
| `audio_url_list` | string[] | 否 | 参考音频,最多 3 条;时长限制同上 |
| `model` | string | 否 | 默认 `seedance20_fast`,可选同剧情营销 |
| `duration` | int32 | 否 | ≥5s,默认 15s;`seedance25` 最大 30s,其他 15s |
| `ratio` | string | 否 | `21:9` / `16:9` / `4:3` / `1:1` / `3:4` / `9:16` |
| `resolution` | string | 否 | 默认 720P;`seedance25`:480/720/1080;`seedance20`:480/720/1080/4K;fast/mini:480/720 |
| `seed` | int32 | 否 | 随机种子,不传由下游生成 |
| `enable_watermark` | bool | 否 | 默认 false |
### 6.4 图片通用规格(所有引用图片,带货营销 ≤3 张,剧情/短片 ≤9 张)
- 格式:JPEG、PNG、WebP、BMP、TIFF、GIF、HEIC、HEIF。
- Seedance 2.0:宽高比 `(0.4, 2.5)` 不含边界;宽高各 `(300, 6000)` px 不含边界。
- Seedance 2.5:宽高比 `[0.4, 2.5]` 含边界;宽高各 `[300, 6000]` px 含边界。
- 单张 < 30 MB,URL 需公网可访问。
### 6.5 查询任务 `POST /agent_openapi/v1/video/query`
请求 Body:`task_id` 或 `run_id` **二选一**。
响应关键字段(`data`):
| 字段 | 说明 |
|---|---|
| `status` | 状态机取值 |
| `usage.*` | 计费明细(短片/剧情营销含 `withhold_quota` / `actual_quota`) |
| `video_artifact.url` | **成品视频下载 URL**(24h 有效) |
| `video_artifact.cover_url` | 封面图 |
| `video_artifact.duration` | 成片时长(秒) |
| `video_artifact.width` / `height` | 成片像素 |
| `start_at` / `elapsed_seconds` | 时间信息 |
| `err_code` / `err_msg` | 终态 `FAILED` 时才有 |
---
## 7. 错误码(对接需处理的分段与关键码)
分段:`1xxxx`=参数/鉴权/账号风控(可校对重试);`2xxxx`=业务规则(配额/审核/下游终态);`5xxxx`=系统内部(稍后重试)。
| code | 含义 | 建议 |
|---|---|---|
| `0` | 成功 | — |
| `10001` | 参数非法 | 校验请求体字段 |
| `10002` | AK 不存在 | 确认 AK,重新签发 |
| `10003` | AK 被禁用/冻结 | 联系管理员 |
| `10004` | 未授权/UserID 缺失 | 确认 AK 已开通 |
| `10005` | 未知能力/动作 | 核对 capability_key/action_key |
| `10006` | 签名校验失败 | 见 §1.4 逐项排查 |
| `10007` | 身份权限不满足 | 确认账号具备能力权限(如即梦 Ultra 会员) |
| `10008` | 请求频率超限 | 降并发(提交 10qps / 查询 20qps) |
| `10020` | 账号风控拦截 | 联系管理员,提供 request_id |
| `20001` | 任务不存在 | 核对 task_id / run_id |
| `20002` | Token 余额不足/预扣失败 | 充值或稍后重试 |
| `20004` | 计费失败 | 稍后重试,持续失败给 request_id |
| `20010`~`20016` | 输入/输出音频/文本/图片/视频审核未通过 | 更换合规素材 / 调整 prompt,可重试 |
| `20020` | 下游任务执行失败 | 确定性终态;重试仍失败给 request_id |
| `20021` | 下游任务已取消 | 确定性终态 |
| `50000` | 系统内部错误 | 稍后重试 |
| `50001` | 下游任务轮询超时 | 稍后重试 |
---
## 8. Golang 对接骨架(提交 → 轮询)
```go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"yourmod/signer" // §1.3 的签名包
)
const (
submitVideoPath = "/agent_openapi/v1/video/submit"
queryVideoPath = "/agent_openapi/v1/video/query"
)
type Client struct {
signer *signer.Config
hc *http.Client
}
func NewClient(ak, sk string) *Client {
return &Client{
signer: &signer.Config{AccessKey: ak, SecretKey: sk, Host: "https://jimeng.jianying.com"},
hc: &http.Client{Timeout: 30 * time.Second},
}
}
// post 统一发送带签名的 POST 请求
func (c *Client) post(path string, body any, out any) error {
b, err := json.Marshal(body)
if err != nil {
return err
}
hdrs, err := c.signer.Headers(&signer.Request{
Method: "POST",
Path: path,
Body: b,
ContentType: "application/json",
})
if err != nil {
return err
}
req, err := http.NewRequest("POST", c.signer.Host+path, bytes.NewReader(b))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
for k, v := range hdrs {
req.Header.Set(k, v)
}
resp, err := c.hc.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
rb, _ := io.ReadAll(resp.Body)
if err := json.Unmarshal(rb, out); err != nil {
return fmt.Errorf("decode resp: %w, body=%s", err, string(rb))
}
return nil
}
// ---- 提交:带货营销视频 ----
type SubmitResp struct {
Code string `json:"code"`
Message string `json:"message"`
Data struct {
TaskID string `json:"task_id"`
RunID string `json:"run_id"`
ThreadID string `json:"thread_id"`
Status string `json:"status"`
} `json:"data"`
}
func (c *Client) SubmitMarketingVideo(productName string, productImages []string) (*SubmitResp, error) {
req := map[string]any{
"capability_key": "pippit_avatar_marketing_agent",
"action_key": "generate_video",
"run_id": fmt.Sprintf("biz-%d", time.Now().UnixNano()),
"avatar_marketing_input": map[string]any{
"product_name": productName,
"product_image_url_list": productImages,
"model_image_url_list": []string{},
"enable_watermark": false,
},
}
var out SubmitResp
if err := c.post(submitVideoPath, req, &out); err != nil {
return nil, err
}
if out.Code != "0" {
return nil, fmt.Errorf("submit failed code=%s msg=%s", out.Code, out.Message)
}
return &out, nil
}
// ---- 查询 + 轮询 ----
type QueryVideoResp struct {
Code string `json:"code"`
Message string `json:"message"`
Data struct {
Status string `json:"status"`
ErrCode string `json:"err_code"`
ErrMsg string `json:"err_msg"`
VideoArtifact struct {
URL string `json:"url"`
CoverURL string `json:"cover_url"`
Duration float64 `json:"duration"`
Width int64 `json:"width"`
Height int64 `json:"height"`
} `json:"video_artifact"`
} `json:"data"`
}
// PollVideo 提交后轮询直到终态;SUCCESS 返回下载 URL,FAILED 返回错误
func (c *Client) PollVideo(taskID, runID string, timeout time.Duration) (string, error) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
var out QueryVideoResp
body := map[string]string{}
if taskID != "" {
body["task_id"] = taskID
} else {
body["run_id"] = runID
}
if err := c.post(queryVideoPath, body, &out); err != nil {
return "", err
}
switch out.Data.Status {
case "SUCCESS":
return out.Data.VideoArtifact.URL, nil
case "FAILED", "CANCELED":
return "", fmt.Errorf("task %s err=%s msg=%s", out.Data.Status, out.Data.ErrCode, out.Data.ErrMsg)
}
time.Sleep(3 * time.Second)
}
return "", fmt.Errorf("timeout after %v", timeout)
}
```
### 8.1 对接注意点(代码里务必处理)
1. **时钟同步**:本地时间偏差 >300s 会直接 `10006`;部署前校准服务器时钟。
2. **幂等**:`run_id` 自行生成(建议 `biz-<毫秒/纳秒>` 或业务单号);重复提交同 run_id 返回同一任务。
3. **产物时效**:`video_artifact.url` / `novel_data` 里的下载 URL **24h 过期**,成功终态后立即下载落库。
4. **轮询**:提交返回 `QUEUED` 后开始轮询 query,间隔建议 3~5s,注意查询接口限流 20qps。
5. **短剧多阶段**:每阶段是独立 submit+query,从上一阶段 `resp_data` 里取 `OverviewAssetID` / `StoryboardAssetID` 作为下一阶段 `asset_id`,`thread_id` 全程回填。
6. **图片规格**:提交前校验宽高比、尺寸、格式,避免 `10001` 参数非法。
7. **排障**:所有报错都把 `request_id` 记录下来,供反馈排查。