feat(plugins): 方舟 Agent Plan - 合并 PR #527 图片视频插件并阻止图片误用 asset://

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ddCat
2026-09-18 10:41:19 +08:00
49 changed files with 3263 additions and 74 deletions

View File

@@ -438,7 +438,7 @@ func (s *Service) syncLogicalModelsFromChannelModel(actor *model.User, channelMo
}
func supportsTokenBilling(capability string, protocol model.ChannelInterfaceType) bool {
return capability == "text" || (capability == "video" && protocol == model.ChannelInterfaceVolcengineArkVideo)
return capability == "text" || (capability == "video" && model.IsVolcengineArkVideoProtocol(protocol))
}
func (s *Service) normalizeChannelModelPriceTiers(req ChannelModelRequest, capability string, protocol model.ChannelInterfaceType, fallbackProviderModelKey string) ([]model.ChannelModelPriceTier, error) {

View File

@@ -153,7 +153,7 @@ func DefaultImageCapabilityConfig(protocol string, modelName string) *ImageCapab
image.ResponseFormat = ParameterSupport{Supported: true}
image.OutputFormat = ParameterSupport{Supported: false}
image.MaxOutputs = 1
case model.ChannelInterfaceVolcengineArkImage:
case model.ChannelInterfaceVolcengineArkImage, model.ChannelInterfaceVolcengineArkAgentPlanImage:
image.References.MaskSupported = false
image.Quality.Supported = false
image.TransparentBackground.Supported = false
@@ -229,7 +229,7 @@ func DefaultModelCapabilityConfigForModel(protocol string, modelName string) *Mo
case model.ChannelInterfaceGeminiVeo:
video.Duration = VideoDurationConfig{Selection: "enum", Values: []int{4, 6, 8}, Default: 6}
video.Resolutions = []string{"720p", "1080p"}
case model.ChannelInterfaceVolcengineArkVideo:
case model.ChannelInterfaceVolcengineArkVideo, model.ChannelInterfaceVolcengineArkAgentPlanVideo:
video.Operations = append(video.Operations, "reference_to_video", "audio_to_video")
video.References.MaxVideos, video.References.MaxAudios = 3, 3
video.References.MaxVideoBytes, video.References.MaxAudioBytes = 200*1024*1024, 15*1024*1024
@@ -798,7 +798,7 @@ func validateVideoTask(profile *VideoCapabilityConfig, input canvasGenerationInp
if len(input.ReferenceImages) > profile.References.MaxImages || len(input.ReferenceVideos) > profile.References.MaxVideos || len(input.ReferenceAudios) > profile.References.MaxAudios {
return BadAuthRequest("参考素材数量超过当前模型限制")
}
if input.Config.InterfaceType == string(model.ChannelInterfaceVolcengineArkVideo) && len(input.ReferenceAudios) > 0 && len(input.ReferenceImages) == 0 && len(input.ReferenceVideos) == 0 {
if model.IsVolcengineArkVideoProtocol(model.ChannelInterfaceType(input.Config.InterfaceType)) && len(input.ReferenceAudios) > 0 && len(input.ReferenceImages) == 0 && len(input.ReferenceVideos) == 0 {
return BadAuthRequest("火山方舟全模态参考不支持纯音频或文本+音频,请同时添加参考图片或参考视频")
}
if len(input.ReferenceImages) < profile.References.MinImages {

View File

@@ -14,7 +14,7 @@ func ValidateChannelModelPrice(billingMode string, capability string, protocol m
return unitPrice >= 0
case "token":
if capability == "video" {
return protocol == model.ChannelInterfaceVolcengineArkVideo && inputPrice >= 0 && outputPrice >= 0 && cachedPrice >= 0
return model.IsVolcengineArkVideoProtocol(protocol) && inputPrice >= 0 && outputPrice >= 0 && cachedPrice >= 0
}
if capability != "" && capability != "text" {
return false

View File

@@ -464,7 +464,7 @@ type providerMediaHydrationPolicy struct {
func providerMediaHydrationPolicyFor(ctx context.Context, input canvasGenerationInput) providerMediaHydrationPolicy {
policy := providerMediaHydrationPolicy{preferURL: providerPrefersMediaURLs(input.Config.InterfaceType, input)}
switch strings.TrimSpace(input.Config.InterfaceType) {
case string(model.ChannelInterfaceNewAPIVideo), string(model.ChannelInterfaceNewAPIChannel1), string(model.ChannelInterfaceNewAPIChannel2), string(model.ChannelInterfaceVolcengineArkVideo), string(model.ChannelInterfaceMiniMaxVideo):
case string(model.ChannelInterfaceNewAPIVideo), string(model.ChannelInterfaceNewAPIChannel1), string(model.ChannelInterfaceNewAPIChannel2), string(model.ChannelInterfaceVolcengineArkVideo), string(model.ChannelInterfaceVolcengineArkAgentPlanVideo), string(model.ChannelInterfaceMiniMaxVideo):
policy.requireURL = true
policy.preferURL = true
}
@@ -488,11 +488,11 @@ func providerPrefersMediaURLs(interfaceType string, input canvasGenerationInput)
}
switch strings.TrimSpace(interfaceType) {
case string(model.ChannelInterfaceChatCompletion), string(model.ChannelInterfaceOpenAIResponse), string(model.ChannelInterfaceClaudeAPI),
string(model.ChannelInterfaceGrokImage), string(model.ChannelInterfaceVolcengineArkImage),
string(model.ChannelInterfaceGrokImage), string(model.ChannelInterfaceVolcengineArkImage), string(model.ChannelInterfaceVolcengineArkAgentPlanImage),
string(model.ChannelInterfaceXAIVideo), string(model.ChannelInterfaceNovitaVideo),
string(model.ChannelInterfaceMiniMaxVideo), string(model.ChannelInterfaceNewAPIVideo),
string(model.ChannelInterfaceNewAPIChannel1), string(model.ChannelInterfaceNewAPIChannel2),
string(model.ChannelInterfaceVolcengineArkVideo):
string(model.ChannelInterfaceVolcengineArkVideo), string(model.ChannelInterfaceVolcengineArkAgentPlanVideo):
return true
}
if isGrokVideoConfig(input.Config) || isSeedanceVideoConfig(input.Config) || isArkPlanVideoConfig(input.Config) {

View File

@@ -47,7 +47,8 @@ func withoutProviderAnalytics(ctx context.Context) context.Context {
}
func (s *Service) prepareArkPrivateAssetReferences(ctx context.Context, userID string, input *canvasGenerationInput) error {
if input == nil || !isArkPrivateAssetVideoConfig(input.Config) || !parseBool(input.Config.ArkPrivateAssetUpload, true) {
// 可信素材 asset:// 仅方舟视频协议支持;Agent Plan Seedream 等图片渠道不能上传或改写。
if input == nil || input.Mode != "video" || !isArkPrivateAssetVideoConfig(input.Config) || !parseBool(input.Config.ArkPrivateAssetUpload, true) {
return nil
}
hasOwnedReference := false
@@ -107,7 +108,12 @@ func (s *Service) prepareArkPrivateAssetReferences(ctx context.Context, userID s
}
func isArkPrivateAssetVideoConfig(config providerConfig) bool {
return config.InterfaceType == string(model.ChannelInterfaceVolcengineArkVideo) || isArkPlanVideoConfig(config)
iface := strings.TrimSpace(config.InterfaceType)
// Agent Plan 图片与视频共用 /api/plan/v3;图片协议不得进入可信素材同步。
if iface == string(model.ChannelInterfaceVolcengineArkImage) || iface == "volcengine-ark-agent-plan-image" {
return false
}
return iface == string(model.ChannelInterfaceVolcengineArkVideo) || iface == "volcengine-ark-agent-plan-video" || isArkPlanVideoConfig(config)
}
func arkPrivateAssetAutomaticSyncEnabled(setting arkPrivateAssetSettingValue) (bool, error) {

View File

@@ -172,7 +172,7 @@ func asyncAudioErrorMessage(state map[string]interface{}) string {
func decodeProviderDataURL(value string) (string, []byte, error) {
header, encoded, ok := strings.Cut(value, ",")
if !ok || !strings.HasPrefix(header, "data:") || !strings.HasSuffix(strings.ToLower(header), ";base64") {
return "", nil, errors.New("音频 data URL 格式无效")
return "", nil, errors.New("data URL 格式无效")
}
mimeType := strings.TrimSuffix(strings.TrimPrefix(header, "data:"), ";base64")
data, err := base64.StdEncoding.DecodeString(encoded)

View File

@@ -28,7 +28,7 @@ func runImageTask(ctx context.Context, input canvasGenerationInput) (map[string]
if input.Config.InterfaceType == string(model.ChannelInterfaceVolcengineJiMengImage) {
return runVolcengineJiMengImageTask(ctx, input)
}
if input.Config.InterfaceType == string(model.ChannelInterfaceVolcengineArkImage) {
if model.IsVolcengineArkImageProtocol(model.ChannelInterfaceType(input.Config.InterfaceType)) {
return runVolcengineArkImageTask(ctx, input)
}
if input.Config.InterfaceType == string(model.ChannelInterfaceGeminiImage) {

View File

@@ -194,15 +194,15 @@ func (s *Service) providerCancellationInput(task *model.Task) (canvasGenerationI
}
func supportsProviderCancellation(interfaceType string) bool {
return interfaceType == string(model.ChannelInterfaceGeminiVeo) || interfaceType == string(model.ChannelInterfaceVolcengineArkVideo)
return interfaceType == string(model.ChannelInterfaceGeminiVeo) || model.IsVolcengineArkVideoProtocol(model.ChannelInterfaceType(interfaceType))
}
func cancelProviderTask(ctx context.Context, config providerConfig, providerRequestID string) error {
switch config.InterfaceType {
case string(model.ChannelInterfaceGeminiVeo):
switch {
case config.InterfaceType == string(model.ChannelInterfaceGeminiVeo):
path := "/" + strings.TrimLeft(providerRequestID, "/") + ":cancel"
return postGeminiJSON(ctx, config, path, map[string]any{}, &map[string]any{})
case string(model.ChannelInterfaceVolcengineArkVideo):
case model.IsVolcengineArkVideoProtocol(model.ChannelInterfaceType(config.InterfaceType)):
path := "/contents/generations/tasks/" + url.PathEscape(providerRequestID)
return deleteProviderTask(ctx, config, path)
default:
@@ -228,8 +228,8 @@ func deleteProviderTask(ctx context.Context, config providerConfig, path string)
}
func queryProviderCancellation(ctx context.Context, config providerConfig, providerRequestID string) (providerCancellationOutcome, string, error) {
switch config.InterfaceType {
case string(model.ChannelInterfaceGeminiVeo):
switch {
case config.InterfaceType == string(model.ChannelInterfaceGeminiVeo):
// Gemini 用 operation.error 表示取消终态,此处必须保留该字段再判断,
// 不能让通用 JSON 解包提前把它转换成请求失败。
var operation geminiOperation
@@ -248,7 +248,7 @@ func queryProviderCancellation(ctx context.Context, config providerConfig, provi
return providerCancellationFailed, firstNonEmpty(message, "failed"), nil
}
return providerCancellationSucceeded, "succeeded", nil
case string(model.ChannelInterfaceVolcengineArkVideo):
case model.IsVolcengineArkVideoProtocol(model.ChannelInterfaceType(config.InterfaceType)):
var state map[string]any
if err := getJSON(ctx, config, "/contents/generations/tasks/"+url.PathEscape(providerRequestID), &state); err != nil {
return "", "", err

View File

@@ -2224,6 +2224,48 @@ func TestArkPlanConfigStaysSeparateFromSeedanceVideosEndpoint(t *testing.T) {
}
}
func TestArkPlanImageConfigDoesNotUseVideoAssetPath(t *testing.T) {
imageConfig := providerConfig{
InterfaceType: "volcengine-ark-agent-plan-image",
BaseURL: "https://ark.cn-beijing.volces.com/api/plan/v3",
Model: "doubao-seedream-4-0-250828",
}
if isArkPlanVideoConfig(imageConfig) {
t.Fatal("agent plan image must not match isArkPlanVideoConfig")
}
if isArkPrivateAssetVideoConfig(imageConfig) {
t.Fatal("agent plan image must not trigger ark private asset sync")
}
if isSeedanceVideoConfig(imageConfig) {
t.Fatal("agent plan image must not match isSeedanceVideoConfig")
}
videoConfig := providerConfig{
InterfaceType: "volcengine-ark-agent-plan-video",
BaseURL: "https://ark.cn-beijing.volces.com/api/plan/v3",
Model: "doubao-seedance-1-5-pro-251215",
}
if !isArkPlanVideoConfig(videoConfig) || !isArkPrivateAssetVideoConfig(videoConfig) {
t.Fatal("agent plan video should keep video/asset path")
}
}
func TestPrepareArkPrivateAssetReferencesSkipsImageMode(t *testing.T) {
svc := &Service{}
err := svc.prepareArkPrivateAssetReferences(context.Background(), "user-1", &canvasGenerationInput{
Mode: "image",
Config: providerConfig{
InterfaceType: "volcengine-ark-agent-plan-image",
BaseURL: "https://ark.cn-beijing.volces.com/api/plan/v3",
ArkPrivateAssetUpload: "true",
},
ReferenceImages: []providerMedia{{StorageKey: "resource:res-1", URL: "https://example.com/ref.png"}},
})
if err != nil {
t.Fatalf("prepareArkPrivateAssetReferences() error = %v", err)
}
}
func TestVolcengineArkVideoProtocolUsesContentTaskAndDownloadsResult(t *testing.T) {
t.Setenv("CANVAS_ALLOW_PRIVATE_UPSTREAMS", "true")
paths := make([]string, 0, 3)

View File

@@ -287,7 +287,7 @@ func runSeedanceVideosTask(ctx context.Context, input canvasGenerationInput, pol
func runSeedanceAgentPlanVideoTask(ctx context.Context, input canvasGenerationInput, pollPolicy videoPollPolicy) (map[string]interface{}, error) {
providerName := "Seedance"
if input.Config.InterfaceType == string(model.ChannelInterfaceVolcengineArkVideo) {
if model.IsVolcengineArkVideoProtocol(model.ChannelInterfaceType(input.Config.InterfaceType)) {
providerName = "火山方舟"
}
// Agent Plan 与 Videos API 使用相同的恢复合同:已有任务只查询,不重复创建。
@@ -299,7 +299,7 @@ func runSeedanceAgentPlanVideoTask(ctx context.Context, input canvasGenerationIn
if err != nil {
return nil, err
}
if input.Config.InterfaceType == string(model.ChannelInterfaceVolcengineArkVideo) {
if model.IsVolcengineArkVideoProtocol(model.ChannelInterfaceType(input.Config.InterfaceType)) {
for _, item := range content {
if item["type"] == "image_url" {
item["role"] = "reference_image"

View File

@@ -5,6 +5,8 @@ import (
"strconv"
"strings"
"time"
"infinite-canvas/backend/internal/model"
)
func isPublicMediaURL(value string) bool {
@@ -13,8 +15,8 @@ func isPublicMediaURL(value string) bool {
}
func isSeedanceVideoConfig(config providerConfig) bool {
model := strings.ToLower(config.Model)
return strings.Contains(model, "seedance") || strings.Contains(model, "doubao-seedance") || isArkPlanVideoConfig(config)
modelName := strings.ToLower(config.Model)
return strings.Contains(modelName, "seedance") || strings.Contains(modelName, "doubao-seedance") || isArkPlanVideoConfig(config)
}
func isGrokVideoConfig(config providerConfig) bool {
@@ -22,7 +24,15 @@ func isGrokVideoConfig(config providerConfig) bool {
}
func isArkPlanVideoConfig(config providerConfig) bool {
return strings.Contains(strings.ToLower(config.BaseURL), "/api/plan/v3")
if !strings.Contains(strings.ToLower(config.BaseURL), "/api/plan/v3") {
return false
}
// Agent Plan 图片与视频共用 /api/plan/v3;按协议排除图片,避免 Seedream 误走视频/可信素材路径。
iface := strings.TrimSpace(config.InterfaceType)
if iface == string(model.ChannelInterfaceVolcengineArkImage) || iface == "volcengine-ark-agent-plan-image" {
return false
}
return true
}
func normalizeImageQuality(value string) string {

View File

@@ -70,7 +70,7 @@ func OfficialDeclarativeVideoInterface(interfaceType string) (string, bool) {
return "NewAPI 媒体任务", true
case string(model.ChannelInterfaceXAIVideo):
return "xAI", true
case string(model.ChannelInterfaceVolcengineArkVideo):
case string(model.ChannelInterfaceVolcengineArkVideo), string(model.ChannelInterfaceVolcengineArkAgentPlanVideo):
return "火山方舟", true
case string(model.ChannelInterfaceVolcengineJiMengVideo):
return "即梦", true
@@ -91,6 +91,8 @@ func OfficialDeclarativeImageInterface(interfaceType string) (string, bool) {
return "Grok Images", true
case string(model.ChannelInterfaceVolcengineArkImage):
return "火山方舟图片", true
case string(model.ChannelInterfaceVolcengineArkAgentPlanImage):
return "火山方舟 Agent Plan 图片", true
case string(model.ChannelInterfaceVolcengineJiMengImage):
return "即梦图片", true
default:

View File

@@ -244,11 +244,11 @@ func interfaceAllowsProxyPath(interfaceType model.ChannelInterfaceType, requestP
return requestPath == "/messages"
case model.ChannelInterfaceOpenAIImage, model.ChannelInterfaceGrokImage:
return requestPath == "/images/generations" || requestPath == "/images/edits"
case model.ChannelInterfaceVolcengineArkImage:
case model.ChannelInterfaceVolcengineArkImage, model.ChannelInterfaceVolcengineArkAgentPlanImage:
return requestPath == "/images/generations"
case model.ChannelInterfaceOpenAIAudio:
return requestPath == "/audio/speech"
case model.ChannelInterfaceAsyncAudio, model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo, model.ChannelInterfaceVolcengineArkVideo, model.ChannelInterfaceVolcengineJiMengImage, model.ChannelInterfaceVolcengineJiMengVideo, model.ChannelInterfaceGeminiVeo, model.ChannelInterfaceGeminiImage, model.ChannelInterfaceNovitaVideo, model.ChannelInterfaceMiniMaxVideo:
case model.ChannelInterfaceAsyncAudio, model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo, model.ChannelInterfaceVolcengineArkVideo, model.ChannelInterfaceVolcengineArkAgentPlanVideo, model.ChannelInterfaceVolcengineJiMengImage, model.ChannelInterfaceVolcengineJiMengVideo, model.ChannelInterfaceGeminiVeo, model.ChannelInterfaceGeminiImage, model.ChannelInterfaceNovitaVideo, model.ChannelInterfaceMiniMaxVideo:
return false
default:
return true

View File

@@ -63,16 +63,18 @@ const (
ChannelInterfaceClaudeAPI ChannelInterfaceType = "claude-api"
ChannelInterfaceOpenAIImage ChannelInterfaceType = "openai-image"
ChannelInterfaceGrokImage ChannelInterfaceType = "grok-image"
ChannelInterfaceVolcengineArkImage ChannelInterfaceType = "volcengine-ark-image"
ChannelInterfaceVolcengineJiMengImage ChannelInterfaceType = "volcengine-jimeng-image"
ChannelInterfaceGeminiImage ChannelInterfaceType = "gemini-image"
ChannelInterfaceOpenAIAudio ChannelInterfaceType = "openai-audio"
ChannelInterfaceAsyncAudio ChannelInterfaceType = "async-audio"
ChannelInterfaceNewAPIVideo ChannelInterfaceType = "newapi"
ChannelInterfaceNewAPIChannel1 ChannelInterfaceType = "newapi-channel-1"
ChannelInterfaceNewAPIChannel2 ChannelInterfaceType = "newapi-channel-2"
ChannelInterfaceXAIVideo ChannelInterfaceType = "xai-video"
ChannelInterfaceVolcengineArkVideo ChannelInterfaceType = "volcengine-ark-video"
ChannelInterfaceVolcengineArkImage ChannelInterfaceType = "volcengine-ark-image"
ChannelInterfaceVolcengineArkAgentPlanImage ChannelInterfaceType = "volcengine-ark-agent-plan-image"
ChannelInterfaceVolcengineJiMengImage ChannelInterfaceType = "volcengine-jimeng-image"
ChannelInterfaceGeminiImage ChannelInterfaceType = "gemini-image"
ChannelInterfaceOpenAIAudio ChannelInterfaceType = "openai-audio"
ChannelInterfaceAsyncAudio ChannelInterfaceType = "async-audio"
ChannelInterfaceNewAPIVideo ChannelInterfaceType = "newapi"
ChannelInterfaceNewAPIChannel1 ChannelInterfaceType = "newapi-channel-1"
ChannelInterfaceNewAPIChannel2 ChannelInterfaceType = "newapi-channel-2"
ChannelInterfaceXAIVideo ChannelInterfaceType = "xai-video"
ChannelInterfaceVolcengineArkVideo ChannelInterfaceType = "volcengine-ark-video"
ChannelInterfaceVolcengineArkAgentPlanVideo ChannelInterfaceType = "volcengine-ark-agent-plan-video"
ChannelInterfaceVolcengineJiMengVideo ChannelInterfaceType = "volcengine-jimeng-video"
ChannelInterfaceGeminiVeo ChannelInterfaceType = "gemini-veo"
ChannelInterfaceNovitaVideo ChannelInterfaceType = "novita-video"

View File

@@ -0,0 +1,11 @@
package model
// IsVolcengineArkImageProtocol 识别官方 Ark 图片与 Agent Plan 图片协议。
func IsVolcengineArkImageProtocol(protocol ChannelInterfaceType) bool {
return protocol == ChannelInterfaceVolcengineArkImage || protocol == ChannelInterfaceVolcengineArkAgentPlanImage
}
// IsVolcengineArkVideoProtocol 识别官方 Ark 视频与 Agent Plan 视频协议。
func IsVolcengineArkVideoProtocol(protocol ChannelInterfaceType) bool {
return protocol == ChannelInterfaceVolcengineArkVideo || protocol == ChannelInterfaceVolcengineArkAgentPlanVideo
}

View File

@@ -0,0 +1,24 @@
package model
import "testing"
func TestIsVolcengineArkProtocols(t *testing.T) {
if !IsVolcengineArkImageProtocol(ChannelInterfaceVolcengineArkImage) {
t.Fatal("official ark image should match")
}
if !IsVolcengineArkImageProtocol(ChannelInterfaceVolcengineArkAgentPlanImage) {
t.Fatal("agent plan image should match")
}
if IsVolcengineArkImageProtocol(ChannelInterfaceOpenAIImage) {
t.Fatal("openai image should not match ark image")
}
if !IsVolcengineArkVideoProtocol(ChannelInterfaceVolcengineArkVideo) {
t.Fatal("official ark video should match")
}
if !IsVolcengineArkVideoProtocol(ChannelInterfaceVolcengineArkAgentPlanVideo) {
t.Fatal("agent plan video should match")
}
if IsVolcengineArkVideoProtocol(ChannelInterfaceNewAPIVideo) {
t.Fatal("newapi video should not match ark video")
}
}

View File

@@ -0,0 +1,37 @@
# 火山方舟 Agent Plan 图片生成
本插件面向火山方舟 Agent Plan 专属图片入口。请求体与官方 Ark Seedream 协议一致,但 Base URL 与 API Key 必须来自 Agent Plan 控制台。
## 接口、鉴权与区域
{{OPERATIONS}}
```http
POST {channel_base_url}/api/plan/v3/images/generations
Authorization: Bearer <AGENT_PLAN_API_KEY>
Content-Type: application/json
```
与官方 `/api/v3` 的差异:
| 项 | 官方 Ark | Agent Plan |
| --- | --- | --- |
| Base URL | `https://ark.cn-beijing.volces.com/api/v3` | `https://ark.cn-beijing.volces.com/api/plan/v3` |
| API Key | 推理接入控制台 Key | Agent Plan 专属 Key |
| 计费 | 推理后付费/项目额度 | AFP 套餐抵扣 |
| 请求体 | Seedream images/generations | 同左 |
不要把官方 Key 打到 `/api/plan/v3`,也不要把 Agent Plan Key 打到 `/api/v3`。
## 参数与字段映射
{{PARAMETERS}}
当前实现:`aspectRatio -> size`,所有 `images -> image[]`;`extra` 可透传 `size`、`sequential_image_generation`、`sequential_image_generation_options`、`watermark`。
## 官方资料
- [火山方舟 Agent Plan 文档](https://www.volcengine.com/docs/82379/2375486)
- [火山方舟视觉模型 API 文档](https://www.volcengine.com/docs/82379)
{{CONTRACT}}

View File

@@ -0,0 +1,43 @@
# 火山方舟 Agent Plan 视频任务
本插件使用 Agent Plan 专属 Contents Generations Tasks 异步接口。提示词、参考图、参考视频和参考音频组成 `content[]`;比例、分辨率、时长、生成音频和水印位于顶层任务参数。请求体与官方 Ark Seedance 一致,入口改为 `/api/plan/v3`。
## 接口与鉴权
{{OPERATIONS}}
```http
POST {channel_base_url}/api/plan/v3/contents/generations/tasks
GET {channel_base_url}/api/plan/v3/contents/generations/tasks/{task_id}
DELETE {channel_base_url}/api/plan/v3/contents/generations/tasks/{task_id}
Authorization: Bearer <AGENT_PLAN_API_KEY>
Content-Type: application/json
```
与官方 `/api/v3` 的差异:
| 项 | 官方 Ark | Agent Plan |
| --- | --- | --- |
| Base URL | `https://ark.cn-beijing.volces.com/api/v3` | `https://ark.cn-beijing.volces.com/api/plan/v3` |
| API Key | 推理接入控制台 Key | Agent Plan 专属 Key |
| 计费 | 推理后付费/项目额度 | AFP 套餐抵扣 |
| 请求体 | contents/generations/tasks | 同左 |
## 参数与字段映射
{{PARAMETERS}}
全模态参考的素材映射如下:
- 图片:`{"type":"image_url","image_url":{"url":"..."},"role":"reference_image"}`
- 视频:`{"type":"video_url","video_url":{"url":"..."},"role":"reference_video"}`
- 音频:`{"type":"audio_url","audio_url":{"url":"..."},"role":"reference_audio"}`
单次最多发送 9 张图片、3 个视频和 3 个音频。参考素材可以组合使用,但上游不支持纯音频或“文本 + 音频”。
## 官方资料
- [火山方舟 Agent Plan 文档](https://www.volcengine.com/docs/82379/2375486)
- [火山方舟内容生成任务文档](https://www.volcengine.com/docs/82379/1520757)
{{CONTRACT}}

View File

@@ -711,6 +711,8 @@ func mediaReferencesFromManifestValue(value any, kind string, ephemeral bool) []
if reference.DataURL == "" {
reference.DataURL = firstString(typed, "data_url", "b64_json")
}
// OpenAI / Ark 等渠道常直接返回裸 b64_json;声明式结果下载要求 data URL。
reference.DataURL = normalizeManifestInlineDataURL(reference.DataURL, reference.Kind, firstString(typed, "output_format", "mime_type", "mimeType"))
if reference.URL != "" || reference.DataURL != "" {
result = append(result, reference)
}
@@ -719,6 +721,50 @@ func mediaReferencesFromManifestValue(value any, kind string, ephemeral bool) []
return result
}
func normalizeManifestInlineDataURL(value, kind, formatHint string) string {
value = strings.TrimSpace(value)
if value == "" || strings.HasPrefix(value, "data:") {
return value
}
return "data:" + manifestInlineMediaMIME(kind, formatHint) + ";base64," + value
}
func manifestInlineMediaMIME(kind, formatHint string) string {
hint := strings.ToLower(strings.TrimSpace(formatHint))
switch hint {
case "image/png", "image/jpeg", "image/webp", "image/gif", "audio/mpeg", "audio/wav", "audio/ogg", "video/mp4", "video/webm":
return hint
case "image/jpg":
return "image/jpeg"
case "audio/mp3":
return "audio/mpeg"
case "png":
return "image/png"
case "jpeg", "jpg":
return "image/jpeg"
case "webp":
return "image/webp"
case "gif":
return "image/gif"
case "mp3", "mpeg":
return "audio/mpeg"
case "wav":
return "audio/wav"
case "mp4":
return "video/mp4"
case "webm":
return "video/webm"
}
switch strings.ToLower(strings.TrimSpace(kind)) {
case "audio":
return "audio/mpeg"
case "video":
return "video/mp4"
default:
return "image/png"
}
}
var (
manifestMDImageRegex = regexp.MustCompile(`!\[[^\]]*\]\(([^)\s]+)\)`)
manifestHTMLVideoRegex = regexp.MustCompile(`(?i)<video[^>]+src=['"]([^'"]+)['"]`)

View File

@@ -599,6 +599,36 @@ func TestOfficialArkSeedreamMapsAspectRatioToPixelSize(t *testing.T) {
}
}
func TestOfficialArkAgentPlanPluginsUsePlanPaths(t *testing.T) {
image := officialPackageAdapter(t, "volcengine-ark-agent-plan-seedream.yingce-plugin", "volcengine-ark-agent-plan-image")
imageCreate, err := image.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{Model: "doubao-seedream-5-0-260128", Prompt: "circle", AspectRatio: "1:1"}})
if err != nil {
t.Fatal(err)
}
if imageCreate.Path != "/api/plan/v3/images/generations" {
t.Fatalf("agent plan image create = %#v", imageCreate)
}
if body := manifestTestBody(t, imageCreate); body["size"] != "2048x2048" {
t.Fatalf("agent plan image size = %#v", body["size"])
}
video := officialPackageAdapter(t, "volcengine-ark-agent-plan-seedance.yingce-plugin", "volcengine-ark-agent-plan-video")
videoCreate, err := video.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{Model: "doubao-seedance-2-0-260128", Prompt: "walk", AspectRatio: "16:9", Resolution: "720p", Duration: 5}})
if err != nil {
t.Fatal(err)
}
if videoCreate.Path != "/api/plan/v3/contents/generations/tasks" {
t.Fatalf("agent plan video create = %#v", videoCreate)
}
poll, err := video.BuildPoll(context.Background(), PollContext{TaskID: "task-1", Model: "doubao-seedance-2-0-260128"})
if err != nil {
t.Fatal(err)
}
if poll.Path != "/api/plan/v3/contents/generations/tasks/task-1" {
t.Fatalf("agent plan video poll = %#v", poll)
}
}
func TestOfficialGeminiImageMapsQualityToImageSize(t *testing.T) {
adapter := officialPackageAdapter(t, "google-gemini-image.yingce-plugin", "gemini-image")
tests := []struct {
@@ -754,3 +784,27 @@ func TestOfficialOpenAIAudioSpeedDefaultsInvalidAndZeroValues(t *testing.T) {
})
}
}
func TestOfficialArkSeedreamParsesB64JSONAsDataURL(t *testing.T) {
for _, tc := range []struct {
packageName, providerID string
}{
{"volcengine-ark-seedream.yingce-plugin", "volcengine-ark-image"},
{"volcengine-ark-agent-plan-seedream.yingce-plugin", "volcengine-ark-agent-plan-image"},
} {
t.Run(tc.providerID, func(t *testing.T) {
adapter := officialPackageAdapter(t, tc.packageName, tc.providerID)
result, err := adapter.ParseCreate(context.Background(), []byte(`{"created":1,"data":[{"b64_json":"aW1hZ2U=","output_format":"jpeg","size":"1824x1024"}]}`))
if err != nil {
t.Fatal(err)
}
if result.Result == nil || len(result.Result.Images) != 1 {
t.Fatalf("result = %#v", result.Result)
}
dataURL := result.Result.Images[0].DataURL
if dataURL != "data:image/jpeg;base64,aW1hZ2U=" {
t.Fatalf("DataURL = %q, want jpeg data URL from b64_json + output_format", dataURL)
}
})
}
}

View File

@@ -17,6 +17,29 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
- [ ] 用 Excel/WPS 验收中文、含逗号/引号/换行名称、长数字及前导零订单号、微积分和公式触发文本。数字订单号的单引号保护前缀属于预期;不要按数值列导入。
- [ ] 检查明暗主题、窄屏筛选换行、键盘操作及导出 loading;本清单中的人工验收尚未由用户确认。
## 火山方舟 Agent Plan 图片 / 视频插件
- 新增官方协议插件:`volcengine-ark-agent-plan-seedream`(provider `volcengine-ark-agent-plan-image`)与 `volcengine-ark-agent-plan-seedance`(provider `volcengine-ark-agent-plan-video`)。
- 与官方 Ark 插件的差异只在接入面:默认 Base URL 为 `https://ark.cn-beijing.volces.com/api/plan/v3`,创建路径分别为 `/api/plan/v3/images/generations` 与 `/api/plan/v3/contents/generations/tasks`;请求体、参考素材 role、能力档位与官方 Seedream/Seedance 对齐。必须使用 Agent Plan 专属 API Key,不可与 `/api/v3` 官方 Key 混用。
- 宿主已把 Agent Plan 协议纳入与官方 Ark 相同的能力默认值、Token 计费校验、方舟素材引用、取消任务与前端 payload 分支。
- 待管理端安装这两个 `.yingce-plugin` 后:分别创建图片/视频渠道,确认默认 Base URL 为 `/api/plan/v3`;用 Agent Plan Key 文生图、参考图生图、Seedance 文生视频与全模态参考成功;故意混用官方 Key 应失败;官方 `volcengine-ark-*` 渠道行为不受影响。
## Seedream 声明式结果裸 b64_json(2026-09-17)
根因:`volcengine-ark-seedream` / `volcengine-ark-agent-plan-seedream` 响应把 `response.data` 原样映射为 images;解析回退把裸 `b64_json` 写入 `DataURL` 却未加 `data:` 前缀,随后 `decodeProviderDataURL` 失败。UI 报「声明式协议媒体结果下载失败:音频 data URL 格式无效」(旧错误文案误写「音频」),但管理端请求明细显示上游 HTTP 200 且已返回内嵌编码。修复:声明式媒体解析对裸 base64 按 `output_format`/kind 补全 data URL;错误文案改为通用「data URL 格式无效」。协议单测已覆盖。需重建后端容器后浏览器验收:
- [ ] Agent Plan / 官方 Seedream 图片渠道(`response_format=b64_json`)文生图、参考图生图成功落盘,不再出现「媒体结果下载失败」。
- [ ] 上游返回 `output_format=jpeg` 时结果可正常预览。
- [ ] 管理端该任务对应 create 日志仍为 succeeded / 200,与前端成功态一致。
## Agent Plan Seedream 误走可信素材(2026-09-17)
根因:`isArkPlanVideoConfig` / 前端 `usesArkVideoAssetReference` 仅按 Base URL 含 `/api/plan/v3` 判断,Agent Plan Seedream 与 Seedance 共用该路径,图片生成被误判为视频可信素材路径,自动 CreateAsset 并把参考图改成 `asset://`,上游图片接口拒绝。修复:后端仅 `mode=video` 且非图片协议时同步可信素材;前端仅视频 mode + 视频协议(或非图片协议的 Plan BaseURL)才提交 `asset://`。后端单测已覆盖。需浏览器验收:
- [ ] Agent Plan Seedream / 任意 Base URL 为 `/api/plan/v3` 的图片渠道带参考图生成:任务进度无「同步方舟可信素材」,请求 `image` 为公网 URL 或 data URL,不是 `asset://`。
- [ ] 同一张已录入方舟素材 ID 的参考图用于 Agent Plan / 方舟 Seedance 视频生成时,仍可走 `asset://` 并跳过重复上传。
- [ ] 官方 `volcengine-ark-image` / `volcengine-ark-video` 行为不变。
## 画布保存保护与版本记录
已增加版本 CAS、冲突草稿、视口与内容分离、打开时核对云端、历史快照和恢复,以及右侧历史树和独立只读预览。上游云端 Agent 与创作任务使用同一版本和历史事务。专项测试覆盖并发旧写入、断档增量、草稿写入失败、恢复冲突、历史权限、保留数量和历史媒体删除保护。Schema 顺延为 v23,不占用已发布的 v16 Agent 记忆版本。

View File

@@ -435,6 +435,8 @@ if model name contains minimax/h3
| `openai-videos` | multipart create/task/content/cancel |
| `xai-video` | xAI JSON video + reference_images |
| `volcengine-ark-seedance` | Ark contents task + multimodal roles |
| `volcengine-ark-agent-plan-seedream` | Agent Plan `/api/plan/v3/images/generations` |
| `volcengine-ark-agent-plan-seedance` | Agent Plan `/api/plan/v3/contents/generations/tasks` |
| `minimax-hailuo-video-v2` | MiniMax `/v2/video_generation` |
| `dashscope-wan-video` | DashScope async Wan task |
| `newapi-media-task-v1` | NewAPI Channel 1 包装 |

View File

@@ -374,27 +374,123 @@ const arkSeedreamRatioSizes = [
["3:2", "2496x1664"], ["2:3", "1664x2496"], ["21:9", "3024x1296"]
];
const arkSeedreamImageBody = (optionNamespace) => ({
model: ref("request.model"), prompt: ref("request.prompt"),
// Ark 的 size 只接受 WIDTHxHEIGHT 或 2k/3k/4k;统一层的比例值在这里换算成 2K 档像素尺寸,像素或档位值原样透传。
size: omit({ $switch: { cases: arkSeedreamRatioSizes.map(([ratio, size]) => ({ when: eq(ref("request.aspectRatio"), ratio), then: size })), default: ref("request.aspectRatio") } }),
image: omit(conditional(
eq(len(ref("request.images")), 1),
first(map(ref("request.images"), "media", ref("media.value"))),
conditional(gt(len(ref("request.images")), 1), map(ref("request.images"), "media", ref("media.value")), null)
)),
sequential_image_generation: omit(ref(`request.providerOptions.${optionNamespace}.sequential_image_generation`)),
sequential_image_generation_options: omit(ref(`request.providerOptions.${optionNamespace}.sequential_image_generation_options`)),
watermark: coalesce(ref(`request.providerOptions.${optionNamespace}.watermark`), ref("request.watermark"), false),
seed: omit(ref(`request.providerOptions.${optionNamespace}.seed`)),
response_format: coalesce(ref(`request.providerOptions.${optionNamespace}.response_format`), "b64_json")
});
add({
id: "volcengine-ark-seedream", providerId: "volcengine-ark-image", name: "Volcengine Ark Seedream Images", vendor: "Volcengine", capability: "image",
baseUrl: "https://ark.cn-beijing.volces.com", auth: bearer, params: imageParams,
create: jsonCreate("/api/v3/images/generations", {
model: ref("request.model"), prompt: ref("request.prompt"),
// Ark 的 size 只接受 WIDTHxHEIGHT 或 2k/3k/4k;统一层的比例值在这里换算成 2K 档像素尺寸,像素或档位值原样透传。
size: omit({ $switch: { cases: arkSeedreamRatioSizes.map(([ratio, size]) => ({ when: eq(ref("request.aspectRatio"), ratio), then: size })), default: ref("request.aspectRatio") } }),
image: omit(conditional(
eq(len(ref("request.images")), 1),
first(map(ref("request.images"), "media", ref("media.value"))),
conditional(gt(len(ref("request.images")), 1), map(ref("request.images"), "media", ref("media.value")), null)
)),
sequential_image_generation: omit(ref("request.providerOptions.volcengine-ark-image.sequential_image_generation")),
sequential_image_generation_options: omit(ref("request.providerOptions.volcengine-ark-image.sequential_image_generation_options")),
watermark: coalesce(ref("request.providerOptions.volcengine-ark-image.watermark"), ref("request.watermark"), false),
seed: omit(ref("request.providerOptions.volcengine-ark-image.seed")),
response_format: coalesce(ref("request.providerOptions.volcengine-ark-image.response_format"), "b64_json")
}),
notes: "官方 Ark 推理接入:Base URL 使用 /api/v3,API Key 来自方舟推理接入控制台,不可与 Agent Plan 专属 Key 混用。",
create: jsonCreate("/api/v3/images/generations", arkSeedreamImageBody("volcengine-ark-image")),
response: { status: "succeeded", images: ref("response.data"), usage: ref("response.usage"), errorPaths: ["error.code"], messagePaths: ["error.message"] }
});
add({
id: "volcengine-ark-agent-plan-seedream", providerId: "volcengine-ark-agent-plan-image", name: "Volcengine Ark Agent Plan Seedream Images", vendor: "Volcengine", capability: "image",
baseUrl: "https://ark.cn-beijing.volces.com", auth: bearer, params: imageParams,
notes: "Agent Plan 专属接入:请求路径为 /api/plan/v3/images/generations,必须使用 Agent Plan 控制台专属 API Key;请求体与官方 Seedream 协议一致,但凭证与额度按 AFP 套餐结算,不能与 /api/v3 官方 Key 混用。",
create: jsonCreate("/api/plan/v3/images/generations", arkSeedreamImageBody("volcengine-ark-agent-plan-image")),
response: { status: "succeeded", images: ref("response.data"), usage: ref("response.usage"), errorPaths: ["error.code"], messagePaths: ["error.message"] }
});
const arkSeedanceValidations = [
{ assert: { $lte: [len(ref("request.images")), 9] }, message: "Seedance 最多支持 9 张参考图片" },
{ assert: { $lte: [len(ref("request.videos")), 3] }, message: "Seedance 最多支持 3 个参考视频" },
{ assert: { $lte: [len(ref("request.audios")), 3] }, message: "Seedance 最多支持 3 个参考音频" },
{ assert: { $or: [eq(len(ref("request.audios")), 0), gt({ $add: [len(ref("request.images")), len(ref("request.videos"))] }, 0)] }, message: "Seedance 不支持纯音频或文本+音频,请同时添加参考图片或参考视频" }
];
const arkSeedanceParams = [
["model", "string", true, "model", "Ark endpoint/model ID。"],
["prompt", "string", true, "content[type=text].text", "视频提示词。"],
["images", "media[]", false, "content[type=image_url]", "first_frame、last_frame、reference_image 等 role 原样映射。"],
["videos", "media[]", false, "content[type=video_url]", "reference_video。"],
["audios", "media[]", false, "content[type=audio_url]", "reference_audio/reference_voice。"],
["aspectRatio", "string", false, "ratio", "输出画幅。"],
["resolution", "string", false, "resolution", "输出分辨率档位。"],
["duration", "integer", false, "duration", "输出时长秒数。"],
["generateAudio", "boolean", false, "generate_audio", "是否生成音频。"],
["watermark", "boolean", false, "watermark", "是否带水印。"],
["seed", "integer", false, "seed", "providerOptions seed。"],
["camera_fixed", "boolean", false, "camera_fixed", "providerOptions camera_fixed。"]
];
const arkSeedanceBody = (optionNamespace) => ({
model: ref("request.model"),
content: {
$concatArrays: [
[{ type: "text", text: ref("request.prompt") }],
map(sorted(ref("request.images")), "media", {
type: "image_url",
image_url: { url: ref("media.value") },
role: coalesce(ref("media.role"), "reference_image")
}),
map(sorted(ref("request.videos")), "media", {
type: "video_url",
video_url: { url: ref("media.value") },
role: coalesce(ref("media.role"), "reference_video")
}),
map(sorted(ref("request.audios")), "media", {
type: "audio_url",
audio_url: { url: ref("media.value") },
role: coalesce(ref("media.role"), "reference_audio")
})
]
},
ratio: coalesce(ref("request.aspectRatio"), "16:9"),
resolution: coalesce(ref("request.resolution"), "720p"),
duration: conditional(gt(ref("request.duration"), 0), ref("request.duration"), 5),
generate_audio: ref("request.generateAudio"),
watermark: ref("request.watermark"),
seed: omit(ref(`request.providerOptions.${optionNamespace}.seed`)),
camera_fixed: omit(ref(`request.providerOptions.${optionNamespace}.camera_fixed`))
});
const arkSeedanceResponse = {
taskId: coalesce(ref("response.id"), ref("response.task_id"), ref("response.data.id"), ref("taskId")),
status: coalesce(ref("response.status"), ref("response.data.status"), "pending"),
message: coalesce(ref("response.error.message"), ref("response.message"), ref("response.fail_reason")),
videos: coalesce(ref("response.content.video_url"), ref("response.video_url"), ref("response.output.video_url"), ref("response.data.video_url")),
usage: ref("response.usage"),
errorPaths: ["error.code"],
resultEphemeral: true
};
add({
id: "volcengine-ark-seedance", providerId: "volcengine-ark-video", name: "Volcengine Ark Seedance", vendor: "Volcengine", capability: "video",
baseUrl: "https://ark.cn-beijing.volces.com", auth: bearer, params: arkSeedanceParams, requiresPublicMediaUrls: true,
validations: arkSeedanceValidations,
notes: "官方 Ark 推理接入:创建/查询/取消走 /api/v3/contents/generations/tasks;插件不根据图片下标推断首尾帧,role 由业务层确定。API Key 来自方舟推理接入控制台。",
create: jsonCreate("/api/v3/contents/generations/tasks", arkSeedanceBody("volcengine-ark-video")),
poll: { method: "GET", path: "/api/v3/contents/generations/tasks/{{taskId}}" },
cancel: { method: "DELETE", path: "/api/v3/contents/generations/tasks/{{taskId}}" },
response: arkSeedanceResponse
});
add({
id: "volcengine-ark-agent-plan-seedance", providerId: "volcengine-ark-agent-plan-video", name: "Volcengine Ark Agent Plan Seedance", vendor: "Volcengine", capability: "video",
baseUrl: "https://ark.cn-beijing.volces.com", auth: bearer, params: arkSeedanceParams, requiresPublicMediaUrls: true,
validations: arkSeedanceValidations,
notes: "Agent Plan 专属接入:创建/查询/取消走 /api/plan/v3/contents/generations/tasks;请求体与官方 Seedance 协议一致,但必须使用 Agent Plan 专属 API Key 与 AFP 额度,不能与 /api/v3 官方 Key 混用。",
create: jsonCreate("/api/plan/v3/contents/generations/tasks", arkSeedanceBody("volcengine-ark-agent-plan-video")),
poll: { method: "GET", path: "/api/plan/v3/contents/generations/tasks/{{taskId}}" },
cancel: { method: "DELETE", path: "/api/plan/v3/contents/generations/tasks/{{taskId}}" },
response: arkSeedanceResponse
});
// Gemini imageConfig.imageSize 只接受 1K/2K/4K;画布统一层用 1k/2k/4k 或 low/medium/high。
const geminiImageSize = omit({
$coalesce: [

View File

@@ -0,0 +1,5 @@
# Volcengine Ark Agent Plan Seedance
该目录是独立官方协议插件源码。后端从生成的 `volcengine-ark-agent-plan-seedance.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。
完整接口见 [docs/interface.md](docs/interface.md)。

View File

@@ -0,0 +1,527 @@
# Volcengine Ark Agent Plan Seedance 接口字段
## 协议身份
- 插件 ID:`volcengine-ark-agent-plan-seedance`。
- Provider ID:`volcengine-ark-agent-plan-video`。
- 能力:`video`。
- 默认 Base URL:`https://ark.cn-beijing.volces.com`。
- 鉴权驱动:`bearer`。
- 创建:`POST /api/plan/v3/contents/generations/tasks`。
- 查询:`GET /api/plan/v3/contents/generations/tasks/{{taskId}}`。
- 取消:`DELETE /api/plan/v3/contents/generations/tasks/{{taskId}}`。
## 配置字段
| 字段 | 类型 | 必填 | 含义 |
| --- | --- | --- | --- |
| `apiKey` | secret | 是 | API Key |
## 统一字段映射
| 统一字段 | 类型 | 必填 | 上游映射 | 说明 |
| --- | --- | --- | --- | --- |
| `model` | string | 是 | `model` | Ark endpoint/model ID。 |
| `prompt` | string | 是 | `content[type=text].text` | 视频提示词。 |
| `images` | media[] | 否 | `content[type=image_url]` | first_frame、last_frame、reference_image 等 role 原样映射。 |
| `videos` | media[] | 否 | `content[type=video_url]` | reference_video。 |
| `audios` | media[] | 否 | `content[type=audio_url]` | reference_audio/reference_voice。 |
| `aspectRatio` | string | 否 | `ratio` | 输出画幅。 |
| `resolution` | string | 否 | `resolution` | 输出分辨率档位。 |
| `duration` | integer | 否 | `duration` | 输出时长秒数。 |
| `generateAudio` | boolean | 否 | `generate_audio` | 是否生成音频。 |
| `watermark` | boolean | 否 | `watermark` | 是否带水印。 |
| `seed` | integer | 否 | `seed` | providerOptions seed。 |
| `camera_fixed` | boolean | 否 | `camera_fixed` | providerOptions camera_fixed。 |
## 上游请求模板逐字段清单
下表由插件请求模板生成,覆盖 body、query、headers 和 multipart 文件声明中的每个字段。
| 上游位置 | 值或转换表达式 |
| --- | --- |
| `create.method` | `"POST"` |
| `create.path` | `"/api/plan/v3/contents/generations/tasks"` |
| `create.contentType` | `"application/json"` |
| `create.body.model` | `{"$ref":"request.model"}` |
| `create.body.content` | `{"$concatArrays":[[{"type":"text","text":{"$ref":"request.prompt"}}],{"$map":{"from":{"$sortByOrder":{"$ref":"request.images"}},"as":"media","in":{"type":"image_url","image_url":{"url":{"$ref":"media.value"}},"role":{"$coalesce":[{"$ref":"media.role"},"reference_image"]}}}},{"$map":{"from":{"$sortByOrder":{"$ref":"request.videos"}},"as":"media","in":{"type":"video_url","video_url":{"url":{"$ref":"media.value"}},"role":{"$coalesce":[{"$ref":"media.role"},"reference_video"]}}}},{"$map":{"from":{"$sortByOrder":{"$ref":"request.audios"}},"as":"media","in":{"type":"audio_url","audio_url":{"url":{"$ref":"media.value"}},"role":{"$coalesce":[{"$ref":"media.role"},"reference_audio"]}}}}]}` |
| `create.body.ratio` | `{"$coalesce":[{"$ref":"request.aspectRatio"},"16:9"]}` |
| `create.body.resolution` | `{"$coalesce":[{"$ref":"request.resolution"},"720p"]}` |
| `create.body.duration` | `{"$if":{"condition":{"$gt":[{"$ref":"request.duration"},0]},"then":{"$ref":"request.duration"},"else":5}}` |
| `create.body.generate_audio` | `{"$ref":"request.generateAudio"}` |
| `create.body.watermark` | `{"$ref":"request.watermark"}` |
| `create.body.seed` | `{"$omitEmpty":{"$ref":"request.providerOptions.volcengine-ark-agent-plan-video.seed"}}` |
| `create.body.camera_fixed` | `{"$omitEmpty":{"$ref":"request.providerOptions.volcengine-ark-agent-plan-video.camera_fixed"}}` |
| `poll.method` | `"GET"` |
| `poll.path` | `"/api/plan/v3/contents/generations/tasks/{{taskId}}"` |
| `poll.contentType` | `"application/json"` |
| `cancel.method` | `"DELETE"` |
| `cancel.path` | `"/api/plan/v3/contents/generations/tasks/{{taskId}}"` |
| `cancel.contentType` | `"application/json"` |
## Provider 扩展键
- `providerOptions.volcengine-ark-agent-plan-video.camera_fixed`
- `providerOptions.volcengine-ark-agent-plan-video.seed`
动态模型或工作流允许使用文档声明的完整 `parameters/input/extra_body` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。
## 响应映射逐字段清单
| 映射位置 | 上游路径或转换表达式 |
| --- | --- |
| `response.taskId` | `{"$coalesce":[{"$ref":"response.id"},{"$ref":"response.task_id"},{"$ref":"response.data.id"},{"$ref":"taskId"}]}` |
| `response.status` | `{"$coalesce":[{"$ref":"response.status"},{"$ref":"response.data.status"},"pending"]}` |
| `response.message` | `{"$coalesce":[{"$ref":"response.error.message"},{"$ref":"response.message"},{"$ref":"response.fail_reason"}]}` |
| `response.videos` | `{"$coalesce":[{"$ref":"response.content.video_url"},{"$ref":"response.video_url"},{"$ref":"response.output.video_url"},{"$ref":"response.data.video_url"}]}` |
| `response.usage` | `{"$ref":"response.usage"}` |
| `response.errorPaths[0]` | `"error.code"` |
| `response.resultEphemeral` | `true` |
## 响应与错误
插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。
## 兼容边界
Agent Plan 专属接入:创建/查询/取消走 /api/plan/v3/contents/generations/tasks;请求体与官方 Seedance 协议一致,但必须使用 Agent Plan 专属 API Key 与 AFP 额度,不能与 /api/v3 官方 Key 混用。
<!-- YINGCE_MANIFEST_CONTRACT_START -->
## Manifest 完整接口定义
以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。
```json
{
"apiVersion": "yingce.plugin/v2",
"id": "volcengine-ark-agent-plan-seedance",
"name": "Volcengine Ark Agent Plan Seedance",
"version": "2.0.0",
"author": "Volcengine / 影策",
"description": "Volcengine Ark Agent Plan Seedance 独立请求协议插件。",
"documentation": "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>",
"permissions": [
"generation.run",
"media.read"
],
"configuration": {
"fields": [
{
"name": "apiKey",
"type": "secret",
"label": "API Key",
"required": true
}
]
},
"contributes": {
"providers": [
{
"id": "volcengine-ark-agent-plan-video",
"label": "Volcengine Ark Agent Plan Seedance",
"capabilities": [
"video"
],
"scopes": [
"admin.system-channel",
"user.custom-channel",
"canvas",
"creation",
"agent"
],
"baseUrl": "https://ark.cn-beijing.volces.com",
"requiresPublicMediaUrls": true,
"auth": {
"type": "bearer",
"field": "apiKey"
},
"parameters": [
{
"name": "model",
"type": "string",
"required": true,
"mapping": "model",
"description": "Ark endpoint/model ID。"
},
{
"name": "prompt",
"type": "string",
"required": true,
"mapping": "content[type=text].text",
"description": "视频提示词。"
},
{
"name": "images",
"type": "media[]",
"required": false,
"mapping": "content[type=image_url]",
"description": "first_frame、last_frame、reference_image 等 role 原样映射。"
},
{
"name": "videos",
"type": "media[]",
"required": false,
"mapping": "content[type=video_url]",
"description": "reference_video。"
},
{
"name": "audios",
"type": "media[]",
"required": false,
"mapping": "content[type=audio_url]",
"description": "reference_audio/reference_voice。"
},
{
"name": "aspectRatio",
"type": "string",
"required": false,
"mapping": "ratio",
"description": "输出画幅。"
},
{
"name": "resolution",
"type": "string",
"required": false,
"mapping": "resolution",
"description": "输出分辨率档位。"
},
{
"name": "duration",
"type": "integer",
"required": false,
"mapping": "duration",
"description": "输出时长秒数。"
},
{
"name": "generateAudio",
"type": "boolean",
"required": false,
"mapping": "generate_audio",
"description": "是否生成音频。"
},
{
"name": "watermark",
"type": "boolean",
"required": false,
"mapping": "watermark",
"description": "是否带水印。"
},
{
"name": "seed",
"type": "integer",
"required": false,
"mapping": "seed",
"description": "providerOptions seed。"
},
{
"name": "camera_fixed",
"type": "boolean",
"required": false,
"mapping": "camera_fixed",
"description": "providerOptions camera_fixed。"
}
],
"validations": [
{
"assert": {
"$lte": [
{
"$len": {
"$ref": "request.images"
}
},
9
]
},
"message": "Seedance 最多支持 9 张参考图片"
},
{
"assert": {
"$lte": [
{
"$len": {
"$ref": "request.videos"
}
},
3
]
},
"message": "Seedance 最多支持 3 个参考视频"
},
{
"assert": {
"$lte": [
{
"$len": {
"$ref": "request.audios"
}
},
3
]
},
"message": "Seedance 最多支持 3 个参考音频"
},
{
"assert": {
"$or": [
{
"$eq": [
{
"$len": {
"$ref": "request.audios"
}
},
0
]
},
{
"$gt": [
{
"$add": [
{
"$len": {
"$ref": "request.images"
}
},
{
"$len": {
"$ref": "request.videos"
}
}
]
},
0
]
}
]
},
"message": "Seedance 不支持纯音频或文本+音频,请同时添加参考图片或参考视频"
}
],
"create": {
"method": "POST",
"path": "/api/plan/v3/contents/generations/tasks",
"contentType": "application/json",
"body": {
"model": {
"$ref": "request.model"
},
"content": {
"$concatArrays": [
[
{
"type": "text",
"text": {
"$ref": "request.prompt"
}
}
],
{
"$map": {
"from": {
"$sortByOrder": {
"$ref": "request.images"
}
},
"as": "media",
"in": {
"type": "image_url",
"image_url": {
"url": {
"$ref": "media.value"
}
},
"role": {
"$coalesce": [
{
"$ref": "media.role"
},
"reference_image"
]
}
}
}
},
{
"$map": {
"from": {
"$sortByOrder": {
"$ref": "request.videos"
}
},
"as": "media",
"in": {
"type": "video_url",
"video_url": {
"url": {
"$ref": "media.value"
}
},
"role": {
"$coalesce": [
{
"$ref": "media.role"
},
"reference_video"
]
}
}
}
},
{
"$map": {
"from": {
"$sortByOrder": {
"$ref": "request.audios"
}
},
"as": "media",
"in": {
"type": "audio_url",
"audio_url": {
"url": {
"$ref": "media.value"
}
},
"role": {
"$coalesce": [
{
"$ref": "media.role"
},
"reference_audio"
]
}
}
}
}
]
},
"ratio": {
"$coalesce": [
{
"$ref": "request.aspectRatio"
},
"16:9"
]
},
"resolution": {
"$coalesce": [
{
"$ref": "request.resolution"
},
"720p"
]
},
"duration": {
"$if": {
"condition": {
"$gt": [
{
"$ref": "request.duration"
},
0
]
},
"then": {
"$ref": "request.duration"
},
"else": 5
}
},
"generate_audio": {
"$ref": "request.generateAudio"
},
"watermark": {
"$ref": "request.watermark"
},
"seed": {
"$omitEmpty": {
"$ref": "request.providerOptions.volcengine-ark-agent-plan-video.seed"
}
},
"camera_fixed": {
"$omitEmpty": {
"$ref": "request.providerOptions.volcengine-ark-agent-plan-video.camera_fixed"
}
}
}
},
"poll": {
"method": "GET",
"path": "/api/plan/v3/contents/generations/tasks/{{taskId}}"
},
"cancel": {
"method": "DELETE",
"path": "/api/plan/v3/contents/generations/tasks/{{taskId}}"
},
"response": {
"taskId": {
"$coalesce": [
{
"$ref": "response.id"
},
{
"$ref": "response.task_id"
},
{
"$ref": "response.data.id"
},
{
"$ref": "taskId"
}
]
},
"status": {
"$coalesce": [
{
"$ref": "response.status"
},
{
"$ref": "response.data.status"
},
"pending"
]
},
"message": {
"$coalesce": [
{
"$ref": "response.error.message"
},
{
"$ref": "response.message"
},
{
"$ref": "response.fail_reason"
}
]
},
"videos": {
"$coalesce": [
{
"$ref": "response.content.video_url"
},
{
"$ref": "response.video_url"
},
{
"$ref": "response.output.video_url"
},
{
"$ref": "response.data.video_url"
}
]
},
"usage": {
"$ref": "response.usage"
},
"errorPaths": [
"error.code"
],
"resultEphemeral": true
}
}
]
}
}
```
<!-- YINGCE_MANIFEST_CONTRACT_END -->

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
# Volcengine Ark Agent Plan Seedream Images
该目录是独立官方协议插件源码。后端从生成的 `volcengine-ark-agent-plan-seedream.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。
完整接口见 [docs/interface.md](docs/interface.md)。

View File

@@ -0,0 +1,418 @@
# Volcengine Ark Agent Plan Seedream Images 接口字段
## 协议身份
- 插件 ID:`volcengine-ark-agent-plan-seedream`。
- Provider ID:`volcengine-ark-agent-plan-image`。
- 能力:`image`。
- 默认 Base URL:`https://ark.cn-beijing.volces.com`。
- 鉴权驱动:`bearer`。
- 创建:`POST /api/plan/v3/images/generations`。
- 生命周期:同步响应。
## 配置字段
| 字段 | 类型 | 必填 | 含义 |
| --- | --- | --- | --- |
| `apiKey` | secret | 是 | API Key |
## 统一字段映射
| 统一字段 | 类型 | 必填 | 上游映射 | 说明 |
| --- | --- | --- | --- | --- |
| `model` | string | 是 | `model` | 图片模型 ID。 |
| `prompt` | string | 是 | `prompt` | 图片提示词。 |
| `images` | media[] | 否 | `provider image/reference fields` | 参考图或编辑源图,role 由业务层确定。 |
| `imageCount` | integer | 否 | `n/sample_count` | 输出数量。 |
| `aspectRatio` | string | 否 | `size/aspect_ratio` | 比例或尺寸,语义按协议说明。 |
| `resolution` | string | 否 | `resolution/imageSize` | 分辨率档位。 |
| `quality` | string | 否 | `quality` | 质量档位。 |
| `providerOptions` | object | 否 | `provider-specific fields` | 插件命名空间内的厂商扩展字段。 |
## 上游请求模板逐字段清单
下表由插件请求模板生成,覆盖 body、query、headers 和 multipart 文件声明中的每个字段。
| 上游位置 | 值或转换表达式 |
| --- | --- |
| `create.method` | `"POST"` |
| `create.path` | `"/api/plan/v3/images/generations"` |
| `create.contentType` | `"application/json"` |
| `create.body.model` | `{"$ref":"request.model"}` |
| `create.body.prompt` | `{"$ref":"request.prompt"}` |
| `create.body.size` | `{"$omitEmpty":{"$switch":{"cases":[{"when":{"$eq":[{"$ref":"request.aspectRatio"},"auto"]},"then":"2k"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"1:1"]},"then":"2048x2048"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"4:3"]},"then":"2304x1728"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"3:4"]},"then":"1728x2304"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"16:9"]},"then":"2560x1440"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"9:16"]},"then":"1440x2560"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"3:2"]},"then":"2496x1664"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"2:3"]},"then":"1664x2496"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"21:9"]},"then":"3024x1296"}],"default":{"$ref":"request.aspectRatio"}}}}` |
| `create.body.image` | `{"$omitEmpty":{"$if":{"condition":{"$eq":[{"$len":{"$ref":"request.images"}},1]},"then":{"$first":{"$map":{"from":{"$ref":"request.images"},"as":"media","in":{"$ref":"media.value"}}}},"else":{"$if":{"condition":{"$gt":[{"$len":{"$ref":"request.images"}},1]},"then":{"$map":{"from":{"$ref":"request.images"},"as":"media","in":{"$ref":"media.value"}}},"else":null}}}}}` |
| `create.body.sequential_image_generation` | `{"$omitEmpty":{"$ref":"request.providerOptions.volcengine-ark-agent-plan-image.sequential_image_generation"}}` |
| `create.body.sequential_image_generation_options` | `{"$omitEmpty":{"$ref":"request.providerOptions.volcengine-ark-agent-plan-image.sequential_image_generation_options"}}` |
| `create.body.watermark` | `{"$coalesce":[{"$ref":"request.providerOptions.volcengine-ark-agent-plan-image.watermark"},{"$ref":"request.watermark"},false]}` |
| `create.body.seed` | `{"$omitEmpty":{"$ref":"request.providerOptions.volcengine-ark-agent-plan-image.seed"}}` |
| `create.body.response_format` | `{"$coalesce":[{"$ref":"request.providerOptions.volcengine-ark-agent-plan-image.response_format"},"b64_json"]}` |
## Provider 扩展键
- `providerOptions.volcengine-ark-agent-plan-image.response_format`
- `providerOptions.volcengine-ark-agent-plan-image.seed`
- `providerOptions.volcengine-ark-agent-plan-image.sequential_image_generation`
- `providerOptions.volcengine-ark-agent-plan-image.sequential_image_generation_options`
- `providerOptions.volcengine-ark-agent-plan-image.watermark`
动态模型或工作流允许使用文档声明的完整 `parameters/input/extra_body` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。
## 响应映射逐字段清单
| 映射位置 | 上游路径或转换表达式 |
| --- | --- |
| `response.status` | `"succeeded"` |
| `response.images` | `{"$ref":"response.data"}` |
| `response.usage` | `{"$ref":"response.usage"}` |
| `response.errorPaths[0]` | `"error.code"` |
| `response.messagePaths[0]` | `"error.message"` |
## 响应与错误
插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。
## 兼容边界
Agent Plan 专属接入:请求路径为 /api/plan/v3/images/generations,必须使用 Agent Plan 控制台专属 API Key;请求体与官方 Seedream 协议一致,但凭证与额度按 AFP 套餐结算,不能与 /api/v3 官方 Key 混用。
<!-- YINGCE_MANIFEST_CONTRACT_START -->
## Manifest 完整接口定义
以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。
```json
{
"apiVersion": "yingce.plugin/v2",
"id": "volcengine-ark-agent-plan-seedream",
"name": "Volcengine Ark Agent Plan Seedream Images",
"version": "2.0.0",
"author": "Volcengine / 影策",
"description": "Volcengine Ark Agent Plan Seedream Images 独立请求协议插件。",
"documentation": "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>",
"permissions": [
"generation.run",
"media.read"
],
"configuration": {
"fields": [
{
"name": "apiKey",
"type": "secret",
"label": "API Key",
"required": true
}
]
},
"contributes": {
"providers": [
{
"id": "volcengine-ark-agent-plan-image",
"label": "Volcengine Ark Agent Plan Seedream Images",
"capabilities": [
"image"
],
"scopes": [
"admin.system-channel",
"user.custom-channel",
"canvas",
"creation",
"agent"
],
"baseUrl": "https://ark.cn-beijing.volces.com",
"requiresPublicMediaUrls": false,
"auth": {
"type": "bearer",
"field": "apiKey"
},
"parameters": [
{
"name": "model",
"type": "string",
"required": true,
"mapping": "model",
"description": "图片模型 ID。"
},
{
"name": "prompt",
"type": "string",
"required": true,
"mapping": "prompt",
"description": "图片提示词。"
},
{
"name": "images",
"type": "media[]",
"required": false,
"mapping": "provider image/reference fields",
"description": "参考图或编辑源图,role 由业务层确定。"
},
{
"name": "imageCount",
"type": "integer",
"required": false,
"mapping": "n/sample_count",
"description": "输出数量。"
},
{
"name": "aspectRatio",
"type": "string",
"required": false,
"mapping": "size/aspect_ratio",
"description": "比例或尺寸,语义按协议说明。"
},
{
"name": "resolution",
"type": "string",
"required": false,
"mapping": "resolution/imageSize",
"description": "分辨率档位。"
},
{
"name": "quality",
"type": "string",
"required": false,
"mapping": "quality",
"description": "质量档位。"
},
{
"name": "providerOptions",
"type": "object",
"required": false,
"mapping": "provider-specific fields",
"description": "插件命名空间内的厂商扩展字段。"
}
],
"create": {
"method": "POST",
"path": "/api/plan/v3/images/generations",
"contentType": "application/json",
"body": {
"model": {
"$ref": "request.model"
},
"prompt": {
"$ref": "request.prompt"
},
"size": {
"$omitEmpty": {
"$switch": {
"cases": [
{
"when": {
"$eq": [
{
"$ref": "request.aspectRatio"
},
"auto"
]
},
"then": "2k"
},
{
"when": {
"$eq": [
{
"$ref": "request.aspectRatio"
},
"1:1"
]
},
"then": "2048x2048"
},
{
"when": {
"$eq": [
{
"$ref": "request.aspectRatio"
},
"4:3"
]
},
"then": "2304x1728"
},
{
"when": {
"$eq": [
{
"$ref": "request.aspectRatio"
},
"3:4"
]
},
"then": "1728x2304"
},
{
"when": {
"$eq": [
{
"$ref": "request.aspectRatio"
},
"16:9"
]
},
"then": "2560x1440"
},
{
"when": {
"$eq": [
{
"$ref": "request.aspectRatio"
},
"9:16"
]
},
"then": "1440x2560"
},
{
"when": {
"$eq": [
{
"$ref": "request.aspectRatio"
},
"3:2"
]
},
"then": "2496x1664"
},
{
"when": {
"$eq": [
{
"$ref": "request.aspectRatio"
},
"2:3"
]
},
"then": "1664x2496"
},
{
"when": {
"$eq": [
{
"$ref": "request.aspectRatio"
},
"21:9"
]
},
"then": "3024x1296"
}
],
"default": {
"$ref": "request.aspectRatio"
}
}
}
},
"image": {
"$omitEmpty": {
"$if": {
"condition": {
"$eq": [
{
"$len": {
"$ref": "request.images"
}
},
1
]
},
"then": {
"$first": {
"$map": {
"from": {
"$ref": "request.images"
},
"as": "media",
"in": {
"$ref": "media.value"
}
}
}
},
"else": {
"$if": {
"condition": {
"$gt": [
{
"$len": {
"$ref": "request.images"
}
},
1
]
},
"then": {
"$map": {
"from": {
"$ref": "request.images"
},
"as": "media",
"in": {
"$ref": "media.value"
}
}
},
"else": null
}
}
}
}
},
"sequential_image_generation": {
"$omitEmpty": {
"$ref": "request.providerOptions.volcengine-ark-agent-plan-image.sequential_image_generation"
}
},
"sequential_image_generation_options": {
"$omitEmpty": {
"$ref": "request.providerOptions.volcengine-ark-agent-plan-image.sequential_image_generation_options"
}
},
"watermark": {
"$coalesce": [
{
"$ref": "request.providerOptions.volcengine-ark-agent-plan-image.watermark"
},
{
"$ref": "request.watermark"
},
false
]
},
"seed": {
"$omitEmpty": {
"$ref": "request.providerOptions.volcengine-ark-agent-plan-image.seed"
}
},
"response_format": {
"$coalesce": [
{
"$ref": "request.providerOptions.volcengine-ark-agent-plan-image.response_format"
},
"b64_json"
]
}
}
},
"response": {
"status": "succeeded",
"images": {
"$ref": "response.data"
},
"usage": {
"$ref": "response.usage"
},
"errorPaths": [
"error.code"
],
"messagePaths": [
"error.message"
]
}
}
]
}
}
```
<!-- YINGCE_MANIFEST_CONTRACT_END -->

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
# Volcengine Ark Seedance
该目录是独立官方协议插件源码。后端从生成的 `volcengine-ark-seedance.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。
完整接口见 [docs/interface.md](docs/interface.md)。

View File

@@ -0,0 +1,527 @@
# Volcengine Ark Seedance 接口字段
## 协议身份
- 插件 ID:`volcengine-ark-seedance`。
- Provider ID:`volcengine-ark-video`。
- 能力:`video`。
- 默认 Base URL:`https://ark.cn-beijing.volces.com`。
- 鉴权驱动:`bearer`。
- 创建:`POST /api/v3/contents/generations/tasks`。
- 查询:`GET /api/v3/contents/generations/tasks/{{taskId}}`。
- 取消:`DELETE /api/v3/contents/generations/tasks/{{taskId}}`。
## 配置字段
| 字段 | 类型 | 必填 | 含义 |
| --- | --- | --- | --- |
| `apiKey` | secret | 是 | API Key |
## 统一字段映射
| 统一字段 | 类型 | 必填 | 上游映射 | 说明 |
| --- | --- | --- | --- | --- |
| `model` | string | 是 | `model` | Ark endpoint/model ID。 |
| `prompt` | string | 是 | `content[type=text].text` | 视频提示词。 |
| `images` | media[] | 否 | `content[type=image_url]` | first_frame、last_frame、reference_image 等 role 原样映射。 |
| `videos` | media[] | 否 | `content[type=video_url]` | reference_video。 |
| `audios` | media[] | 否 | `content[type=audio_url]` | reference_audio/reference_voice。 |
| `aspectRatio` | string | 否 | `ratio` | 输出画幅。 |
| `resolution` | string | 否 | `resolution` | 输出分辨率档位。 |
| `duration` | integer | 否 | `duration` | 输出时长秒数。 |
| `generateAudio` | boolean | 否 | `generate_audio` | 是否生成音频。 |
| `watermark` | boolean | 否 | `watermark` | 是否带水印。 |
| `seed` | integer | 否 | `seed` | providerOptions seed。 |
| `camera_fixed` | boolean | 否 | `camera_fixed` | providerOptions camera_fixed。 |
## 上游请求模板逐字段清单
下表由插件请求模板生成,覆盖 body、query、headers 和 multipart 文件声明中的每个字段。
| 上游位置 | 值或转换表达式 |
| --- | --- |
| `create.method` | `"POST"` |
| `create.path` | `"/api/v3/contents/generations/tasks"` |
| `create.contentType` | `"application/json"` |
| `create.body.model` | `{"$ref":"request.model"}` |
| `create.body.content` | `{"$concatArrays":[[{"type":"text","text":{"$ref":"request.prompt"}}],{"$map":{"from":{"$sortByOrder":{"$ref":"request.images"}},"as":"media","in":{"type":"image_url","image_url":{"url":{"$ref":"media.value"}},"role":{"$coalesce":[{"$ref":"media.role"},"reference_image"]}}}},{"$map":{"from":{"$sortByOrder":{"$ref":"request.videos"}},"as":"media","in":{"type":"video_url","video_url":{"url":{"$ref":"media.value"}},"role":{"$coalesce":[{"$ref":"media.role"},"reference_video"]}}}},{"$map":{"from":{"$sortByOrder":{"$ref":"request.audios"}},"as":"media","in":{"type":"audio_url","audio_url":{"url":{"$ref":"media.value"}},"role":{"$coalesce":[{"$ref":"media.role"},"reference_audio"]}}}}]}` |
| `create.body.ratio` | `{"$coalesce":[{"$ref":"request.aspectRatio"},"16:9"]}` |
| `create.body.resolution` | `{"$coalesce":[{"$ref":"request.resolution"},"720p"]}` |
| `create.body.duration` | `{"$if":{"condition":{"$gt":[{"$ref":"request.duration"},0]},"then":{"$ref":"request.duration"},"else":5}}` |
| `create.body.generate_audio` | `{"$ref":"request.generateAudio"}` |
| `create.body.watermark` | `{"$ref":"request.watermark"}` |
| `create.body.seed` | `{"$omitEmpty":{"$ref":"request.providerOptions.volcengine-ark-video.seed"}}` |
| `create.body.camera_fixed` | `{"$omitEmpty":{"$ref":"request.providerOptions.volcengine-ark-video.camera_fixed"}}` |
| `poll.method` | `"GET"` |
| `poll.path` | `"/api/v3/contents/generations/tasks/{{taskId}}"` |
| `poll.contentType` | `"application/json"` |
| `cancel.method` | `"DELETE"` |
| `cancel.path` | `"/api/v3/contents/generations/tasks/{{taskId}}"` |
| `cancel.contentType` | `"application/json"` |
## Provider 扩展键
- `providerOptions.volcengine-ark-video.camera_fixed`
- `providerOptions.volcengine-ark-video.seed`
动态模型或工作流允许使用文档声明的完整 `parameters/input/extra_body` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。
## 响应映射逐字段清单
| 映射位置 | 上游路径或转换表达式 |
| --- | --- |
| `response.taskId` | `{"$coalesce":[{"$ref":"response.id"},{"$ref":"response.task_id"},{"$ref":"response.data.id"},{"$ref":"taskId"}]}` |
| `response.status` | `{"$coalesce":[{"$ref":"response.status"},{"$ref":"response.data.status"},"pending"]}` |
| `response.message` | `{"$coalesce":[{"$ref":"response.error.message"},{"$ref":"response.message"},{"$ref":"response.fail_reason"}]}` |
| `response.videos` | `{"$coalesce":[{"$ref":"response.content.video_url"},{"$ref":"response.video_url"},{"$ref":"response.output.video_url"},{"$ref":"response.data.video_url"}]}` |
| `response.usage` | `{"$ref":"response.usage"}` |
| `response.errorPaths[0]` | `"error.code"` |
| `response.resultEphemeral` | `true` |
## 响应与错误
插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。
## 兼容边界
官方 Ark 推理接入:创建/查询/取消走 /api/v3/contents/generations/tasks;插件不根据图片下标推断首尾帧,role 由业务层确定。API Key 来自方舟推理接入控制台。
<!-- YINGCE_MANIFEST_CONTRACT_START -->
## Manifest 完整接口定义
以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。
```json
{
"apiVersion": "yingce.plugin/v2",
"id": "volcengine-ark-seedance",
"name": "Volcengine Ark Seedance",
"version": "2.0.0",
"author": "Volcengine / 影策",
"description": "Volcengine Ark Seedance 独立请求协议插件。",
"documentation": "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>",
"permissions": [
"generation.run",
"media.read"
],
"configuration": {
"fields": [
{
"name": "apiKey",
"type": "secret",
"label": "API Key",
"required": true
}
]
},
"contributes": {
"providers": [
{
"id": "volcengine-ark-video",
"label": "Volcengine Ark Seedance",
"capabilities": [
"video"
],
"scopes": [
"admin.system-channel",
"user.custom-channel",
"canvas",
"creation",
"agent"
],
"baseUrl": "https://ark.cn-beijing.volces.com",
"requiresPublicMediaUrls": true,
"auth": {
"type": "bearer",
"field": "apiKey"
},
"parameters": [
{
"name": "model",
"type": "string",
"required": true,
"mapping": "model",
"description": "Ark endpoint/model ID。"
},
{
"name": "prompt",
"type": "string",
"required": true,
"mapping": "content[type=text].text",
"description": "视频提示词。"
},
{
"name": "images",
"type": "media[]",
"required": false,
"mapping": "content[type=image_url]",
"description": "first_frame、last_frame、reference_image 等 role 原样映射。"
},
{
"name": "videos",
"type": "media[]",
"required": false,
"mapping": "content[type=video_url]",
"description": "reference_video。"
},
{
"name": "audios",
"type": "media[]",
"required": false,
"mapping": "content[type=audio_url]",
"description": "reference_audio/reference_voice。"
},
{
"name": "aspectRatio",
"type": "string",
"required": false,
"mapping": "ratio",
"description": "输出画幅。"
},
{
"name": "resolution",
"type": "string",
"required": false,
"mapping": "resolution",
"description": "输出分辨率档位。"
},
{
"name": "duration",
"type": "integer",
"required": false,
"mapping": "duration",
"description": "输出时长秒数。"
},
{
"name": "generateAudio",
"type": "boolean",
"required": false,
"mapping": "generate_audio",
"description": "是否生成音频。"
},
{
"name": "watermark",
"type": "boolean",
"required": false,
"mapping": "watermark",
"description": "是否带水印。"
},
{
"name": "seed",
"type": "integer",
"required": false,
"mapping": "seed",
"description": "providerOptions seed。"
},
{
"name": "camera_fixed",
"type": "boolean",
"required": false,
"mapping": "camera_fixed",
"description": "providerOptions camera_fixed。"
}
],
"validations": [
{
"assert": {
"$lte": [
{
"$len": {
"$ref": "request.images"
}
},
9
]
},
"message": "Seedance 最多支持 9 张参考图片"
},
{
"assert": {
"$lte": [
{
"$len": {
"$ref": "request.videos"
}
},
3
]
},
"message": "Seedance 最多支持 3 个参考视频"
},
{
"assert": {
"$lte": [
{
"$len": {
"$ref": "request.audios"
}
},
3
]
},
"message": "Seedance 最多支持 3 个参考音频"
},
{
"assert": {
"$or": [
{
"$eq": [
{
"$len": {
"$ref": "request.audios"
}
},
0
]
},
{
"$gt": [
{
"$add": [
{
"$len": {
"$ref": "request.images"
}
},
{
"$len": {
"$ref": "request.videos"
}
}
]
},
0
]
}
]
},
"message": "Seedance 不支持纯音频或文本+音频,请同时添加参考图片或参考视频"
}
],
"create": {
"method": "POST",
"path": "/api/v3/contents/generations/tasks",
"contentType": "application/json",
"body": {
"model": {
"$ref": "request.model"
},
"content": {
"$concatArrays": [
[
{
"type": "text",
"text": {
"$ref": "request.prompt"
}
}
],
{
"$map": {
"from": {
"$sortByOrder": {
"$ref": "request.images"
}
},
"as": "media",
"in": {
"type": "image_url",
"image_url": {
"url": {
"$ref": "media.value"
}
},
"role": {
"$coalesce": [
{
"$ref": "media.role"
},
"reference_image"
]
}
}
}
},
{
"$map": {
"from": {
"$sortByOrder": {
"$ref": "request.videos"
}
},
"as": "media",
"in": {
"type": "video_url",
"video_url": {
"url": {
"$ref": "media.value"
}
},
"role": {
"$coalesce": [
{
"$ref": "media.role"
},
"reference_video"
]
}
}
}
},
{
"$map": {
"from": {
"$sortByOrder": {
"$ref": "request.audios"
}
},
"as": "media",
"in": {
"type": "audio_url",
"audio_url": {
"url": {
"$ref": "media.value"
}
},
"role": {
"$coalesce": [
{
"$ref": "media.role"
},
"reference_audio"
]
}
}
}
}
]
},
"ratio": {
"$coalesce": [
{
"$ref": "request.aspectRatio"
},
"16:9"
]
},
"resolution": {
"$coalesce": [
{
"$ref": "request.resolution"
},
"720p"
]
},
"duration": {
"$if": {
"condition": {
"$gt": [
{
"$ref": "request.duration"
},
0
]
},
"then": {
"$ref": "request.duration"
},
"else": 5
}
},
"generate_audio": {
"$ref": "request.generateAudio"
},
"watermark": {
"$ref": "request.watermark"
},
"seed": {
"$omitEmpty": {
"$ref": "request.providerOptions.volcengine-ark-video.seed"
}
},
"camera_fixed": {
"$omitEmpty": {
"$ref": "request.providerOptions.volcengine-ark-video.camera_fixed"
}
}
}
},
"poll": {
"method": "GET",
"path": "/api/v3/contents/generations/tasks/{{taskId}}"
},
"cancel": {
"method": "DELETE",
"path": "/api/v3/contents/generations/tasks/{{taskId}}"
},
"response": {
"taskId": {
"$coalesce": [
{
"$ref": "response.id"
},
{
"$ref": "response.task_id"
},
{
"$ref": "response.data.id"
},
{
"$ref": "taskId"
}
]
},
"status": {
"$coalesce": [
{
"$ref": "response.status"
},
{
"$ref": "response.data.status"
},
"pending"
]
},
"message": {
"$coalesce": [
{
"$ref": "response.error.message"
},
{
"$ref": "response.message"
},
{
"$ref": "response.fail_reason"
}
]
},
"videos": {
"$coalesce": [
{
"$ref": "response.content.video_url"
},
{
"$ref": "response.video_url"
},
{
"$ref": "response.output.video_url"
},
{
"$ref": "response.data.video_url"
}
]
},
"usage": {
"$ref": "response.usage"
},
"errorPaths": [
"error.code"
],
"resultEphemeral": true
}
}
]
}
}
```
<!-- YINGCE_MANIFEST_CONTRACT_END -->

File diff suppressed because one or more lines are too long

View File

@@ -74,7 +74,7 @@
## 兼容边界
该包只代表上述线协议 profile;同一品牌的其他 endpoint、云区域或网关包装必须使用独立插件,不能根据模型名猜测。
官方 Ark 推理接入:Base URL 使用 /api/v3,API Key 来自方舟推理接入控制台,不可与 Agent Plan 专属 Key 混用。
<!-- YINGCE_MANIFEST_CONTRACT_START -->
## Manifest 完整接口定义

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,7 @@ import { Switch } from "@/components/ui/base/switch";
import { ImageSettingsTheme } from "@/components/image-settings-panel";
import { boolConfig, isSeedanceFastModel, isSeedanceVideoConfig, normalizeSeedanceDuration, normalizeSeedanceRatio, normalizeSeedanceResolution, seedanceRatioOptions } from "@/lib/seedance-video";
import { isVolcengineArkVideoProtocol } from "@/lib/model-protocols";
import { type CanvasTheme } from "@/lib/canvas-theme";
import { formatVideoResolutionLabel, isVideoResolutionMatch, normalizeVideoDuration, videoDimensionsForRatioAndResolution, videoResolutionComparisonKey, VIDEO_DURATION_MIN } from "@/lib/video-generation-options";
import { modelCapabilityConfigFor, resolveVideoRatioValue, resolveVideoResolutionValue, videoDurationOptions, type VideoCapabilityConfig } from "@/lib/model-capabilities";
@@ -115,7 +116,7 @@ function SeedanceVideoSettingsPanel({ config, profile, priceTiers, onConfigChang
const generateAudio = boolConfig(config.videoGenerateAudio, profile.generateAudio.default);
const watermark = boolConfig(config.videoWatermark, profile.watermark.default);
const useArkPrivateAssets = boolConfig(config.videoArkPrivateAssetUpload, true);
const isArkSeedance = resolveModelRequestConfig(config, config.model).interfaceType === "volcengine-ark-video";
const isArkSeedance = isVolcengineArkVideoProtocol(resolveModelRequestConfig(config, config.model).interfaceType);
return (
<ImageSettingsTheme theme={theme}>

View File

@@ -216,7 +216,7 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = "
image.responseFormat = { supported: true };
image.outputFormat = { supported: false };
image.maxOutputs = 1;
} else if (protocol === "volcengine-ark-image") {
} else if (protocol === "volcengine-ark-image" || protocol === "volcengine-ark-agent-plan-image") {
image.references.maskSupported = false;
image.quality.supported = false;
image.transparentBackground.supported = false;
@@ -320,7 +320,7 @@ export function defaultModelCapabilityConfig(protocol?: ModelProtocol, model = "
video.duration = { selection: "enum", values: [4, 6, 8], default: 6 };
video.resolutions = ["720p", "1080p"];
}
if (protocol === "volcengine-ark-video" || protocol === "newapi-channel-1" || protocol === "newapi-channel-2") {
if (protocol === "volcengine-ark-video" || protocol === "volcengine-ark-agent-plan-video" || protocol === "newapi-channel-1" || protocol === "newapi-channel-2") {
video.references.maxVideos = 3;
video.references.maxAudios = 3;
video.references.maxVideoBytes = 200 * 1024 * 1024;
@@ -329,8 +329,8 @@ export function defaultModelCapabilityConfig(protocol?: ModelProtocol, model = "
video.references.maxAudioDurationSeconds = 15;
video.generateAudio = { supported: true, default: true };
}
if (protocol === "volcengine-ark-video" || protocol === "newapi-channel-1") video.resolutions = ["480p", "720p", "1080p"];
if (protocol === "volcengine-ark-video") {
if (protocol === "volcengine-ark-video" || protocol === "volcengine-ark-agent-plan-video" || protocol === "newapi-channel-1") video.resolutions = ["480p", "720p", "1080p"];
if (protocol === "volcengine-ark-video" || protocol === "volcengine-ark-agent-plan-video") {
video.watermark = { supported: true, default: false };
video.operations.push("reference_to_video", "audio_to_video");
}

View File

@@ -9,8 +9,16 @@ export function protocolGroups(protocols: ModelProtocolDefinition[]) {
export function modelProtocolDefinition(value: string | undefined, definitions: ModelProtocolDefinition[] = []) { return definitions.find((item) => item.value === value); }
export function modelProtocolLabel(value: string | undefined, definitions: ModelProtocolDefinition[] = []) { return modelProtocolDefinition(value, definitions)?.label || (value ? value : "未安装协议"); }
export function modelProtocolCapability(value: string | undefined, definitions: ModelProtocolDefinition[] = []) { return modelProtocolDefinition(value, definitions)?.capability; }
export function isVolcengineArkImageProtocol(protocol?: string) {
return protocol === "volcengine-ark-image" || protocol === "volcengine-ark-agent-plan-image";
}
export function isVolcengineArkVideoProtocol(protocol?: string) {
return protocol === "volcengine-ark-video" || protocol === "volcengine-ark-agent-plan-video";
}
export function modelProtocolSupportsTokenBilling(capability?: string, protocol?: string) {
return capability === "text" || (capability === "video" && protocol === "volcengine-ark-video");
return capability === "text" || (capability === "video" && isVolcengineArkVideoProtocol(protocol));
}
export function protocolForModelCatalog(_endpointTypes: string[] = []): ModelProtocol | undefined {

View File

@@ -136,7 +136,7 @@ function resolveModelAdaptationProfile(input: PromptOptimizationInput): ModelAda
avoid: ["不要添加 SD 权重、Midjourney 参数或模型未确认支持的控制语法。"],
};
}
if (containsAny(model, ["seedream", "jimeng", "doubao", "volcengine-ark-image", "volcengine-jimeng-image"])) {
if (containsAny(model, ["seedream", "jimeng", "doubao", "volcengine-ark-image", "volcengine-ark-agent-plan-image", "volcengine-jimeng-image"])) {
return {
id: "seedream-image",
label: "Seedream / 即梦 / 火山图片模型",
@@ -184,7 +184,7 @@ function resolveModelAdaptationProfile(input: PromptOptimizationInput): ModelAda
return genericImageProfile;
}
if (containsAny(model, ["seedance", "doubao-seedance", "volcengine-ark-video", "volcengine-jimeng-video", "jimeng-video"])) {
if (containsAny(model, ["seedance", "doubao-seedance", "volcengine-ark-video", "volcengine-ark-agent-plan-video", "volcengine-jimeng-video", "jimeng-video"])) {
return {
id: "seedance-video",
label: "Seedance / 即梦视频模型",

View File

@@ -59,7 +59,12 @@ const seedancePixels = {
export function isSeedanceVideoConfig(config: AiConfig | Pick<AiConfig, "model" | "videoModel" | "baseUrl">) {
const requestConfig = "channels" in config ? resolveModelRequestConfig(config, config.model || config.videoModel) : config;
return isSeedanceVideoModel(modelOptionName(requestConfig.model || requestConfig.videoModel)) || isArkPlanBaseUrl(requestConfig.baseUrl);
// Agent Plan 图片与视频共用 /api/plan/v3;按协议排除图片,避免 Seedream 误判为 Seedance。
if ("interfaceType" in requestConfig) {
const interfaceType = requestConfig.interfaceType;
if (interfaceType === "volcengine-ark-image" || interfaceType === "volcengine-ark-agent-plan-image") return false;
}
return isSeedanceVideoModel(modelOptionName(requestConfig.model || requestConfig.videoModel)) || isArkPlanBaseUrl(requestConfig.baseUrl || "");
}
export function isSeedanceVideoModel(model: string) {

View File

@@ -95,7 +95,7 @@ export async function runBackendGenerationTask(
throwIfAborted(signal);
assertClientPromptLimit(mode, prompt, config, metadata);
assertBackendRuntimeConfigured(config, mode);
const prepared = await prepareGenerationReferences({ config, referenceImages, referenceVideos, referenceAudios, mask });
const prepared = await prepareGenerationReferences({ config, mode, referenceImages, referenceVideos, referenceAudios, mask });
throwIfAborted(signal);
return createAndWaitGenerationTask({ projectId, mode, prompt, config, referenceImages, referenceVideos, referenceAudios, textHistory, signal, metadata, onTaskUpdate, onTextDelta, streamText, enableThinking, clientOperationId, retryOf, attemptGroupId }, prepared, dependencies);
}
@@ -233,12 +233,14 @@ function assertClientPromptLimit(mode: BackendGenerationMode, prompt: string, co
async function prepareGenerationReferences({
config,
mode,
referenceImages = [],
referenceVideos = [],
referenceAudios = [],
mask,
}: Pick<BackendGenerationTaskOptions, "config" | "referenceImages" | "referenceVideos" | "referenceAudios" | "mask">): Promise<PreparedGenerationReferences> {
const preferArkAssetUrl = usesArkVideoAssetReference(config);
}: Pick<BackendGenerationTaskOptions, "config" | "mode" | "referenceImages" | "referenceVideos" | "referenceAudios" | "mask">): Promise<PreparedGenerationReferences> {
// asset:// 仅视频生成可用;Agent Plan Seedream 与 Seedance 共用 /api/plan/v3,不能按 BaseURL 误判。
const preferArkAssetUrl = mode === "video" && usesArkVideoAssetReference(config);
const preparedImages = await Promise.all(referenceImages.map((image) => prepareBackendImageReference(image, preferArkAssetUrl)));
const preparedVideos = await Promise.all(referenceVideos.map(prepareBackendMediaReference));
const preparedAudios = await Promise.all(referenceAudios.map(prepareBackendMediaReference));
@@ -249,7 +251,10 @@ async function prepareGenerationReferences({
// 与后端 isArkPrivateAssetVideoConfig 对齐:方舟视频渠道允许参考图直接 asset:// 引用,
// 跳过可信素材上传同步(适合已录入方舟素材 ID 的素材,例如被授权的真人像素材)。
function usesArkVideoAssetReference(config: AiConfig) {
return resolveModelRequestConfig(config, config.model).interfaceType === "volcengine-ark-video" || isArkPlanBaseUrl(config.baseUrl || "");
const interfaceType = resolveModelRequestConfig(config, config.model).interfaceType;
if (interfaceType === "volcengine-ark-video" || interfaceType === "volcengine-ark-agent-plan-video") return true;
if (interfaceType === "volcengine-ark-image" || interfaceType === "volcengine-ark-agent-plan-image") return false;
return isArkPlanBaseUrl(config.baseUrl || "");
}
async function createAndWaitGenerationTask(options: BackendGenerationTaskOptions, prepared: PreparedGenerationReferences, dependencies: GenerationTaskDependencies) {

View File

@@ -6,6 +6,7 @@ import type { ReferenceImage } from "@/types/image";
import { withOpenAIPromptCacheKey } from "@/lib/openai-prompt-cache";
import { modelCapabilityConfigFor, normalizeImageValue } from "@/lib/model-capabilities";
import { buildGeminiImageGenerationConfig, parseGeminiImageDataUrl, type GeminiImageGenerationConfig } from "@/lib/gemini-image";
import { isVolcengineArkImageProtocol } from "@/lib/model-protocols";
import { aiApiUrl, aiHeaders, imageChannelTransport, postChannelJSON, postGeminiJSON, postVolcengineArkImage } from "@/services/api/image-transport";
const IMAGE_OUTPUT_FORMAT = "png";
@@ -94,7 +95,7 @@ export async function requestGeneration(config: AiConfig, prompt: string, option
}
const quality = imageProfile.quality.supported && normalizedImage.quality !== "auto" ? normalizeQuality(normalizedImage.quality) || normalizedImage.quality : undefined;
const requestSize = resolveImageRequestSize(imageProfile, quality, normalizedImage.size);
const isVolcengineArk = requestConfig.interfaceType === "volcengine-ark-image";
const isVolcengineArk = isVolcengineArkImageProtocol(requestConfig.interfaceType);
const normalizedRequestSize = requestSize?.parameter === "size" && isVolcengineArk ? { ...requestSize, value: normalizeVolcengineArkImageSize(requestSize.value)! } : requestSize;
try {
const payload = isVolcengineArk
@@ -174,7 +175,7 @@ export async function requestEdit(config: AiConfig, prompt: string, references:
throw new Error(readAxiosError(error, "Grok 图片编辑失败"));
}
}
if (requestConfig.interfaceType === "volcengine-ark-image") {
if (isVolcengineArkImageProtocol(requestConfig.interfaceType)) {
if (mask) throw new Error("火山方舟图片协议不支持蒙版编辑,请移除蒙版后重试");
const quality = imageProfile.quality.supported && normalizedImage.quality !== "auto" ? normalizeQuality(normalizedImage.quality) || normalizedImage.quality : undefined;
const sizeRequest = resolveImageRequestSize(imageProfile, quality, normalizedImage.size);

View File

@@ -1,4 +1,5 @@
import { modelCapabilityConfigFor } from "@/lib/model-capabilities";
import { isVolcengineArkVideoProtocol } from "@/lib/model-protocols";
import { boolConfig, buildSeedancePromptText, isArkPlanBaseUrl, isSeedanceVideoConfig, normalizeSeedanceDuration, normalizeSeedanceRatio, normalizeSeedanceResolution, seedanceVideoReferenceError, SEEDANCE_REFERENCE_LIMITS } from "@/lib/seedance-video";
import { getResourceOSSUrl } from "@/services/api/resources";
import { getMediaBlob } from "@/services/file-storage";
@@ -13,13 +14,13 @@ import type { VideoProviderDeps } from "./video-provider-deps";
import { hasExplicitVideoFrames, resolveVideoImageReferences } from "./video-reference-roles";
export function isSeedanceConfig(config: ResolvedAiConfig) {
return config.interfaceType === "volcengine-ark-video" || isSeedanceVideoConfig(config);
return isVolcengineArkVideoProtocol(config.interfaceType) || isSeedanceVideoConfig(config);
}
export async function createSeedanceTask(deps: VideoProviderDeps, config: ResolvedAiConfig, model: string, prompt: string, references: ReferenceImage[], videoReferences: ReferenceVideo[], audioReferences: ReferenceAudio[], options?: RequestOptions): Promise<VideoGenerationTask> {
assertSeedanceVideoReferences(videoReferences);
assertSeedanceAudioReferences(audioReferences);
const isVolcengineArk = config.interfaceType === "volcengine-ark-video";
const isVolcengineArk = isVolcengineArkVideoProtocol(config.interfaceType);
const payload = isVolcengineArk || isArkPlanBaseUrl(config.baseUrl)
? await buildSeedanceAgentPlanPayload(config, model, prompt, references, videoReferences, audioReferences, deps, options)
: await buildSeedanceVideosPayload(config, model, prompt, references, videoReferences, audioReferences, deps, options);
@@ -77,15 +78,15 @@ function assertSeedanceAudioReferences(audioReferences: ReferenceAudio[]) {
}
function seedanceApiUrl(config: ResolvedAiConfig, taskId?: string) {
if (config.interfaceType === "volcengine-ark-video" || isArkPlanBaseUrl(config.baseUrl)) return buildApiUrl(config.baseUrl, `/contents/generations/tasks${taskId ? `/${encodeURIComponent(taskId)}` : ""}`);
if (isVolcengineArkVideoProtocol(config.interfaceType) || isArkPlanBaseUrl(config.baseUrl)) return buildApiUrl(config.baseUrl, `/contents/generations/tasks${taskId ? `/${encodeURIComponent(taskId)}` : ""}`);
return buildApiUrl(config.baseUrl, `/videos${taskId ? `/${encodeURIComponent(taskId)}` : ""}`);
}
async function buildSeedanceAgentPlanPayload(config: ResolvedAiConfig, model: string, prompt: string, references: ReferenceImage[], videoReferences: ReferenceVideo[], audioReferences: ReferenceAudio[], deps: VideoProviderDeps, options?: RequestOptions) {
if (audioReferences.length && !references.length && !videoReferences.length) {
throw new Error(config.interfaceType === "volcengine-ark-video" ? "火山方舟全模态参考不支持纯音频或文本+音频,请同时添加参考图片或参考视频" : "Seedance 参考音频不能单独使用,请同时添加参考图或参考视频");
throw new Error(isVolcengineArkVideoProtocol(config.interfaceType) ? "火山方舟全模态参考不支持纯音频或文本+音频,请同时添加参考图片或参考视频" : "Seedance 参考音频不能单独使用,请同时添加参考图或参考视频");
}
const content = config.interfaceType === "volcengine-ark-video"
const content = isVolcengineArkVideoProtocol(config.interfaceType)
? await buildVolcengineArkContent(prompt, references, videoReferences, audioReferences, options)
: await buildSeedanceContent(config, prompt, references, videoReferences, audioReferences, deps, options);
if (!content.length) throw new Error("请输入视频提示词,或连接参考图片/视频/音频");

View File

@@ -912,6 +912,7 @@ export function defaultBaseUrlForApiFormat(apiFormat: ApiCallFormat) {
export function defaultBaseUrlForChannelInterface(interfaceType?: ChannelInterfaceType) {
if (interfaceType === "gemini-veo" || interfaceType === "gemini-image") return GEMINI_BASE_URL;
if (interfaceType === "novita-video") return "https://api.novita.ai/v3";
if (interfaceType === "volcengine-ark-agent-plan-image" || interfaceType === "volcengine-ark-agent-plan-video") return "https://ark.cn-beijing.volces.com/api/plan/v3";
if (interfaceType === "volcengine-ark-image" || interfaceType === "volcengine-ark-video") return "https://ark.cn-beijing.volces.com/api/v3";
if (interfaceType === "volcengine-jimeng-image" || interfaceType === "volcengine-jimeng-video") return "https://visual.volcengineapi.com";
if (interfaceType === "minimax-video") return "https://api.minimaxi.com";

View File

@@ -0,0 +1,86 @@
import { describe, expect, test } from "bun:test";
import { prepareBackendGenerationTask } from "../src/services/api/generation-task";
import { createModelChannel, defaultConfig, encodeChannelModel } from "../src/stores/use-config-store";
function agentPlanImageConfig() {
const channel = createModelChannel({
id: "ark-plan-image",
name: "Ark Plan Seedream",
baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3",
apiKey: "test-key",
interfaceType: "volcengine-ark-agent-plan-image",
models: ["doubao-seedream-4-0-250828"],
modelCosts: [{
model: "doubao-seedream-4-0-250828",
capability: "image",
protocol: "volcengine-ark-agent-plan-image",
billingMode: "fixed_request",
unitPriceMicrocredits: 1,
}],
});
const model = encodeChannelModel(channel.id, "doubao-seedream-4-0-250828");
return { ...defaultConfig, channels: [channel], model, baseUrl: channel.baseUrl, interfaceType: channel.interfaceType };
}
function agentPlanVideoConfig() {
const channel = createModelChannel({
id: "ark-plan-video",
name: "Ark Plan Seedance",
baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3",
apiKey: "test-key",
interfaceType: "volcengine-ark-agent-plan-video",
models: ["doubao-seedance-1-5-pro-251215"],
modelCosts: [{
model: "doubao-seedance-1-5-pro-251215",
capability: "video",
protocol: "volcengine-ark-agent-plan-video",
billingMode: "token",
inputPricePerMillion: 1,
outputPricePerMillion: 1,
}],
});
const model = encodeChannelModel(channel.id, "doubao-seedance-1-5-pro-251215");
return { ...defaultConfig, channels: [channel], model, videoModel: model, baseUrl: channel.baseUrl, interfaceType: channel.interfaceType };
}
describe("generation-task ark asset references", () => {
test("agent plan Seedream image mode does not rewrite references to asset://", async () => {
const input = await prepareBackendGenerationTask({
mode: "image",
prompt: "edit this",
config: agentPlanImageConfig(),
referenceImages: [{
id: "ref-1",
name: "ref.png",
type: "image/png",
url: "https://cdn.example.com/ref.png",
storageKey: "resource:res-1",
arkAssetId: "asset-already-synced",
}],
});
const images = (input.input as { referenceImages?: Array<{ url?: string; storageKey?: string }> }).referenceImages || [];
expect(images).toHaveLength(1);
expect(images[0]?.url).not.toStartWith("asset://");
expect(images[0]?.storageKey).toBe("resource:res-1");
});
test("agent plan Seedance video mode still prefers asset:// when arkAssetId exists", async () => {
const input = await prepareBackendGenerationTask({
mode: "video",
prompt: "animate this",
config: agentPlanVideoConfig(),
referenceImages: [{
id: "ref-1",
name: "ref.png",
type: "image/png",
url: "https://cdn.example.com/ref.png",
storageKey: "resource:res-1",
arkAssetId: "asset-already-synced",
}],
});
const images = (input.input as { referenceImages?: Array<{ url?: string }> }).referenceImages || [];
expect(images).toHaveLength(1);
expect(images[0]?.url).toBe("asset://asset-already-synced");
});
});

View File

@@ -6,8 +6,10 @@ describe("model protocol Token billing", () => {
test("supports text models and Volcengine Ark video only", () => {
expect(modelProtocolSupportsTokenBilling("text", "chat-completion")).toBe(true);
expect(modelProtocolSupportsTokenBilling("video", "volcengine-ark-video")).toBe(true);
expect(modelProtocolSupportsTokenBilling("video", "volcengine-ark-agent-plan-video")).toBe(true);
expect(modelProtocolSupportsTokenBilling("video", "volcengine-jimeng-video")).toBe(false);
expect(modelProtocolSupportsTokenBilling("video", "newapi")).toBe(false);
expect(modelProtocolSupportsTokenBilling("image", "volcengine-ark-image")).toBe(false);
expect(modelProtocolSupportsTokenBilling("image", "volcengine-ark-agent-plan-image")).toBe(false);
});
});