fix(*): 创作工作台 - 修复 Agent、资源引用、附件交互与 Agnes 视频协议

This commit is contained in:
ddCat
2026-08-27 22:12:04 +08:00
parent 0b0d4f0774
commit d42e392ed7
33 changed files with 1218 additions and 239 deletions

View File

@@ -487,11 +487,218 @@ func asyncAudioAdapter() Adapter {
}
func agnesAdapter() Adapter {
info := metadata("agnes-video", "Agnes", "Agnes", CapabilityVideo, "", "", "")
info.Enabled = false
info.Installable = false
info.UnavailableReason = "未提供可验证的官方请求、鉴权、轮询和响应协议资料"
return UnavailableAdapter{Info: info}
info := metadata("agnes-video", "Agnes 视频", "Agnes AI", CapabilityVideo, "POST /v1/videos", "GET /agnesapi?video_id={id}&model_name={model}", "application/json")
info.Description = "Agnes AI 官方异步视频协议,支持 Agnes Video V2.0、2.5 与 2.5 Flash。"
info.RequiresPublicMediaURLs = true
info.Parameters = []Parameter{
{Name: "model", Type: "string", Required: true, Mapping: "model", Description: "官方模型 ID:agnes-video-v2.0、agnes-video-2.5 或 agnes-video-2.5-flash。"},
{Name: "prompt", Type: "string", Required: true, Mapping: "prompt", Description: "视频生成提示词。"},
{Name: "images", Type: "media[]", Mapping: "first_frame/last_frame/images/image/extra_body.image", Description: "2.5 系列按 mode 映射为关键帧或参考图;V2.0 映射为 image 或关键帧数组。"},
{Name: "videos", Type: "media[]", Mapping: "videos", Description: "仅 Agnes Video 2.5 reference 模式支持,Flash 与 V2.0 不支持。"},
{Name: "audios", Type: "media[]", Mapping: "audios", Description: "Agnes Video 2.5 系列 reference 模式支持。"},
{Name: "duration", Type: "integer", Mapping: "seconds/num_frames", Description: "2.5 系列映射为字符串 seconds;V2.0 按帧率换算 num_frames。"},
{Name: "aspectRatio", Type: "string", Mapping: "aspect_ratio", Description: "2.5 系列画幅比例。"},
{Name: "resolution", Type: "string", Mapping: "size", Description: "2.5 支持 720P、960P、2K;Flash 固定 720P。"},
}
return builtinAdapter{
info: info,
create: buildAgnesCreate,
parseCreate: parseAgnesCreate,
poll: func(c PollContext) (RequestSpec, error) {
model := strings.TrimSpace(c.Model)
if model == "" {
model = strings.TrimSpace(c.Request.Model)
}
path := "/agnesapi?video_id=" + url.QueryEscape(c.TaskID)
if model != "" {
path += "&model_name=" + url.QueryEscape(model)
}
return RequestSpec{Method: http.MethodGet, Path: path, OriginPath: true}, nil
},
parsePoll: parseAgnesPoll,
}
}
func buildAgnesCreate(r GenerationRequest) (RequestSpec, error) {
model := strings.TrimSpace(r.Model)
switch model {
case "agnes-video-v2.0":
body, err := buildAgnesV20Body(r)
if err != nil {
return RequestSpec{}, err
}
return jsonSpec(http.MethodPost, "/v1/videos", body), nil
case "agnes-video-2.5", "agnes-video-2.5-flash":
body, err := buildAgnesV25Body(r)
if err != nil {
return RequestSpec{}, err
}
return jsonSpec(http.MethodPost, "/v1/videos", body), nil
default:
return RequestSpec{}, fmt.Errorf("Agnes 不支持模型 %q", model)
}
}
func buildAgnesV25Body(r GenerationRequest) (map[string]any, error) {
mode := firstString(r.Extra, "mode")
if mode == "" {
switch {
case len(r.Videos) > 0 || len(r.Audios) > 0:
mode = "reference"
case len(r.Images) > 0:
mode = "keyframe"
default:
mode = "text"
}
}
if mode != "text" && mode != "keyframe" && mode != "reference" {
return nil, fmt.Errorf("Agnes 2.5 mode 必须是 text、keyframe 或 reference")
}
seconds := defaultInt(r.Duration, 5)
if seconds < 4 || seconds > 12 {
return nil, fmt.Errorf("Agnes 2.5 seconds 必须在 4 到 12 秒之间")
}
size := strings.ToUpper(defaultValue(r.Resolution, firstString(r.Extra, "size")))
if size == "" {
size = "720P"
}
if r.Model == "agnes-video-2.5-flash" {
if size != "720P" {
return nil, fmt.Errorf("Agnes Video 2.5 Flash 仅支持 720P")
}
if len(r.Images) > 5 {
return nil, fmt.Errorf("Agnes Video 2.5 Flash 最多支持 5 张参考图")
}
if len(r.Videos) > 0 {
return nil, fmt.Errorf("Agnes Video 2.5 Flash 不支持参考视频")
}
} else if size != "720P" && size != "960P" && size != "2K" {
return nil, fmt.Errorf("Agnes Video 2.5 size 必须是 720P、960P 或 2K")
}
body := map[string]any{
"model": r.Model,
"prompt": r.Prompt,
"mode": mode,
"seconds": strconv.Itoa(seconds),
"size": size,
"aspect_ratio": defaultValue(r.AspectRatio, firstString(r.Extra, "aspect_ratio")),
"n": 1,
}
if seed, ok := r.Extra["seed"]; ok {
body["seed"] = seed
}
switch mode {
case "text":
if len(r.Images)+len(r.Videos)+len(r.Audios) > 0 {
return nil, fmt.Errorf("Agnes 2.5 text 模式不接受参考媒体")
}
case "keyframe":
if len(r.Images) == 0 || len(r.Images) > 2 || len(r.Videos)+len(r.Audios) > 0 {
return nil, fmt.Errorf("Agnes 2.5 keyframe 模式需要 1 到 2 张图片,且不接受音视频参考")
}
body["first_frame"] = mediaValue(r.Images[0])
if len(r.Images) > 1 {
body["last_frame"] = mediaValue(r.Images[1])
}
case "reference":
if len(r.Images)+len(r.Videos)+len(r.Audios) == 0 {
return nil, fmt.Errorf("Agnes 2.5 reference 模式至少需要一种参考媒体")
}
if len(r.Images) > 0 {
body["images"] = mediaValues(r.Images)
}
if len(r.Audios) > 0 {
body["audios"] = mediaValues(r.Audios)
}
if len(r.Videos) > 0 {
videos := make([]any, 0, len(r.Videos))
for _, video := range r.Videos {
videos = append(videos, map[string]any{"url": mediaValue(video)})
}
body["videos"] = videos
}
}
return compactMap(body), nil
}
func buildAgnesV20Body(r GenerationRequest) (map[string]any, error) {
if len(r.Videos)+len(r.Audios) > 0 {
return nil, fmt.Errorf("Agnes Video V2.0 不支持参考视频或音频")
}
body := map[string]any{"model": r.Model, "prompt": r.Prompt}
mergeExtra(body, r.Extra, "mode", "width", "height", "num_frames", "frame_rate", "num_inference_steps", "seed", "negative_prompt")
if r.Duration > 0 {
frameRate := 24
if value, ok := r.Extra["frame_rate"].(float64); ok && value >= 1 && value <= 60 {
frameRate = int(value)
}
if value, ok := r.Extra["frame_rate"].(int); ok && value >= 1 && value <= 60 {
frameRate = value
}
if _, supplied := r.Extra["frame_rate"]; !supplied {
body["frame_rate"] = frameRate
}
if _, supplied := r.Extra["num_frames"]; !supplied {
target := r.Duration * frameRate
frames := ((target-1+4)/8)*8 + 1
if frames > 441 {
frames = 441
}
body["num_frames"] = frames
}
}
if len(r.Images) == 1 {
body["image"] = mediaValue(r.Images[0])
} else if len(r.Images) > 1 {
extraBody := map[string]any{}
if configured := object(r.Extra["extra_body"]); configured != nil {
for key, value := range configured {
extraBody[key] = value
}
}
extraBody["image"] = mediaValues(r.Images)
extraBody["mode"] = "keyframes"
body["extra_body"] = extraBody
} else if configured := object(r.Extra["extra_body"]); configured != nil {
body["extra_body"] = configured
}
return compactMap(body), nil
}
func parseAgnesCreate(payload map[string]any) (CreateResult, error) {
id := firstString(payload, "video_id", "task_id", "id")
if id == "" {
return CreateResult{}, fmt.Errorf("Agnes 创建响应没有 video_id")
}
status := normalizeStatus(firstString(payload, "status"))
if status == "" {
status = StatusPending
}
return CreateResult{TaskID: id, Status: status}, nil
}
func parseAgnesPoll(c PollContext, payload map[string]any) (PollResult, error) {
id := defaultValue(firstString(payload, "video_id", "task_id", "id"), c.TaskID)
status := normalizeStatus(firstString(payload, "status"))
if status == "" {
status = StatusProcessing
}
message := firstString(payload, "message", "detail")
if failure := object(payload["error"]); failure != nil {
message = defaultValue(firstString(failure, "message", "detail", "code"), message)
}
if status == StatusFailed {
return PollResult{TaskID: id, Status: status, Message: defaultValue(message, "Agnes 视频生成失败")}, nil
}
if status != StatusSucceeded {
return PollResult{TaskID: id, Status: status, Message: message}, nil
}
metadata := object(payload["metadata"])
videoURL := firstString(metadata, "url")
if videoURL == "" {
return PollResult{}, fmt.Errorf("Agnes 完成响应没有 metadata.url")
}
return PollResult{TaskID: id, Status: status, Result: &Result{Videos: []MediaReference{{URL: videoURL, Kind: string(CapabilityVideo)}}}}, nil
}
func videoAdapter(info Metadata, create func(GenerationRequest) (RequestSpec, error)) Adapter {