jytest/work.md

22 KiB
Raw Blame History

即梦 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 签名实现(标准库,无第三方依赖)

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 通用响应结构

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

{
  "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 对接骨架(提交 → 轮询)

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 记录下来,供反馈排查。