# 即梦 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 | 是 | 商品图 1~3 张 | | `model_image_url_list` | array | 否 | 模特图,最多 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 | 是 | 商品图 1~9 张 | | `model_image_url_list` | array | 否 | 模特图,最多 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` 记录下来,供反馈排查。