From 9b9a7f4a72e6314e3da084f96d522af433ad12d5 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 18 Sep 2026 14:09:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(merge):=20=E4=BB=A5=E5=AE=98=E6=96=B9=20ma?= =?UTF-8?q?in=20=E4=B8=BA=E5=9F=BA=E5=BA=95=E8=9E=8D=E5=90=88=20PR#530=20?= =?UTF-8?q?=E5=A4=9A=E7=BB=B4=E8=A1=A8=E6=A0=BC=E4=B8=8E=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E5=AE=9A=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 多维表格(canvas-batch-table-node/use-canvas-batch-table/canvas-batch-table 等)以 PR#530 实现为主 - 保留官方 main 全部新功能:banner/画布版本历史/提示词编辑/方舟 Agent Plan/支付导出 - 移植本地定制:ddcat 多参考图校验、万有引力 lxmone 连接错误提示、Description v20(v24)、自建插件(a6api/metaso-h3/antigravity-proxy/lxmone)、seccomp、Windows 脚本 - 迁移版本升至 v24(官方 v20-23 banner/版本历史 + 我们的 Description 列) - 修复官方缺失的 markdownPlainText import --- .gitignore | 2 - CHANGELOG.md | 9 + README.md | 6 - backend/internal/app/channel_models.go | 2 + backend/internal/app/ddcat_live_test.go | 60 + backend/internal/app/model_catalog.go | 2 + backend/internal/app/provider_http_client.go | 9 + backend/internal/app/provider_image.go | 29 + .../app/provider_image_boundary_test.go | 65 + backend/internal/app/provider_protocol.go | 7 +- backend/internal/database/migrations.go | 12 +- backend/internal/model/models_channel.go | 1 + docker-compose.deploy.yml | 8 +- plugin-packages/a6api.yingce-plugin | Bin 0 -> 8332 bytes plugin-packages/a6api/README.md | 20 + plugin-packages/a6api/docs/interface.md | 522 ++++ plugin-packages/a6api/manifest.json | 484 ++++ .../antigravity-proxy.yingce-plugin | Bin 0 -> 7286 bytes plugin-packages/antigravity-proxy/README.md | 41 + .../antigravity-proxy/docs/interface.md | 303 +++ .../antigravity-proxy/manifest.json | 224 ++ .../lxmone-video-suite.yingce-plugin | Bin 0 -> 17228 bytes plugin-packages/lxmone-video-suite/README.md | 27 + .../lxmone-video-suite/docs/interface.md | 2391 +++++++++++++++++ .../lxmone-video-suite/manifest.json | 2289 ++++++++++++++++ plugin-packages/metaso-h3.yingce-plugin | Bin 0 -> 8173 bytes plugin-packages/metaso-h3/README.md | 22 + plugin-packages/metaso-h3/docs/interface.md | 437 +++ plugin-packages/metaso-h3/manifest.json | 367 +++ plugin-packages/openai-images.yingce-plugin | Bin 9919 -> 9471 bytes .../openai-images/docs/interface.md | 186 +- plugin-packages/openai-images/manifest.json | 176 +- plugin-packages/src-a6api/README.md | 20 + plugin-packages/src-a6api/docs/interface.md | 29 + plugin-packages/src-a6api/manifest.json | 143 + .../src-antigravity-proxy/README.md | 41 + .../src-antigravity-proxy/docs/interface.md | 70 + .../src-antigravity-proxy/manifest.json | 68 + plugin-packages/src-metaso-h3/README.md | 22 + .../src-metaso-h3/docs/interface.md | 61 + plugin-packages/src-metaso-h3/manifest.json | 100 + scripts/launch-canvas.ps1 | 105 + scripts/start-canvas.ps1 | 56 + scripts/start-local.ps1 | 3 + scripts/windows-proxy.ps1 | 35 + .../canvas/canvas-batch-table-node.tsx | 803 +++--- .../canvas/canvas-cloud-agent-panel.tsx | 1 + web/src/components/canvas/infinite-canvas.tsx | 37 +- web/src/constant/canvas.ts | 2 +- web/src/lib/canvas/canvas-batch-table.ts | 254 +- .../definitions/builtin-nodes.tsx | 9 +- web/src/lib/user-session.ts | 4 +- .../components/channel-model-editor-form.ts | 2 + .../admin/components/channel-model-editor.tsx | 4 + web/src/pages/canvas/project.tsx | 60 +- web/src/pages/canvas/shared.tsx | 2 +- .../pages/canvas/use-canvas-batch-table.ts | 64 +- web/src/pages/canvas/use-canvas-upload.ts | 10 +- web/src/services/api/logical-models.ts | 1 + web/src/services/api/wallet.ts | 2 + web/src/types/canvas.ts | 25 +- web/test/canvas-batch-connection.test.ts | 6 - web/test/canvas-batch-table.test.ts | 239 +- 63 files changed, 9087 insertions(+), 892 deletions(-) create mode 100644 backend/internal/app/ddcat_live_test.go create mode 100644 backend/internal/app/provider_image_boundary_test.go create mode 100644 plugin-packages/a6api.yingce-plugin create mode 100644 plugin-packages/a6api/README.md create mode 100644 plugin-packages/a6api/docs/interface.md create mode 100644 plugin-packages/a6api/manifest.json create mode 100644 plugin-packages/antigravity-proxy.yingce-plugin create mode 100644 plugin-packages/antigravity-proxy/README.md create mode 100644 plugin-packages/antigravity-proxy/docs/interface.md create mode 100644 plugin-packages/antigravity-proxy/manifest.json create mode 100644 plugin-packages/lxmone-video-suite.yingce-plugin create mode 100644 plugin-packages/lxmone-video-suite/README.md create mode 100644 plugin-packages/lxmone-video-suite/docs/interface.md create mode 100644 plugin-packages/lxmone-video-suite/manifest.json create mode 100644 plugin-packages/metaso-h3.yingce-plugin create mode 100644 plugin-packages/metaso-h3/README.md create mode 100644 plugin-packages/metaso-h3/docs/interface.md create mode 100644 plugin-packages/metaso-h3/manifest.json create mode 100644 plugin-packages/src-a6api/README.md create mode 100644 plugin-packages/src-a6api/docs/interface.md create mode 100644 plugin-packages/src-a6api/manifest.json create mode 100644 plugin-packages/src-antigravity-proxy/README.md create mode 100644 plugin-packages/src-antigravity-proxy/docs/interface.md create mode 100644 plugin-packages/src-antigravity-proxy/manifest.json create mode 100644 plugin-packages/src-metaso-h3/README.md create mode 100644 plugin-packages/src-metaso-h3/docs/interface.md create mode 100644 plugin-packages/src-metaso-h3/manifest.json create mode 100644 scripts/launch-canvas.ps1 create mode 100644 scripts/start-canvas.ps1 create mode 100644 scripts/windows-proxy.ps1 diff --git a/.gitignore b/.gitignore index 25947b833..b206b6e3d 100644 --- a/.gitignore +++ b/.gitignore @@ -68,5 +68,3 @@ camera_language_guide.md # Local backend executable; never commit build output. backend/cmd/server/server -# One-off local probe scripts and their compiled output. -backend/tmp_* diff --git a/CHANGELOG.md b/CHANGELOG.md index 88272394e..4ab03091f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,15 @@ - 工作台侧栏增加账号容量进度与签到加油站;创作页加宽对话区、工具条进顶栏、默认图片生成,并修正参数弹窗表面色。 - 画布新增发送到 Agent 快捷入口,修复重生成媒体绑定死锁、素材删除误拦已结束任务,以及图片档位并集与视频提示词上限。 +## v1.4.0 + +- 发布正式版本 `v1.4.0`,提供 `ghcr.io/ddcat-ai/open-ai-canvas-web:1.4.0` 与 `ghcr.io/ddcat-ai/open-ai-canvas-backend:1.4.0` 的 Linux amd64/arm64 镜像,并更新 `latest` 镜像。 +- 管理后台改为独立设计系统,与用户端皮肤隔离。 +- 完善云端 Agent:计划插话与记忆、统一文本模型选择、媒体失败后对话可继续、漏传快照由服务端补齐;生成审批支持改模型参数并取消 8 轮硬限制。 +- 新增虎皮椒聚合支付官方插件与跨平台 provider;积分中心改为全局弹窗,去掉独立钱包页。 +- 工作台侧栏增加账号容量进度与签到加油站;创作页加宽对话区、工具条进顶栏、默认图片生成,并修正参数弹窗表面色。 +- 画布新增发送到 Agent 快捷入口,修复重生成媒体绑定死锁、素材删除误拦已结束任务,以及图片档位并集与视频提示词上限。 + ## v1.3.0 - 发布正式版本 `v1.3.0`,提供 `ghcr.io/ddcat-ai/open-ai-canvas-web:1.3.0` 与 `ghcr.io/ddcat-ai/open-ai-canvas-backend:1.3.0` 的 Linux amd64/arm64 镜像,并更新 `latest` 镜像。 diff --git a/README.md b/README.md index d933eff43..9ef625465 100644 --- a/README.md +++ b/README.md @@ -412,11 +412,5 @@ Issue 反馈、技术讨论和产品升级建议可以在微信交流群中沟 最难不过坚持
- - 春秋 - 春秋
Agent记忆核心贡献者 · VV:A13068788886

- 558328@qq.com
-
- diff --git a/backend/internal/app/channel_models.go b/backend/internal/app/channel_models.go index d105dade0..a218405c7 100644 --- a/backend/internal/app/channel_models.go +++ b/backend/internal/app/channel_models.go @@ -22,6 +22,7 @@ type ChannelModelRequest struct { ProviderModelKey string `json:"providerModelKey"` DisplayName string `json:"displayName"` Icon string `json:"icon"` + Description string `json:"description"` Capability string `json:"capability"` Protocol string `json:"protocol"` BillingMode string `json:"billingMode"` @@ -351,6 +352,7 @@ func (s *Service) SaveAdminChannelModel(actor *model.User, channelID string, id item.DisplayName = modelKey } item.Icon = strings.TrimSpace(req.Icon) + item.Description = truncateRunes(strings.TrimSpace(req.Description), 600) item.Capability = capability item.Protocol = protocol s.applyChannelModelPriceTierSummary(item, tiers) diff --git a/backend/internal/app/ddcat_live_test.go b/backend/internal/app/ddcat_live_test.go new file mode 100644 index 000000000..b9b0ec232 --- /dev/null +++ b/backend/internal/app/ddcat_live_test.go @@ -0,0 +1,60 @@ +package app + +import ( + "bytes" + "context" + "encoding/base64" + "image" + _ "image/png" + "os" + "strings" + "testing" + "time" +) + +// Opt-in only: this test makes two paid requests and never logs credentials or payloads. +func TestDDCatLiveImageRoundTrip(t *testing.T) { + key := os.Getenv("DDCAT_SMOKE_API_KEY") + if key == "" { + t.Skip("set DDCAT_SMOKE_API_KEY to explicitly enable paid live verification") + } + ctx, cancel := context.WithTimeout(context.Background(), 8*time.Minute) + defer cancel() + ctx = withProtocolRegistry(ctx, loadOfficialFallbackRegistry()) + input := canvasGenerationInput{Mode: "image", Prompt: "A single blue ceramic cup on a plain white background, no text.", Config: providerConfig{BaseURL: "https://api.ddcat.pronhubcn.com/v1", APIKey: key, Model: "gpt-image-2.5", InterfaceType: "openai-image", Size: "1:1"}} + for _, mode := range []string{"generate", "edit"} { + result, err := runImageTask(ctx, input) + if err != nil { + t.Fatalf("%s failed: %v", mode, err) + } + images, ok := result["images"].([]interface{}) + if !ok || len(images) == 0 { + t.Fatalf("%s returned no images", mode) + } + item, ok := images[0].(map[string]interface{}) + if !ok { + t.Fatal("invalid image result") + } + dataURL := stringField(item, "dataUrl") + _, encoded, ok := strings.Cut(dataURL, ",") + if !ok { + t.Fatal("missing inline image") + } + raw, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + t.Fatal("invalid image base64") + } + info, format, err := image.DecodeConfig(bytes.NewReader(raw)) + if err != nil { + t.Fatalf("invalid image bytes: %v", err) + } + t.Logf("%s: %s %dx%d, %d bytes", mode, format, info.Width, info.Height, len(raw)) + if dir := os.Getenv("DDCAT_SMOKE_OUTPUT"); dir != "" { + if err := os.WriteFile(dir+"/"+mode+".png", raw, 0600); err != nil { + t.Fatal(err) + } + } + input.ReferenceImages = []providerMedia{{Name: "reference.png", Type: "image/png", DataURL: dataURL}} + input.Prompt = "Change the blue cup to red. Keep the white background and composition." + } +} diff --git a/backend/internal/app/model_catalog.go b/backend/internal/app/model_catalog.go index 0fa48db2a..aff2f4a41 100644 --- a/backend/internal/app/model_catalog.go +++ b/backend/internal/app/model_catalog.go @@ -44,6 +44,7 @@ type PublicChannelModel struct { DisplayName string `json:"displayName"` SortOrder int `json:"sortOrder"` Icon string `json:"icon"` + Description string `json:"description,omitempty"` Capability string `json:"capability"` Protocol model.ChannelInterfaceType `json:"protocol"` CapabilityConfig map[string]any `json:"capabilityConfig,omitempty"` @@ -204,6 +205,7 @@ func (s *Service) sanitizeChannelModel(cm *model.ChannelModel) (PublicChannelMod DisplayName: cm.DisplayName, SortOrder: cm.SortOrder, Icon: cm.Icon, + Description: cm.Description, Capability: cm.Capability, Protocol: cm.Protocol, CapabilityConfig: capabilityConfig, diff --git a/backend/internal/app/provider_http_client.go b/backend/internal/app/provider_http_client.go index 5a7370aa1..7995a6798 100644 --- a/backend/internal/app/provider_http_client.go +++ b/backend/internal/app/provider_http_client.go @@ -271,6 +271,7 @@ func doBinaryWithConsumer(req *http.Request, onChunk func(string, []byte)) ([]by client := OutboundHTTPClient(requestTimeout) resp, err := client.Do(req) if err != nil { + err = providerConnectionError(err) if runtimeService != nil { _ = runtimeService.RecordChannelResult(req.Context(), channelID, !errors.Is(err, context.Canceled)) } @@ -304,6 +305,7 @@ func doBinaryWithConsumer(req *http.Request, onChunk func(string, []byte)) ([]by break } if readErr != nil { + readErr = providerConnectionError(readErr) recordProviderRequest(req, startedAt, resp.StatusCode, buffered.Bytes(), readErr) return nil, "", readErr } @@ -329,6 +331,13 @@ func doBinaryWithConsumer(req *http.Request, onChunk func(string, []byte)) ([]by return data, mimeType, nil } +func providerConnectionError(err error) error { + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { + return fmt.Errorf("模型服务连接提前关闭,未收到完整结果;请先核对中转站任务和扣费记录,再决定是否重试:%w", err) + } + return err +} + func parseRetryAfter(value string, now time.Time) time.Duration { value = strings.TrimSpace(value) if value == "" { diff --git a/backend/internal/app/provider_image.go b/backend/internal/app/provider_image.go index 38d1b27d1..7d10ad9b7 100644 --- a/backend/internal/app/provider_image.go +++ b/backend/internal/app/provider_image.go @@ -19,6 +19,11 @@ import ( ) func runImageTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) { + if input.Config.InterfaceType == string(model.ChannelInterfaceOpenAIImage) { + if err := validateOpenAIImageInput(input); err != nil { + return nil, err + } + } if _, ok := declarativeProtocolAdapterForContext(ctx, input.Config.InterfaceType); ok { return runDeclarativeProtocolTask(ctx, input) } @@ -113,6 +118,30 @@ func runImageTask(ctx context.Context, input canvasGenerationInput) (map[string] return map[string]interface{}{"mode": "image", "images": images}, nil } +// Validate before selecting the plugin so declarative and handwritten paths share +// the same write boundary. Invalid explicit sizes must not silently become auto. +func validateOpenAIImageInput(input canvasGenerationInput) error { + if input.Mask != nil && len(input.ReferenceImages) == 0 { + return errors.New("蒙版编辑必须提供与蒙版同尺寸的源图片") + } + raw := strings.TrimSpace(input.Config.Size) + if raw == "" || raw == "auto" { + return nil + } + size := normalizePixelSize(raw) + widthText, heightText, ok := strings.Cut(size, "x") + width, widthErr := strconv.Atoi(widthText) + height, heightErr := strconv.Atoi(heightText) + if !ok || widthErr != nil || heightErr != nil || width <= 0 || height <= 0 { + return errors.New("图片尺寸无效:请选择支持的宽高比或填写正整数像素尺寸,如 1024x1024") + } + base, err := url.Parse(input.Config.BaseURL) + if err == nil && strings.EqualFold(base.Hostname(), "api.ddcat.pronhubcn.com") && (width > 4096 || height > 4096) { + return errors.New("ddcat 图片尺寸超过上游限制:宽和高均不得超过 4096 像素") + } + return nil +} + func runGeminiImageTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) { if input.Mask != nil { return nil, errors.New("Gemini Images 不支持蒙版编辑,请移除蒙版后重试") diff --git a/backend/internal/app/provider_image_boundary_test.go b/backend/internal/app/provider_image_boundary_test.go new file mode 100644 index 000000000..ef5157ca4 --- /dev/null +++ b/backend/internal/app/provider_image_boundary_test.go @@ -0,0 +1,65 @@ +package app + +import ( + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestOpenAIImageRejectsInvalidSizeBeforeSubmission(t *testing.T) { + for _, size := range []string{"banana", "7:0", "0x1024", "-1x1024", "4097x1024", "1024x5000"} { + t.Run(size, func(t *testing.T) { + _, err := runImageTask(context.Background(), canvasGenerationInput{Mode: "image", Config: providerConfig{InterfaceType: "openai-image", BaseURL: "https://api.ddcat.pronhubcn.com/v1", Size: size}}) + if err == nil || !strings.Contains(err.Error(), "尺寸") { + t.Fatalf("expected local size rejection: %v", err) + } + }) + } + for _, size := range []string{"", "auto", "1:1", "16:9", "1024x1024", "4096x4096"} { + if err := validateOpenAIImageInput(canvasGenerationInput{Config: providerConfig{Size: size}}); err != nil { + t.Fatalf("valid size %q: %v", size, err) + } + } +} + +func TestOpenAIImageRejectsMaskWithoutSourceBeforePlugin(t *testing.T) { + _, err := runImageTask(context.Background(), canvasGenerationInput{Mode: "image", Config: providerConfig{InterfaceType: "openai-image"}, Mask: &providerMedia{ID: "mask"}}) + if err == nil || !strings.Contains(err.Error(), "源图片") { + t.Fatalf("expected missing source rejection: %v", err) + } +} + +func TestProviderConnectionErrorPreservesCause(t *testing.T) { + for _, cause := range []error{io.EOF, io.ErrUnexpectedEOF} { + err := providerConnectionError(cause) + if !errors.Is(err, cause) || !strings.Contains(err.Error(), "扣费记录") { + t.Fatalf("unexpected error: %v", err) + } + } + if providerConnectionError(context.Canceled) != context.Canceled { + t.Fatal("cancellation must remain unchanged") + } +} + +func TestImageSubmissionEOFIsNotAutomaticallyReplayed(t *testing.T) { + t.Setenv("CANVAS_ALLOW_PRIVATE_UPSTREAMS", "true") + calls := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + conn, _, err := w.(http.Hijacker).Hijack() + if err != nil { + t.Error(err) + return + } + conn.Close() + })) + defer server.Close() + err := postJSON(context.Background(), providerConfig{BaseURL: server.URL}, "/v1/images/generations", map[string]string{"prompt": "cup"}, &map[string]interface{}{}) + if err == nil || !strings.Contains(err.Error(), "连接提前关闭") || calls != 1 { + t.Fatalf("calls=%d error=%v", calls, err) + } +} diff --git a/backend/internal/app/provider_protocol.go b/backend/internal/app/provider_protocol.go index 2e98b1098..9a287f8a2 100644 --- a/backend/internal/app/provider_protocol.go +++ b/backend/internal/app/provider_protocol.go @@ -22,6 +22,7 @@ import ( "strings" "time" + "infinite-canvas/backend/internal/model" "infinite-canvas/backend/internal/protocol" "github.com/google/uuid" @@ -186,6 +187,10 @@ func protocolRequestFromInput(input canvasGenerationInput) protocol.GenerationRe resolution = declared } } + aspectRatio := input.Config.Size + if input.Mode == "image" && strings.TrimSpace(input.Config.InterfaceType) == string(model.ChannelInterfaceOpenAIImage) { + aspectRatio = normalizePixelSize(aspectRatio) + } request := protocol.GenerationRequest{ Capability: protocol.Capability(input.Mode), Model: input.Config.Model, @@ -194,7 +199,7 @@ func protocolRequestFromInput(input canvasGenerationInput) protocol.GenerationRe Images: protocolImageReferences(input), Videos: protocolMediaReferences(input.ReferenceVideos, "video"), Audios: protocolMediaReferences(input.ReferenceAudios, "audio"), - AspectRatio: input.Config.Size, + AspectRatio: aspectRatio, Resolution: resolution, Quality: input.Config.Quality, GenerateAudio: parseBool(input.Config.VideoGenerateAudio, false), diff --git a/backend/internal/database/migrations.go b/backend/internal/database/migrations.go index d3837796c..479a9cdf7 100644 --- a/backend/internal/database/migrations.go +++ b/backend/internal/database/migrations.go @@ -11,7 +11,7 @@ import ( "gorm.io/gorm" ) -const CurrentSchemaVersion int64 = 23 +const CurrentSchemaVersion int64 = 24 const baselineSchemaChecksum = "sha256:open-ai-canvas-schema-v1-20260830" const schemaMigrationAppliedAtIndexChecksum = "sha256:schema-migrations-applied-at-index-v2-20260830" @@ -89,6 +89,16 @@ var schemaMigrations = []migration{ {version: 23, name: "canvas_revision_history", checksum: "sha256:canvas-revision-history-v23-20260918", apply: func(tx *gorm.DB) error { return tx.AutoMigrate(&model.CanvasProject{}, &model.CanvasSnapshot{}, &model.CanvasSnapshotResource{}) }}, + {version: 24, name: "channel_model_description", checksum: "sha256:channel-model-description-v24-20260918", apply: migrateSchemaV24}, +} + +// migrateSchemaV24 为系统渠道模型补充面向用户的说明字段。 +// 说明只用于前台选型展示,存量模型保持空说明,不影响价格、路由与结算。 +func migrateSchemaV24(tx *gorm.DB) error { + if !tx.Migrator().HasColumn(&model.ChannelModel{}, "Description") { + return tx.Migrator().AddColumn(&model.ChannelModel{}, "Description") + } + return nil } func migrateSchemaV14(tx *gorm.DB) error { diff --git a/backend/internal/model/models_channel.go b/backend/internal/model/models_channel.go index 609db0934..07eb1abd2 100644 --- a/backend/internal/model/models_channel.go +++ b/backend/internal/model/models_channel.go @@ -44,6 +44,7 @@ type ChannelModel struct { DisplayName string `json:"displayName" gorm:"size:160"` SortOrder int `json:"sortOrder" gorm:"not null;default:0"` Icon string `json:"icon" gorm:"size:80"` + Description string `json:"description" gorm:"size:600"` Capability string `json:"capability" gorm:"size:32;index"` Protocol ChannelInterfaceType `json:"protocol" gorm:"size:32;index"` BillingMode string `json:"billingMode" gorm:"size:32"` diff --git a/docker-compose.deploy.yml b/docker-compose.deploy.yml index cfe4d3fe3..d7858ba8a 100644 --- a/docker-compose.deploy.yml +++ b/docker-compose.deploy.yml @@ -1,6 +1,8 @@ services: postgres: image: postgres:17-alpine + security_opt: + - seccomp:unconfined environment: POSTGRES_DB: ${POSTGRES_DB:-open_ai_canvas} POSTGRES_USER: ${POSTGRES_USER:-open_ai_canvas} @@ -27,7 +29,7 @@ services: restart: unless-stopped migrate: - image: ghcr.io/ddcat-ai/open-ai-canvas-backend:${CANVAS_IMAGE_TAG:-latest} + image: ghcr.io/15657306288/open-ai-canvas-backend:${CANVAS_IMAGE_TAG:-latest} command: ["migrate-schema", "up"] environment: CANVAS_BACKEND_DATA_DIR: /data @@ -38,7 +40,7 @@ services: condition: service_healthy backend: - image: ghcr.io/ddcat-ai/open-ai-canvas-backend:${CANVAS_IMAGE_TAG:-latest} + image: ghcr.io/15657306288/open-ai-canvas-backend:${CANVAS_IMAGE_TAG:-latest} environment: CANVAS_BACKEND_ADDR: ":8080" CANVAS_BACKEND_DATA_DIR: /data @@ -70,7 +72,7 @@ services: stop_grace_period: ${CANVAS_SHUTDOWN_TIMEOUT:-10m} web: - image: ghcr.io/ddcat-ai/open-ai-canvas-web:${CANVAS_IMAGE_TAG:-latest} + image: ghcr.io/15657306288/open-ai-canvas-web:${CANVAS_IMAGE_TAG:-latest} ports: - "${CANVAS_HTTP_PORT:-3000}:3000" depends_on: diff --git a/plugin-packages/a6api.yingce-plugin b/plugin-packages/a6api.yingce-plugin new file mode 100644 index 0000000000000000000000000000000000000000..cd70adef534b9c7fa8db6ba287d2e0d228a109b3 GIT binary patch literal 8332 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 literal 0 HcmV?d00001 diff --git a/plugin-packages/a6api/README.md b/plugin-packages/a6api/README.md new file mode 100644 index 000000000..afeacbe92 --- /dev/null +++ b/plugin-packages/a6api/README.md @@ -0,0 +1,20 @@ +# A6api + +A6api 模型网关协议插件。统一入口 `https://api.a6api.com`,OpenAI 兼容(`Authorization: Bearer `)。 + +## Providers + +| Provider | 能力 | 端点 | +|---|---|---| +| a6api-chat | text | POST /chat/completions | +| a6api-image | image | POST /images/generations、POST /images/edits | + +## 配置 + +在渠道中填写 API Key(A6api 控制台创建,形如 `sk-xxxxxxxx`),baseUrl 使用 `https://api.a6api.com`。 + +## 说明 + +- 模型名以 A6api 模型市场当前列表为准。 +- 图像编辑(带参考图)走 `/images/edits` multipart 请求。 +- 错误体为 OpenAI 风格,携带 `request_id`。 diff --git a/plugin-packages/a6api/docs/interface.md b/plugin-packages/a6api/docs/interface.md new file mode 100644 index 000000000..d50b716d3 --- /dev/null +++ b/plugin-packages/a6api/docs/interface.md @@ -0,0 +1,522 @@ +# A6api 接口说明 + +## 鉴权 + +`Authorization: Bearer sk-xxxxxxxxxxxxxxxxxxxxxxxx` + +## 文本对话 + +POST `https://api.a6api.com/chat/completions` + +请求体 OpenAI Chat Completions 格式:`model`、`messages`、`temperature`、`max_tokens`、`tools`、`tool_choice`、`response_format`、`stream` 等。 + +响应 `choices[0].message.content` 为正文,`choices[0].message.reasoning_content` 为推理内容,`usage` 为用量。 + +## 图像生成 + +POST `https://api.a6api.com/images/generations` + +请求体:`model`、`prompt`、`n`、`size`、`quality`、`response_format`。 + +响应 `data[].url` 或 `data[].b64_json`。 + +## 图像编辑 + +POST `https://api.a6api.com/images/edits`(multipart,带参考图时使用) + +## 模型列表 + +GET `https://api.a6api.com/models` + + +## Manifest 完整接口定义 + +以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。 + +```json +{ + "apiVersion": "yingce.plugin/v2", + "id": "a6api", + "name": "A6api", + "version": "1.0.0", + "author": "A6api", + "description": "A6api 模型网关协议插件:OpenAI 兼容对话与图像生成。", + "permissions": [ + "generation.run", + "media.read" + ], + "configuration": { + "fields": [ + { + "name": "apiKey", + "type": "secret", + "label": "API Key", + "required": true + } + ] + }, + "contributes": { + "providers": [ + { + "id": "a6api-chat", + "label": "A6api Chat", + "capabilities": [ + "text" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://api.a6api.com", + "auth": { + "type": "bearer", + "field": "apiKey" + }, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "A6api 模型市场中的模型 ID。" + }, + { + "name": "messages", + "type": "message[]", + "required": true, + "mapping": "provider message container", + "description": "包含历史消息和当前用户输入。" + }, + { + "name": "instructions", + "type": "string", + "required": false, + "mapping": "system/instructions", + "description": "系统指令。" + }, + { + "name": "temperature", + "type": "number", + "required": false, + "mapping": "temperature", + "description": "采样温度。" + }, + { + "name": "top_p", + "type": "number", + "required": false, + "mapping": "top_p", + "description": "核采样参数。" + }, + { + "name": "max_tokens", + "type": "integer", + "required": false, + "mapping": "max_tokens/max_output_tokens", + "description": "最大输出 token。" + }, + { + "name": "tools", + "type": "array", + "required": false, + "mapping": "tools/toolConfig", + "description": "工具定义。" + }, + { + "name": "tool_choice", + "type": "object|string", + "required": false, + "mapping": "tool_choice", + "description": "工具选择策略。" + }, + { + "name": "response_format", + "type": "object", + "required": false, + "mapping": "response_format/text", + "description": "结构化输出配置。" + }, + { + "name": "stream", + "type": "boolean", + "required": false, + "mapping": "stream", + "description": "流式开关;后台任务当前以最终响应归一。" + }, + { + "name": "providerOptions", + "type": "object", + "required": false, + "mapping": "provider-specific fields", + "description": "插件命名空间内的厂商扩展字段。" + } + ], + "create": { + "method": "POST", + "path": "/chat/completions", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "messages": { + "$ref": "request.messages" + }, + "temperature": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.temperature" + } + }, + "top_p": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.top_p" + } + }, + "max_tokens": { + "$omitEmpty": { + "$coalesce": [ + { + "$ref": "request.extra.max_tokens" + }, + { + "$ref": "request.providerOptions.a6api-chat.max_tokens" + } + ] + } + }, + "tools": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.tools" + } + }, + "tool_choice": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.tool_choice" + } + }, + "response_format": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.response_format" + } + }, + "stream": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.stream" + } + }, + "stop": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.stop" + } + }, + "seed": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.seed" + } + }, + "frequency_penalty": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.frequency_penalty" + } + }, + "presence_penalty": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.presence_penalty" + } + }, + "user": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.user" + } + } + } + }, + "response": { + "status": "succeeded", + "textPaths": [ + "choices.0.message.content", + "choices.0.text" + ], + "reasoningPaths": [ + "choices.0.message.reasoning_content" + ], + "usage": { + "$ref": "response.usage" + }, + "errorPaths": [ + "error.code" + ], + "messagePaths": [ + "error.message" + ] + } + }, + { + "id": "a6api-image", + "label": "A6api Image", + "capabilities": [ + "image" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://api.a6api.com", + "auth": { + "type": "bearer", + "field": "apiKey" + }, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "A6api 模型市场中的图像模型 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": "/images/generations", + "pathTemplate": { + "$if": { + "condition": { + "$gt": [ + { + "$len": { + "$ref": "request.images" + } + }, + 0 + ] + }, + "then": "/images/edits", + "else": "/images/generations" + } + }, + "contentType": "application/json", + "contentTypeTemplate": { + "$if": { + "condition": { + "$gt": [ + { + "$len": { + "$ref": "request.images" + } + }, + 0 + ] + }, + "then": "multipart/form-data", + "else": "application/json" + } + }, + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "n": { + "$omitEmpty": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.imageCount" + }, + 0 + ] + }, + "then": { + "$ref": "request.imageCount" + }, + "else": 1 + } + } + }, + "size": { + "$omitEmpty": { + "$ref": "request.aspectRatio" + } + }, + "quality": { + "$omitEmpty": { + "$ref": "request.quality" + } + }, + "output_format": { + "$omitEmpty": { + "$coalesce": [ + { + "$ref": "request.providerOptions.a6api-image.output_format" + }, + "png" + ] + } + }, + "response_format": { + "$omitEmpty": { + "$coalesce": [ + { + "$ref": "request.providerOptions.a6api-image.response_format" + }, + "b64_json" + ] + } + }, + "user": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-image.user" + } + } + }, + "files": [ + { + "name": "image", + "source": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$ne": [ + { + "$ref": "media.role" + }, + "mask" + ] + } + } + }, + "filename": "source.png" + }, + { + "name": "mask", + "source": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$eq": [ + { + "$ref": "media.role" + }, + "mask" + ] + } + } + }, + "filename": "mask.png" + } + ] + }, + "response": { + "status": "succeeded", + "images": { + "$map": { + "from": { + "$ref": "response.data" + }, + "as": "item", + "in": { + "url": { + "$omitEmpty": { + "$ref": "item.url" + } + }, + "dataUrl": { + "$if": { + "condition": { + "$ref": "item.b64_json" + }, + "then": { + "$concat": [ + "data:image/png;base64,", + { + "$ref": "item.b64_json" + } + ] + }, + "else": null + } + } + } + } + }, + "usage": { + "$ref": "response.usage" + }, + "errorPaths": [ + "error.code" + ], + "messagePaths": [ + "error.message" + ] + } + } + ] + }, + "documentation": "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>" +} +``` + diff --git a/plugin-packages/a6api/manifest.json b/plugin-packages/a6api/manifest.json new file mode 100644 index 000000000..2278f2780 --- /dev/null +++ b/plugin-packages/a6api/manifest.json @@ -0,0 +1,484 @@ +{ + "apiVersion": "yingce.plugin/v2", + "id": "a6api", + "name": "A6api", + "version": "1.0.0", + "author": "A6api", + "description": "A6api 模型网关协议插件:OpenAI 兼容对话与图像生成。", + "permissions": [ + "generation.run", + "media.read" + ], + "configuration": { + "fields": [ + { + "name": "apiKey", + "type": "secret", + "label": "API Key", + "required": true + } + ] + }, + "contributes": { + "providers": [ + { + "id": "a6api-chat", + "label": "A6api Chat", + "capabilities": [ + "text" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://api.a6api.com", + "auth": { + "type": "bearer", + "field": "apiKey" + }, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "A6api 模型市场中的模型 ID。" + }, + { + "name": "messages", + "type": "message[]", + "required": true, + "mapping": "provider message container", + "description": "包含历史消息和当前用户输入。" + }, + { + "name": "instructions", + "type": "string", + "required": false, + "mapping": "system/instructions", + "description": "系统指令。" + }, + { + "name": "temperature", + "type": "number", + "required": false, + "mapping": "temperature", + "description": "采样温度。" + }, + { + "name": "top_p", + "type": "number", + "required": false, + "mapping": "top_p", + "description": "核采样参数。" + }, + { + "name": "max_tokens", + "type": "integer", + "required": false, + "mapping": "max_tokens/max_output_tokens", + "description": "最大输出 token。" + }, + { + "name": "tools", + "type": "array", + "required": false, + "mapping": "tools/toolConfig", + "description": "工具定义。" + }, + { + "name": "tool_choice", + "type": "object|string", + "required": false, + "mapping": "tool_choice", + "description": "工具选择策略。" + }, + { + "name": "response_format", + "type": "object", + "required": false, + "mapping": "response_format/text", + "description": "结构化输出配置。" + }, + { + "name": "stream", + "type": "boolean", + "required": false, + "mapping": "stream", + "description": "流式开关;后台任务当前以最终响应归一。" + }, + { + "name": "providerOptions", + "type": "object", + "required": false, + "mapping": "provider-specific fields", + "description": "插件命名空间内的厂商扩展字段。" + } + ], + "create": { + "method": "POST", + "path": "/chat/completions", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "messages": { + "$ref": "request.messages" + }, + "temperature": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.temperature" + } + }, + "top_p": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.top_p" + } + }, + "max_tokens": { + "$omitEmpty": { + "$coalesce": [ + { + "$ref": "request.extra.max_tokens" + }, + { + "$ref": "request.providerOptions.a6api-chat.max_tokens" + } + ] + } + }, + "tools": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.tools" + } + }, + "tool_choice": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.tool_choice" + } + }, + "response_format": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.response_format" + } + }, + "stream": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.stream" + } + }, + "stop": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.stop" + } + }, + "seed": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.seed" + } + }, + "frequency_penalty": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.frequency_penalty" + } + }, + "presence_penalty": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.presence_penalty" + } + }, + "user": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-chat.user" + } + } + } + }, + "response": { + "status": "succeeded", + "textPaths": [ + "choices.0.message.content", + "choices.0.text" + ], + "reasoningPaths": [ + "choices.0.message.reasoning_content" + ], + "usage": { + "$ref": "response.usage" + }, + "errorPaths": [ + "error.code" + ], + "messagePaths": [ + "error.message" + ] + } + }, + { + "id": "a6api-image", + "label": "A6api Image", + "capabilities": [ + "image" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://api.a6api.com", + "auth": { + "type": "bearer", + "field": "apiKey" + }, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "A6api 模型市场中的图像模型 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": "/images/generations", + "pathTemplate": { + "$if": { + "condition": { + "$gt": [ + { + "$len": { + "$ref": "request.images" + } + }, + 0 + ] + }, + "then": "/images/edits", + "else": "/images/generations" + } + }, + "contentType": "application/json", + "contentTypeTemplate": { + "$if": { + "condition": { + "$gt": [ + { + "$len": { + "$ref": "request.images" + } + }, + 0 + ] + }, + "then": "multipart/form-data", + "else": "application/json" + } + }, + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "n": { + "$omitEmpty": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.imageCount" + }, + 0 + ] + }, + "then": { + "$ref": "request.imageCount" + }, + "else": 1 + } + } + }, + "size": { + "$omitEmpty": { + "$ref": "request.aspectRatio" + } + }, + "quality": { + "$omitEmpty": { + "$ref": "request.quality" + } + }, + "output_format": { + "$omitEmpty": { + "$coalesce": [ + { + "$ref": "request.providerOptions.a6api-image.output_format" + }, + "png" + ] + } + }, + "response_format": { + "$omitEmpty": { + "$coalesce": [ + { + "$ref": "request.providerOptions.a6api-image.response_format" + }, + "b64_json" + ] + } + }, + "user": { + "$omitEmpty": { + "$ref": "request.providerOptions.a6api-image.user" + } + } + }, + "files": [ + { + "name": "image", + "source": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$ne": [ + { + "$ref": "media.role" + }, + "mask" + ] + } + } + }, + "filename": "source.png" + }, + { + "name": "mask", + "source": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$eq": [ + { + "$ref": "media.role" + }, + "mask" + ] + } + } + }, + "filename": "mask.png" + } + ] + }, + "response": { + "status": "succeeded", + "images": { + "$map": { + "from": { + "$ref": "response.data" + }, + "as": "item", + "in": { + "url": { + "$omitEmpty": { + "$ref": "item.url" + } + }, + "dataUrl": { + "$if": { + "condition": { + "$ref": "item.b64_json" + }, + "then": { + "$concat": [ + "data:image/png;base64,", + { + "$ref": "item.b64_json" + } + ] + }, + "else": null + } + } + } + } + }, + "usage": { + "$ref": "response.usage" + }, + "errorPaths": [ + "error.code" + ], + "messagePaths": [ + "error.message" + ] + } + } + ] + }, + "documentation": "# A6api\n\nA6api 模型网关协议插件。统一入口 `https://api.a6api.com`,OpenAI 兼容(`Authorization: Bearer `)。\n\n## Providers\n\n| Provider | 能力 | 端点 |\n|---|---|---|\n| a6api-chat | text | POST /chat/completions |\n| a6api-image | image | POST /images/generations、POST /images/edits |\n\n## 配置\n\n在渠道中填写 API Key(A6api 控制台创建,形如 `sk-xxxxxxxx`),baseUrl 使用 `https://api.a6api.com`。\n\n## 说明\n\n- 模型名以 A6api 模型市场当前列表为准。\n- 图像编辑(带参考图)走 `/images/edits` multipart 请求。\n- 错误体为 OpenAI 风格,携带 `request_id`。\n\n---\n\n# A6api 接口说明\n\n## 鉴权\n\n`Authorization: Bearer sk-xxxxxxxxxxxxxxxxxxxxxxxx`\n\n## 文本对话\n\nPOST `https://api.a6api.com/chat/completions`\n\n请求体 OpenAI Chat Completions 格式:`model`、`messages`、`temperature`、`max_tokens`、`tools`、`tool_choice`、`response_format`、`stream` 等。\n\n响应 `choices[0].message.content` 为正文,`choices[0].message.reasoning_content` 为推理内容,`usage` 为用量。\n\n## 图像生成\n\nPOST `https://api.a6api.com/images/generations`\n\n请求体:`model`、`prompt`、`n`、`size`、`quality`、`response_format`。\n\n响应 `data[].url` 或 `data[].b64_json`。\n\n## 图像编辑\n\nPOST `https://api.a6api.com/images/edits`(multipart,带参考图时使用)\n\n## 模型列表\n\nGET `https://api.a6api.com/models`\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"a6api\",\n \"name\": \"A6api\",\n \"version\": \"1.0.0\",\n \"author\": \"A6api\",\n \"description\": \"A6api 模型网关协议插件:OpenAI 兼容对话与图像生成。\",\n \"permissions\": [\n \"generation.run\",\n \"media.read\"\n ],\n \"configuration\": {\n \"fields\": [\n {\n \"name\": \"apiKey\",\n \"type\": \"secret\",\n \"label\": \"API Key\",\n \"required\": true\n }\n ]\n },\n \"contributes\": {\n \"providers\": [\n {\n \"id\": \"a6api-chat\",\n \"label\": \"A6api Chat\",\n \"capabilities\": [\n \"text\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://api.a6api.com\",\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"A6api 模型市场中的模型 ID。\"\n },\n {\n \"name\": \"messages\",\n \"type\": \"message[]\",\n \"required\": true,\n \"mapping\": \"provider message container\",\n \"description\": \"包含历史消息和当前用户输入。\"\n },\n {\n \"name\": \"instructions\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"system/instructions\",\n \"description\": \"系统指令。\"\n },\n {\n \"name\": \"temperature\",\n \"type\": \"number\",\n \"required\": false,\n \"mapping\": \"temperature\",\n \"description\": \"采样温度。\"\n },\n {\n \"name\": \"top_p\",\n \"type\": \"number\",\n \"required\": false,\n \"mapping\": \"top_p\",\n \"description\": \"核采样参数。\"\n },\n {\n \"name\": \"max_tokens\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"max_tokens/max_output_tokens\",\n \"description\": \"最大输出 token。\"\n },\n {\n \"name\": \"tools\",\n \"type\": \"array\",\n \"required\": false,\n \"mapping\": \"tools/toolConfig\",\n \"description\": \"工具定义。\"\n },\n {\n \"name\": \"tool_choice\",\n \"type\": \"object|string\",\n \"required\": false,\n \"mapping\": \"tool_choice\",\n \"description\": \"工具选择策略。\"\n },\n {\n \"name\": \"response_format\",\n \"type\": \"object\",\n \"required\": false,\n \"mapping\": \"response_format/text\",\n \"description\": \"结构化输出配置。\"\n },\n {\n \"name\": \"stream\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"stream\",\n \"description\": \"流式开关;后台任务当前以最终响应归一。\"\n },\n {\n \"name\": \"providerOptions\",\n \"type\": \"object\",\n \"required\": false,\n \"mapping\": \"provider-specific fields\",\n \"description\": \"插件命名空间内的厂商扩展字段。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/chat/completions\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"messages\": {\n \"$ref\": \"request.messages\"\n },\n \"temperature\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.a6api-chat.temperature\"\n }\n },\n \"top_p\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.a6api-chat.top_p\"\n }\n },\n \"max_tokens\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.extra.max_tokens\"\n },\n {\n \"$ref\": \"request.providerOptions.a6api-chat.max_tokens\"\n }\n ]\n }\n },\n \"tools\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.a6api-chat.tools\"\n }\n },\n \"tool_choice\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.a6api-chat.tool_choice\"\n }\n },\n \"response_format\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.a6api-chat.response_format\"\n }\n },\n \"stream\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.a6api-chat.stream\"\n }\n },\n \"stop\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.a6api-chat.stop\"\n }\n },\n \"seed\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.a6api-chat.seed\"\n }\n },\n \"frequency_penalty\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.a6api-chat.frequency_penalty\"\n }\n },\n \"presence_penalty\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.a6api-chat.presence_penalty\"\n }\n },\n \"user\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.a6api-chat.user\"\n }\n }\n }\n },\n \"response\": {\n \"status\": \"succeeded\",\n \"textPaths\": [\n \"choices.0.message.content\",\n \"choices.0.text\"\n ],\n \"reasoningPaths\": [\n \"choices.0.message.reasoning_content\"\n ],\n \"usage\": {\n \"$ref\": \"response.usage\"\n },\n \"errorPaths\": [\n \"error.code\"\n ],\n \"messagePaths\": [\n \"error.message\"\n ]\n }\n },\n {\n \"id\": \"a6api-image\",\n \"label\": \"A6api Image\",\n \"capabilities\": [\n \"image\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://api.a6api.com\",\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"A6api 模型市场中的图像模型 ID。\"\n },\n {\n \"name\": \"prompt\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"prompt\",\n \"description\": \"图片提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"provider image/reference fields\",\n \"description\": \"参考图或编辑源图,role 由业务层确定。\"\n },\n {\n \"name\": \"imageCount\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"n/sample_count\",\n \"description\": \"输出数量。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"size/aspect_ratio\",\n \"description\": \"比例或尺寸,语义按协议说明。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution/imageSize\",\n \"description\": \"分辨率档位。\"\n },\n {\n \"name\": \"quality\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"quality\",\n \"description\": \"质量档位。\"\n },\n {\n \"name\": \"providerOptions\",\n \"type\": \"object\",\n \"required\": false,\n \"mapping\": \"provider-specific fields\",\n \"description\": \"插件命名空间内的厂商扩展字段。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/images/generations\",\n \"pathTemplate\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 0\n ]\n },\n \"then\": \"/images/edits\",\n \"else\": \"/images/generations\"\n }\n },\n \"contentType\": \"application/json\",\n \"contentTypeTemplate\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 0\n ]\n },\n \"then\": \"multipart/form-data\",\n \"else\": \"application/json\"\n }\n },\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"n\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$ref\": \"request.imageCount\"\n },\n 0\n ]\n },\n \"then\": {\n \"$ref\": \"request.imageCount\"\n },\n \"else\": 1\n }\n }\n },\n \"size\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.aspectRatio\"\n }\n },\n \"quality\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.quality\"\n }\n },\n \"output_format\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.a6api-image.output_format\"\n },\n \"png\"\n ]\n }\n },\n \"response_format\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.a6api-image.response_format\"\n },\n \"b64_json\"\n ]\n }\n },\n \"user\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.a6api-image.user\"\n }\n }\n },\n \"files\": [\n {\n \"name\": \"image\",\n \"source\": {\n \"$filter\": {\n \"from\": {\n \"$ref\": \"request.images\"\n },\n \"as\": \"media\",\n \"where\": {\n \"$ne\": [\n {\n \"$ref\": \"media.role\"\n },\n \"mask\"\n ]\n }\n }\n },\n \"filename\": \"source.png\"\n },\n {\n \"name\": \"mask\",\n \"source\": {\n \"$filter\": {\n \"from\": {\n \"$ref\": \"request.images\"\n },\n \"as\": \"media\",\n \"where\": {\n \"$eq\": [\n {\n \"$ref\": \"media.role\"\n },\n \"mask\"\n ]\n }\n }\n },\n \"filename\": \"mask.png\"\n }\n ]\n },\n \"response\": {\n \"status\": \"succeeded\",\n \"images\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"response.data\"\n },\n \"as\": \"item\",\n \"in\": {\n \"url\": {\n \"$omitEmpty\": {\n \"$ref\": \"item.url\"\n }\n },\n \"dataUrl\": {\n \"$if\": {\n \"condition\": {\n \"$ref\": \"item.b64_json\"\n },\n \"then\": {\n \"$concat\": [\n \"data:image/png;base64,\",\n {\n \"$ref\": \"item.b64_json\"\n }\n ]\n },\n \"else\": null\n }\n }\n }\n }\n },\n \"usage\": {\n \"$ref\": \"response.usage\"\n },\n \"errorPaths\": [\n \"error.code\"\n ],\n \"messagePaths\": [\n \"error.message\"\n ]\n }\n }\n ]\n },\n \"documentation\": \"<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>\"\n}\n```\n\n" +} diff --git a/plugin-packages/antigravity-proxy.yingce-plugin b/plugin-packages/antigravity-proxy.yingce-plugin new file mode 100644 index 0000000000000000000000000000000000000000..f5ad9ab1fc3239e846deefd06ff0e12d0ad71f5c GIT binary patch literal 7286 zcmZ{JWl$VIx8>l$-6as*U4jJ*?hNjOy9^K{4DRlOy99TFLkP~`5;V9&a0vSHcB^*l zeYM;Du z2fH|*8LYbC>yiZ=)CrxO57Yuhrgc_WDp?v*jT<_V5IsuK^U?>A~nP{0(h9&sJx1-}dR7GG3 z{LkGT4$>UY(F~)#VNE$LxuXgo->^0%Hbj+ec7}qlv7vPPe|(R@d9l#i(U>Vt1pkIBy1zmIbT2Bn!EFHlJ1PU`8ZNw00e4} z>@~?kqT5aLS6Piq)A=iiEfGZQ>5%&eX#s#lk`T0c`coA}PL@#IKpD7wOXgB(<_sbD zJ{;Vs%CQPjosx+tN!PH(_%W`0Fd@8vs_k8~Le}#a`GaVU3Dh`s4Cq4PQR!|eE|ifM zd`GZZ;_>iVW0A&$S&LIj7W&~0ixSRk%xivrFR2I>b4f)-mvi)EMT9p?EyT+R?Q*;c z#3)o&U7|qiZX#fH$S)%uo}nU|eS@8;CClAd$#A9A9$Z}#i#iaBbQ)LG*ELK8h$@1+ z1-9Bk?cYi3XmThZ3zkWD1ovn(>IZj|Ct*Bfs!F{VRB>`O+x#_iul2|O!sPfD$~TMQ zcr3`nYr`iWMz}wIHrwsDR6)sDp1n?uA)>243zPONm9t8ekZd1`r+XuN*mc#xYmVMu za=(p_k*Mu?5zDd5XN8CT@BkmI7?^(k4GYoAm-$pp)tyKBE`fk7oDVs!-i-8RTm97g ztO)_i5~v2HV_lPqP6=eOYJSFGH<43%YcPQs%o!e9Bb{wXfze^BCyS>ii`j-1T|~6D z`rOLh(82K7Usa+kdvRFibH#$wqXfh**BJ&)chi*UG%(tJJkG77~5br^uzuA@h8ffhWNQ!Jxv=`?SR#+;H8emJ#5=6fE z%_zU`I&FOEiy+6`;_IA#!Mb!Kya;8Uhl z46E;x1@dFl@8K+Y7x=#Ny$Hu0D^-|C?U~-4+|OKnF!0@|SeTB}e(biNU#vzcwRR#} zH;@o%n>mNcwh++jJsJ9jLj@pJd(AL9Oib%NjgRPhd5}K7#7YGJmY8(nEVVByroA#t z<1oLa>1}O~gAV?Cg6{7j0zIt)0ejNe%9IMVsDF*>0Wh~Ty011T|@lkL& z5NykZ;Pi-XnIwYuEc-}LWwXs)IrMgCeZ3gsZIl{d{2c5))$Ip!0ABCX7LypRDM6um z8B=k>c-wSeO0Z|~|&(dnd;ru`qL;nCd#vMsBqHJ0}O8 zQc}+K*Zv)EsCKC+Ut?mG(CMclc{8(%#&1f@39N9SoSmeb_d;$cP@*AMdP*yVO@g9N z%Yd%ALdN;37)A3cw3Byqbu$mfsS4BYd}Eq!#8F6YJv0toPF}$#-skM%c#&gg9xaorTgf ze?L-u90kH>@C0?mO3=Dqc}atRPf$Bv^>oZR$s`BbhaPD5= zYH*l)3>q^hg~`tnV4-sf4f8e#p#C9j)^9lPY9?Bmp50kYnA-%Y!pIPWJp6%UKY%oX zHz?+Z38y>?3@Mhh(HX=AWb4v*Z+-^>M;k{o-EanmoHd;>l-;rJalekwR5whH>~}kn zJbuHA%zsAwzNnQ`h3d+l@65?c-D*&@VFtPnImEPg^8hdTbYNLxKG%dv8;4Tk%qRFu z;^pL4o4$imZNVd%;ABT7Uw^)Lb2w?a-L7ouvG;#U&*K!{_%&Eo-=n#nW!yjPvGufg z?0W(u2b$uk;=4t_cQQxsg61;$;J|ZHvH1u)?Xet2K=>w<1uGuWde%rY$BYlnGTVw- zmU~A(%~leAm*Hy#CFiA(u}zRj?Y1Mx0EPICgtBRW%wb)ArzZ5(;lGXb-nmUAVwGrvy;@9U(lY)5WQuuo!r+Zi5atr+;!$=NT6J3 zqQ~#xW{7Qp*jeRl%X)Z4id_EpX~A6lf?DT~(I$U{PvGUZ4gJ9t z3MIZ#M$$UL@{-t?=T>CXEO9M>s|!l3rGFi(d^}gPjP1@L zVPm}oitV@GV>i-Qt6GN6SNG3u_n)U=HrLG%%fUmrUL}V9gL2ik+n=LbY6Yu)oE%;otUE8RJ$a*H- z9nul9~Qf){VCoN z%BMD+rEE2?>bT*>F$}}+$_&yxDtn3saMWi$B6+G9!Oa)GqPgA~JYyfnsHst~P#_3W zJ~G}5`{rt+%;Y}G=ZS}Y?uvz+W0c7Uq!G58f_^&Z(EBd7;eF0+4bk%eqsd>~{r;wT zs+$coWh3`P6jdK$0-10AM$3uR+ohgQvJX;ISfs}OUQp*jVi4l-S%X=}2jf$Oyj(HB zsK!^CnIA3Qz47iLoA-XY58D)##7_8Ni@l|0i1MX<>5!uQUb16~Et}}-(slSy_!sbz zLrjA0(5_>O%Nor)m*Zj@<#?ORub2a8WFP$tR7|^^7eBt<4IDa^W>ff+UAkYepR|$w z#c$H8AiLk%)3gq~>8R=W_h_~IZRVZ;Z$AsX-opzESA82O7ZY_!@mcq5qy2J9^?=#+ z_zzaori5!mUgSl_`TB(Fy`JgB@EWcL4Iv%y(F^v7|NFC`UMKgY?+% z9c}!i5-AMG1m)4XZ)m%4l(uPzH`l&LEQKt3yi45r5s^@jpSPyHDd`P9^`x)Yh^R+( zU5~N4Mc6Pl20|pBacG2vv?C<^|D5~byMA9z)ce>aZv7Kp*vo7_0$Z@ce0qVBVpctp zv?FfXGZN~_;Cr!v+C-oC>-*H5#Uwn6DqzUo@fNr&%atQk%f(AL`rUB@X1p!m>=0TK#I-_D{*orA zKho$toukSt+vTS(5IJY3kH635aOh*~w&>M?%JzIbDfI)}9k;frt(G1IraLi`SBsG+ zlKEv!{2s^TOwbb%STl7AX-hHOFmXZoth%~X&U)sI@MNRTIkaR^k_3@ql{O&TGKjn( z4oU#`n%HeX;3jiLUq{OswZbUQJ3&})0~ThmrJBJOoo^r*ho50ev+jupO-&r*;b@Dj zXQ5bAU>@6MR+Wi+R{Z;&kuv$6%YxR+(YgHTGPmuK`J^{^_5YD4^CM7q!{7mcXhZ-2 z^@|nZQGAva|ZB>FG?EaNd7c|F_gCoHLk~>1yK%F0sF0$MGGFM5SqQ`iieRtzC;OO!ZQUyA+C{Ak10j_{~bYeSkj z?*k`GlFP!R=mBJ_XORKrw zI3)X%U1!Tf_gdTmOhAf!}eZ+!5~*8ta-I@kPRFKPCzv1O`bz1-u?i*$%Iu= ziRP3Sjlc{#lGc+CLJ+XW_$ajLloL32m01vAi4$HAP{sKnvPLX|el@G5&*!0~+iB5# z?49@PBY(LK_**21yE2(AOO#OWWN+QTPFVTO=`aW;e43Eh5f}z-SSBd)4is9-`A&@}f$16Ds?&;!N(~m~PZ6Qg$(JZBSvHbfrfb6_>=5XO%TjKAvWF`xPif`2=;BopaXthgn;2_|L?zS+k;eyzYfd$wlwJ z%sjd7cZ=YR=fAk{a%B}}QD)>W3(as@TMnJCldfk3Lu#HP6&-aXldgaV5z*RF;nI{V< zNBY7MdL&OX<(~lhpu%vt6AVELZ4XjO!C$706)OobJ7jZv=9zi9Py+p zs2EFQuk<0zHU0O)}5GE z7|U#XO3HG!)o`-p>=nX<9On*FRFq01MnbwKNiZowA-pkDG5zYJXClFi*H9^zsfWE{20L!5ETa5r7&s2X*RxJ<(LsU}b1bS3Q5@vAeS0~aCFJti$O#NhajbDgB zu7q3vlU9`tS=lhrEPXjGTHD8f$8v6eS9BPW`W_8gJ|9)hk%h?qK?GB;fi4U^nx*_> zhnLst^n^pT;V2V}^5=EOp@?J+-*3dSEwhTo8E9H~srvlv(i5~LTHWf-74At@#l?@a z@g`qG?o0%Mk(vp+tVXnlN}pMx$|7#s@8dp#{@hh;_tLZ^5Cft0wAVX2c}Iodlv3ehiH9WH)5fF{ClQp5cUvi%XUT*bwf0ncfd>6**U*9=MGW zm7^f`3kb`3R9X~U;1H;+%pzZ6+#n&p{^yfsEGy)T3i|TIHB=kyrR}6MW9SuY;k#T* zBOYVSfza~W*Ixo9EohxCK}sT_BYC0f>>G*9o*i(q@EL_AY6^e7bAK_uN35id z{bcI?(x<4{0IAr_xV=$aAR%FQ_s{-S*5ALpENY!<*aD#`G*v(3(JEg+6d^+-Uw?CP zqv(ScAc{9@(j2O)1VE;Wb<(H_r&x1(=20-)e%xziWN=KcZmi9RTQKQU+}|pUC#ZmI z_?v?C=TbWBKD7}q68~Mc%DRe^Kr=h;!Kz$l1sInTm2vVdfl*52Fz&sf+0rn^1NR7T#qY5Lj7)|*xPPE9etv7jg?q51uykqY#-jg-Sr z$^~rxuYuxiKfg&D3%>-B>kfXqtf-_WxpWlZ)FA-CnLj&HQ^!6@-{4ZhXB;IJ^f4?< zWN!L2KZYpr01Nv1c?Z2%ZzEJ$)y46Jsz-vUY51df$#E%PDtjdsd0U#o#IXaGwScQv zxSy>S__`TL=wWU+L*`v~&hzaT(tf$xPicniZOoiv#fl}RUQbIV#k7`&VvW#!+(eqH zNO(@VcAA~c zf>4SE>_8~jGUw7V%L?{^CFw3Be6=tqF-j?z-arz5)F<@dD}pmSJ)1K+sei`U`(#(@ zW8pteva&F|A2)`O`WA@hbFEEvc2G4=U(@I!YQW2|L3)DEM=LUd87B4ng`NWW(--3x zKRKJJw7V+ffRnD}^*OgQ`}?@N`18E%^)tZ^Nug&kuT3=83x9PDxD~cT@cWvs1evhH zL>+^LX#CChf_27725!+G%&_IIY_C7x+EK|>0mXx*Q%EZIt!)*H2*LY@!vpU9U&TP8 z+2uui{-Pt8+o@+q6OzvLL<^X+qUY3tdg$pWx21c>tMF7bQEUG$#Ndz!wM=TW414i6 zd|u{$NjfL~jE*sI`%!Indt+I+3{U+bKE*4MJbLgmM{aPiVsIriZGjYhfJs;OPune{ zAs=Q#nI!~h3h~r~lo8%nhCDHSQjnY8&ieScxMnCv`}eq;|K(Ei_DGpuFovZmVlSBA zhTMQTLD$y8Xq8efMadvcp0kHHV<|RG%zr0M{FCA$8&S*_GsX)Me4sh~tEswWx!uS=Lq}$Itqe= zwD_Ga#+;~b!JO2FmOww)JzBTE5N!_GISc>@RFHs^i|%5Q*=?2}fiu(OnrwPFkL0BR zIODs;?+V+EO*F%aHUYT&MU{uh(SrC@ZCmV$?K(P5<(Ctd-?fEzzR)<$>faon( znOCk)XOnjGz4M2@gjf_~OObrwr)EuiZBw-aT|jc3QmvG8Q+zu*r=0K?_qyk?q2YBe zIKn+^v1UavxOn~BLoK77p5yiMQJs~c%@^+*KxgDoPTa+${^AUW*ieMRuB+ zxtToca5;;tOZ}CA&>wPof$>k45%5?H7q&9y2 zY9RAs$*mC~`v4PM(_zsXjQ89bTPjB`WtiJ2XQTs}9=zm$2%Li^I4;e(p@og(Yo3Pi zrPRW6-B;0APYVI7`rFF9h+HCI%6`-Vm=(8RUwMGC5X_B0@oNax3@I3vT7_v% zyRxkeD(QZIM<2K`q*vg+f^;pD7`Nc+F&>NuzjB3EKeZ^1&Nr|jhd!IwNM`RvAE({Y zvxWqgdqeiSp@jL^3Iq-W%(lZ}_54#Z?`BMAk5<#3SyF{TH#u}LlZFvt(vu$6wuXOE zwOUP9d$72Gh0EhKhhBGYu}gJ9nt6?oK4q>9Yl;wz>EQS*=TGJ|;QWwcF6yMqH}hue z6^R54D1;YJz`5)uE|AhW$Hv8Yee=-55{%->Kf0EjN!v51H|6kZM a&i}(St1BTP|MwGwf4kvdtt0>M?!N%246xz= literal 0 HcmV?d00001 diff --git a/plugin-packages/antigravity-proxy/README.md b/plugin-packages/antigravity-proxy/README.md new file mode 100644 index 000000000..81962c2b8 --- /dev/null +++ b/plugin-packages/antigravity-proxy/README.md @@ -0,0 +1,41 @@ +# Antigravity Proxy + +Antigravity 中转渠道插件:OpenAI Chat Completions 兼容对话协议。 + +## 协议身份 + +- 插件 ID:`antigravity-proxy` +- Provider ID:`antigravity-chat` +- 能力:`text` +- 默认 Base URL:`http://103.242.14.110:8317` +- 鉴权驱动:`bearer` +- 创建:`POST /chat/completions` +- 生命周期:同步响应 + +## 配置字段 + +| 字段 | 类型 | 必填 | 含义 | +| --- | --- | --- | --- | +| `apiKey` | secret | 是 | 中转渠道 API Key | + +## 模型 + +- `gemini-3.8-flash-high` +- `gemini-pro-agent` + +## 统一字段映射 + +| 统一字段 | 类型 | 必填 | 上游映射 | +| --- | --- | --- | --- | +| `model` | string | 是 | `model` | +| `messages` | message[] | 是 | `messages` | +| `instructions` | string | 否 | `system/instructions` | +| `temperature` | number | 否 | `temperature` | +| `top_p` | number | 否 | `top_p` | +| `max_tokens` | integer | 否 | `max_tokens` | +| `tools` | array | 否 | `tools` | +| `tool_choice` | object\|string | 否 | `tool_choice` | +| `response_format` | object | 否 | `response_format` | +| `stream` | boolean | 否 | `stream` | + +完整接口见 [docs/interface.md](docs/interface.md)。 diff --git a/plugin-packages/antigravity-proxy/docs/interface.md b/plugin-packages/antigravity-proxy/docs/interface.md new file mode 100644 index 000000000..66f7d83c9 --- /dev/null +++ b/plugin-packages/antigravity-proxy/docs/interface.md @@ -0,0 +1,303 @@ +# Antigravity Proxy 接口字段 + +## 协议身份 + +- 插件 ID:`antigravity-proxy` +- Provider ID:`antigravity-chat` +- 能力:`text` +- 默认 Base URL:`http://103.242.14.110:8317` +- 鉴权驱动:`bearer` +- 创建:`POST /chat/completions` +- 生命周期:同步响应 + +## 配置字段 + +| 字段 | 类型 | 必填 | 含义 | +| --- | --- | --- | --- | +| `apiKey` | secret | 是 | 中转渠道 API Key | + +## 统一字段映射 + +| 统一字段 | 类型 | 必填 | 上游映射 | 说明 | +| --- | --- | --- | --- | --- | +| `model` | string | 是 | `model` | 上游模型 ID。 | +| `messages` | message[] | 是 | `messages` | 包含历史消息和当前用户输入。 | +| `instructions` | string | 否 | `system/instructions` | 系统指令。 | +| `temperature` | number | 否 | `temperature` | 采样温度。 | +| `top_p` | number | 否 | `top_p` | 核采样参数。 | +| `max_tokens` | integer | 否 | `max_tokens/max_output_tokens` | 最大输出 token。 | +| `tools` | array | 否 | `tools/toolConfig` | 工具定义。 | +| `tool_choice` | object\|string | 否 | `tool_choice` | 工具选择策略。 | +| `response_format` | object | 否 | `response_format/text` | 结构化输出配置。 | +| `stream` | boolean | 否 | `stream` | 流式开关。 | + +## 上游请求模板 + +| 上游位置 | 值或转换表达式 | +| --- | --- | +| `create.method` | `"POST"` | +| `create.path` | `"/chat/completions"` | +| `create.contentType` | `"application/json"` | +| `create.body.model` | `{"$ref":"request.model"}` | +| `create.body.messages` | `{"$ref":"request.messages"}` | +| `create.body.temperature` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.temperature"}}` | +| `create.body.top_p` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.top_p"}}` | +| `create.body.max_tokens` | `{"$omitEmpty":{"$coalesce":[{"$ref":"request.extra.max_tokens"},{"$ref":"request.providerOptions.antigravity-chat.max_tokens"}]}}` | +| `create.body.tools` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.tools"}}` | +| `create.body.tool_choice` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.tool_choice"}}` | +| `create.body.response_format` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.response_format"}}` | +| `create.body.stream` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.stream"}}` | +| `create.body.stop` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.stop"}}` | +| `create.body.seed` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.seed"}}` | +| `create.body.frequency_penalty` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.frequency_penalty"}}` | +| `create.body.presence_penalty` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.presence_penalty"}}` | +| `create.body.user` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.user"}}` | + +## 响应映射 + +| 映射位置 | 上游路径或转换表达式 | +| --- | --- | +| `response.status` | `"succeeded"` | +| `response.textPaths[0]` | `"choices.0.message.content"` | +| `response.textPaths[1]` | `"choices.0.text"` | +| `response.reasoningPaths[0]` | `"choices.0.message.reasoning_content"` | +| `response.usage` | `{"$ref":"response.usage"}` | +| `response.errorPaths[0]` | `"error.code"` | +| `response.messagePaths[0]` | `"error.message"` | + +## 兼容边界 + +该包只代表 Antigravity 中转渠道的 OpenAI Chat Completions 线协议;其他端点或网关包装必须使用独立插件。 + + +## Manifest 完整接口定义 + +以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。 + +```json +{ + "apiVersion": "yingce.plugin/v2", + "id": "antigravity-proxy", + "name": "Antigravity Proxy", + "version": "1.0.0", + "author": "Yingce", + "description": "Antigravity 中转渠道插件:OpenAI 兼容对话(gemini-3.8-flash-high / gemini-pro-agent)。", + "permissions": [ + "generation.run", + "media.read" + ], + "configuration": { + "fields": [ + { + "name": "apiKey", + "type": "secret", + "label": "API Key", + "required": true + } + ] + }, + "contributes": { + "providers": [ + { + "id": "antigravity-chat", + "label": "Antigravity Chat", + "capabilities": [ + "text" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "http://103.242.14.110:8317", + "auth": { + "type": "bearer", + "field": "apiKey" + }, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "模型 ID(gemini-3.8-flash-high / gemini-pro-agent)。" + }, + { + "name": "messages", + "type": "message[]", + "required": true, + "mapping": "provider message container", + "description": "包含历史消息和当前用户输入。" + }, + { + "name": "instructions", + "type": "string", + "required": false, + "mapping": "system/instructions", + "description": "系统指令。" + }, + { + "name": "temperature", + "type": "number", + "required": false, + "mapping": "temperature", + "description": "采样温度。" + }, + { + "name": "top_p", + "type": "number", + "required": false, + "mapping": "top_p", + "description": "核采样参数。" + }, + { + "name": "max_tokens", + "type": "integer", + "required": false, + "mapping": "max_tokens/max_output_tokens", + "description": "最大输出 token。" + }, + { + "name": "tools", + "type": "array", + "required": false, + "mapping": "tools/toolConfig", + "description": "工具定义。" + }, + { + "name": "tool_choice", + "type": "object|string", + "required": false, + "mapping": "tool_choice", + "description": "工具选择策略。" + }, + { + "name": "response_format", + "type": "object", + "required": false, + "mapping": "response_format/text", + "description": "结构化输出配置。" + }, + { + "name": "stream", + "type": "boolean", + "required": false, + "mapping": "stream", + "description": "流式开关;后台任务当前以最终响应归一。" + }, + { + "name": "providerOptions", + "type": "object", + "required": false, + "mapping": "provider-specific fields", + "description": "插件命名空间内的厂商扩展字段。" + } + ], + "create": { + "method": "POST", + "path": "/chat/completions", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "messages": { + "$ref": "request.messages" + }, + "temperature": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.temperature" + } + }, + "top_p": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.top_p" + } + }, + "max_tokens": { + "$omitEmpty": { + "$coalesce": [ + { + "$ref": "request.extra.max_tokens" + }, + { + "$ref": "request.providerOptions.antigravity-chat.max_tokens" + } + ] + } + }, + "tools": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.tools" + } + }, + "tool_choice": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.tool_choice" + } + }, + "response_format": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.response_format" + } + }, + "stream": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.stream" + } + }, + "stop": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.stop" + } + }, + "seed": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.seed" + } + }, + "frequency_penalty": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.frequency_penalty" + } + }, + "presence_penalty": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.presence_penalty" + } + }, + "user": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.user" + } + } + } + }, + "response": { + "status": "succeeded", + "textPaths": [ + "choices.0.message.content", + "choices.0.text" + ], + "reasoningPaths": [ + "choices.0.message.reasoning_content" + ], + "usage": { + "$ref": "response.usage" + }, + "errorPaths": [ + "error.code" + ], + "messagePaths": [ + "error.message" + ] + } + } + ] + }, + "documentation": "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>" +} +``` + diff --git a/plugin-packages/antigravity-proxy/manifest.json b/plugin-packages/antigravity-proxy/manifest.json new file mode 100644 index 000000000..e914d6856 --- /dev/null +++ b/plugin-packages/antigravity-proxy/manifest.json @@ -0,0 +1,224 @@ +{ + "apiVersion": "yingce.plugin/v2", + "id": "antigravity-proxy", + "name": "Antigravity Proxy", + "version": "1.0.0", + "author": "Yingce", + "description": "Antigravity 中转渠道插件:OpenAI 兼容对话(gemini-3.8-flash-high / gemini-pro-agent)。", + "permissions": [ + "generation.run", + "media.read" + ], + "configuration": { + "fields": [ + { + "name": "apiKey", + "type": "secret", + "label": "API Key", + "required": true + } + ] + }, + "contributes": { + "providers": [ + { + "id": "antigravity-chat", + "label": "Antigravity Chat", + "capabilities": [ + "text" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "http://103.242.14.110:8317", + "auth": { + "type": "bearer", + "field": "apiKey" + }, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "模型 ID(gemini-3.8-flash-high / gemini-pro-agent)。" + }, + { + "name": "messages", + "type": "message[]", + "required": true, + "mapping": "provider message container", + "description": "包含历史消息和当前用户输入。" + }, + { + "name": "instructions", + "type": "string", + "required": false, + "mapping": "system/instructions", + "description": "系统指令。" + }, + { + "name": "temperature", + "type": "number", + "required": false, + "mapping": "temperature", + "description": "采样温度。" + }, + { + "name": "top_p", + "type": "number", + "required": false, + "mapping": "top_p", + "description": "核采样参数。" + }, + { + "name": "max_tokens", + "type": "integer", + "required": false, + "mapping": "max_tokens/max_output_tokens", + "description": "最大输出 token。" + }, + { + "name": "tools", + "type": "array", + "required": false, + "mapping": "tools/toolConfig", + "description": "工具定义。" + }, + { + "name": "tool_choice", + "type": "object|string", + "required": false, + "mapping": "tool_choice", + "description": "工具选择策略。" + }, + { + "name": "response_format", + "type": "object", + "required": false, + "mapping": "response_format/text", + "description": "结构化输出配置。" + }, + { + "name": "stream", + "type": "boolean", + "required": false, + "mapping": "stream", + "description": "流式开关;后台任务当前以最终响应归一。" + }, + { + "name": "providerOptions", + "type": "object", + "required": false, + "mapping": "provider-specific fields", + "description": "插件命名空间内的厂商扩展字段。" + } + ], + "create": { + "method": "POST", + "path": "/chat/completions", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "messages": { + "$ref": "request.messages" + }, + "temperature": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.temperature" + } + }, + "top_p": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.top_p" + } + }, + "max_tokens": { + "$omitEmpty": { + "$coalesce": [ + { + "$ref": "request.extra.max_tokens" + }, + { + "$ref": "request.providerOptions.antigravity-chat.max_tokens" + } + ] + } + }, + "tools": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.tools" + } + }, + "tool_choice": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.tool_choice" + } + }, + "response_format": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.response_format" + } + }, + "stream": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.stream" + } + }, + "stop": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.stop" + } + }, + "seed": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.seed" + } + }, + "frequency_penalty": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.frequency_penalty" + } + }, + "presence_penalty": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.presence_penalty" + } + }, + "user": { + "$omitEmpty": { + "$ref": "request.providerOptions.antigravity-chat.user" + } + } + } + }, + "response": { + "status": "succeeded", + "textPaths": [ + "choices.0.message.content", + "choices.0.text" + ], + "reasoningPaths": [ + "choices.0.message.reasoning_content" + ], + "usage": { + "$ref": "response.usage" + }, + "errorPaths": [ + "error.code" + ], + "messagePaths": [ + "error.message" + ] + } + } + ] + }, + "documentation": "# Antigravity Proxy\n\nAntigravity 中转渠道插件:OpenAI Chat Completions 兼容对话协议。\n\n## 协议身份\n\n- 插件 ID:`antigravity-proxy`\n- Provider ID:`antigravity-chat`\n- 能力:`text`\n- 默认 Base URL:`http://103.242.14.110:8317`\n- 鉴权驱动:`bearer`\n- 创建:`POST /chat/completions`\n- 生命周期:同步响应\n\n## 配置字段\n\n| 字段 | 类型 | 必填 | 含义 |\n| --- | --- | --- | --- |\n| `apiKey` | secret | 是 | 中转渠道 API Key |\n\n## 模型\n\n- `gemini-3.8-flash-high`\n- `gemini-pro-agent`\n\n## 统一字段映射\n\n| 统一字段 | 类型 | 必填 | 上游映射 |\n| --- | --- | --- | --- |\n| `model` | string | 是 | `model` |\n| `messages` | message[] | 是 | `messages` |\n| `instructions` | string | 否 | `system/instructions` |\n| `temperature` | number | 否 | `temperature` |\n| `top_p` | number | 否 | `top_p` |\n| `max_tokens` | integer | 否 | `max_tokens` |\n| `tools` | array | 否 | `tools` |\n| `tool_choice` | object\\|string | 否 | `tool_choice` |\n| `response_format` | object | 否 | `response_format` |\n| `stream` | boolean | 否 | `stream` |\n\n完整接口见 [docs/interface.md](docs/interface.md)。\n\n---\n\n# Antigravity Proxy 接口字段\n\n## 协议身份\n\n- 插件 ID:`antigravity-proxy`\n- Provider ID:`antigravity-chat`\n- 能力:`text`\n- 默认 Base URL:`http://103.242.14.110:8317`\n- 鉴权驱动:`bearer`\n- 创建:`POST /chat/completions`\n- 生命周期:同步响应\n\n## 配置字段\n\n| 字段 | 类型 | 必填 | 含义 |\n| --- | --- | --- | --- |\n| `apiKey` | secret | 是 | 中转渠道 API Key |\n\n## 统一字段映射\n\n| 统一字段 | 类型 | 必填 | 上游映射 | 说明 |\n| --- | --- | --- | --- | --- |\n| `model` | string | 是 | `model` | 上游模型 ID。 |\n| `messages` | message[] | 是 | `messages` | 包含历史消息和当前用户输入。 |\n| `instructions` | string | 否 | `system/instructions` | 系统指令。 |\n| `temperature` | number | 否 | `temperature` | 采样温度。 |\n| `top_p` | number | 否 | `top_p` | 核采样参数。 |\n| `max_tokens` | integer | 否 | `max_tokens/max_output_tokens` | 最大输出 token。 |\n| `tools` | array | 否 | `tools/toolConfig` | 工具定义。 |\n| `tool_choice` | object\\|string | 否 | `tool_choice` | 工具选择策略。 |\n| `response_format` | object | 否 | `response_format/text` | 结构化输出配置。 |\n| `stream` | boolean | 否 | `stream` | 流式开关。 |\n\n## 上游请求模板\n\n| 上游位置 | 值或转换表达式 |\n| --- | --- |\n| `create.method` | `\"POST\"` |\n| `create.path` | `\"/chat/completions\"` |\n| `create.contentType` | `\"application/json\"` |\n| `create.body.model` | `{\"$ref\":\"request.model\"}` |\n| `create.body.messages` | `{\"$ref\":\"request.messages\"}` |\n| `create.body.temperature` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.antigravity-chat.temperature\"}}` |\n| `create.body.top_p` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.antigravity-chat.top_p\"}}` |\n| `create.body.max_tokens` | `{\"$omitEmpty\":{\"$coalesce\":[{\"$ref\":\"request.extra.max_tokens\"},{\"$ref\":\"request.providerOptions.antigravity-chat.max_tokens\"}]}}` |\n| `create.body.tools` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.antigravity-chat.tools\"}}` |\n| `create.body.tool_choice` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.antigravity-chat.tool_choice\"}}` |\n| `create.body.response_format` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.antigravity-chat.response_format\"}}` |\n| `create.body.stream` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.antigravity-chat.stream\"}}` |\n| `create.body.stop` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.antigravity-chat.stop\"}}` |\n| `create.body.seed` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.antigravity-chat.seed\"}}` |\n| `create.body.frequency_penalty` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.antigravity-chat.frequency_penalty\"}}` |\n| `create.body.presence_penalty` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.antigravity-chat.presence_penalty\"}}` |\n| `create.body.user` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.antigravity-chat.user\"}}` |\n\n## 响应映射\n\n| 映射位置 | 上游路径或转换表达式 |\n| --- | --- |\n| `response.status` | `\"succeeded\"` |\n| `response.textPaths[0]` | `\"choices.0.message.content\"` |\n| `response.textPaths[1]` | `\"choices.0.text\"` |\n| `response.reasoningPaths[0]` | `\"choices.0.message.reasoning_content\"` |\n| `response.usage` | `{\"$ref\":\"response.usage\"}` |\n| `response.errorPaths[0]` | `\"error.code\"` |\n| `response.messagePaths[0]` | `\"error.message\"` |\n\n## 兼容边界\n\n该包只代表 Antigravity 中转渠道的 OpenAI Chat Completions 线协议;其他端点或网关包装必须使用独立插件。\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"antigravity-proxy\",\n \"name\": \"Antigravity Proxy\",\n \"version\": \"1.0.0\",\n \"author\": \"Yingce\",\n \"description\": \"Antigravity 中转渠道插件:OpenAI 兼容对话(gemini-3.8-flash-high / gemini-pro-agent)。\",\n \"permissions\": [\n \"generation.run\",\n \"media.read\"\n ],\n \"configuration\": {\n \"fields\": [\n {\n \"name\": \"apiKey\",\n \"type\": \"secret\",\n \"label\": \"API Key\",\n \"required\": true\n }\n ]\n },\n \"contributes\": {\n \"providers\": [\n {\n \"id\": \"antigravity-chat\",\n \"label\": \"Antigravity Chat\",\n \"capabilities\": [\n \"text\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"http://103.242.14.110:8317\",\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"模型 ID(gemini-3.8-flash-high / gemini-pro-agent)。\"\n },\n {\n \"name\": \"messages\",\n \"type\": \"message[]\",\n \"required\": true,\n \"mapping\": \"provider message container\",\n \"description\": \"包含历史消息和当前用户输入。\"\n },\n {\n \"name\": \"instructions\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"system/instructions\",\n \"description\": \"系统指令。\"\n },\n {\n \"name\": \"temperature\",\n \"type\": \"number\",\n \"required\": false,\n \"mapping\": \"temperature\",\n \"description\": \"采样温度。\"\n },\n {\n \"name\": \"top_p\",\n \"type\": \"number\",\n \"required\": false,\n \"mapping\": \"top_p\",\n \"description\": \"核采样参数。\"\n },\n {\n \"name\": \"max_tokens\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"max_tokens/max_output_tokens\",\n \"description\": \"最大输出 token。\"\n },\n {\n \"name\": \"tools\",\n \"type\": \"array\",\n \"required\": false,\n \"mapping\": \"tools/toolConfig\",\n \"description\": \"工具定义。\"\n },\n {\n \"name\": \"tool_choice\",\n \"type\": \"object|string\",\n \"required\": false,\n \"mapping\": \"tool_choice\",\n \"description\": \"工具选择策略。\"\n },\n {\n \"name\": \"response_format\",\n \"type\": \"object\",\n \"required\": false,\n \"mapping\": \"response_format/text\",\n \"description\": \"结构化输出配置。\"\n },\n {\n \"name\": \"stream\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"stream\",\n \"description\": \"流式开关;后台任务当前以最终响应归一。\"\n },\n {\n \"name\": \"providerOptions\",\n \"type\": \"object\",\n \"required\": false,\n \"mapping\": \"provider-specific fields\",\n \"description\": \"插件命名空间内的厂商扩展字段。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/chat/completions\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"messages\": {\n \"$ref\": \"request.messages\"\n },\n \"temperature\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.antigravity-chat.temperature\"\n }\n },\n \"top_p\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.antigravity-chat.top_p\"\n }\n },\n \"max_tokens\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.extra.max_tokens\"\n },\n {\n \"$ref\": \"request.providerOptions.antigravity-chat.max_tokens\"\n }\n ]\n }\n },\n \"tools\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.antigravity-chat.tools\"\n }\n },\n \"tool_choice\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.antigravity-chat.tool_choice\"\n }\n },\n \"response_format\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.antigravity-chat.response_format\"\n }\n },\n \"stream\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.antigravity-chat.stream\"\n }\n },\n \"stop\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.antigravity-chat.stop\"\n }\n },\n \"seed\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.antigravity-chat.seed\"\n }\n },\n \"frequency_penalty\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.antigravity-chat.frequency_penalty\"\n }\n },\n \"presence_penalty\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.antigravity-chat.presence_penalty\"\n }\n },\n \"user\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.antigravity-chat.user\"\n }\n }\n }\n },\n \"response\": {\n \"status\": \"succeeded\",\n \"textPaths\": [\n \"choices.0.message.content\",\n \"choices.0.text\"\n ],\n \"reasoningPaths\": [\n \"choices.0.message.reasoning_content\"\n ],\n \"usage\": {\n \"$ref\": \"response.usage\"\n },\n \"errorPaths\": [\n \"error.code\"\n ],\n \"messagePaths\": [\n \"error.message\"\n ]\n }\n }\n ]\n },\n \"documentation\": \"<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>\"\n}\n```\n\n" +} diff --git a/plugin-packages/lxmone-video-suite.yingce-plugin b/plugin-packages/lxmone-video-suite.yingce-plugin new file mode 100644 index 0000000000000000000000000000000000000000..3bcb96aab03b4caac4d6667b0362775f2fc8077d GIT binary patch literal 17228 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。 | + +## 统一字段映射 + +宿主向插件提交统一的视频生成请求,插件按 provider 映射到上游字段: + +| 统一字段 | 类型 | 必填 | 上游映射 | 说明 | +| --- | --- | --- | --- | --- | +| `model` | string | 是 | `model` | 上游模型 ID,透传,不做改名或别名。 | +| `prompt` | string | 是 | `prompt` / `input.prompt` | 视频提示词。 | +| `images` | media[] | 否 | `reference_images` / `images` / `image_url` / `input.media[]` / `input_reference` | 参考图;带 role 的 `first_frame`、`last_frame` 会落到支持首尾帧的 provider 专属字段。 | +| `videos` | media[] | 否 | `reference_videos` / `videos` / `input.media[]` | 参考视频。 | +| `audios` | media[] | 否 | `reference_audios` / `audio` / `audio_urls` / `input.media[]` | 参考音频。 | +| `duration` | integer | 否 | `seconds` / `duration` / `duration_seconds` | 时长秒数;缺省由各 provider 填默认值。 | +| `aspectRatio` | string | 否 | `aspect_ratio` / `parameters.ratio` | 画幅比例,默认 `16:9`。 | +| `resolution` | string | 否 | `resolution` / `size` / `parameters.resolution` | 分辨率档位;上游要求大写 `720P` 形式的 provider 会自动转换。 | +| `generateAudio` | boolean | 否 | `audio` / `generate_audio` | 是否生成或保留音频。 | + +## 各 provider 的创建体 + +### `lxmone-wan-videos`(`wan3.0-video`、`wan3.0-video-prime`) + +`POST /v1/videos`,顶层字段:`model`、`prompt`、`seconds`、`size`、`aspect_ratio`、 +`reference_images[{url, role}]`、`reference_videos[]`、`reference_audios[]`、`prompt_extend`。 + +### `lxmone-wan-channel-s`(`wan3.0-video-s`、`wan3.0-video-prime-s`) + +`POST /v1/videos`,`input.media[{type, url}]` 与 `input.prompt`,参数放在 +`parameters{resolution, ratio, duration, audio, prompt_extend}`。 + +### `lxmone-seedance-videos`(`seedance-2-pro/fast/mini`、`seedance-2.5-pro`、`seedance2.0-a`~`f`、`seedance2.5-a`) + +`POST /v1/videos`,字段:`model`、`prompt`、`duration_seconds`、`resolution`、`aspect_ratio`、 +`reference_images[]`、`reference_videos[]`、`reference_audios[]`。 + +### `lxmone-h3-max-videos`(`minimax-h3-max`) + +官方文档要求 H3 Max 走 `POST /v1/videos`(不要提交到 `/v1/videos/generations`,也要避开 +`input_reference` 与 `audio` 这组独立工作流字段),字段为:`model`、`prompt`、`seconds`、 +`resolution`、`aspect_ratio`、`reference_images[]`、`reference_videos[]`、`reference_audios[]`。 +上游把客户端传入的 720p 映射为 768p,参考素材上限为图 / 视频 / 音频各 12 段。 + +### `lxmone-sd-videos`(`sd-2.0-a`、`sd-2.0-b`、`sd-2.0-c`、`sd-2.5-a`、`sd-2.5`) + +`POST /v1/videos`,字段:`model`、`prompt`、`duration`、`resolution`、`aspect_ratio`、 +`images[]`、`videos[]`、`audio[]`。 + +### `lxmone-sd-mini-videos`(`sd-mini`) + +`POST /v1/videos`,字段:`model`、`prompt`、`seconds`、`resolution`、`aspect_ratio`、 +`image_url`、`reference_image_urls[]`、`audio_urls[]`。 + +### `lxmone-grok-videos`(`grok-imagine-video`、`grok-imagine-video-1.5`、`grok-imagine-video-1.5S`) + +`POST /v1/videos/generations`,字段:`model`、`prompt`、`duration`、`aspect_ratio`、`resolution`、 +`generate_audio`,以及互斥的 `image{url}`(首帧)或 `reference_images[{url}]`(参考图)。 + +### `lxmone-h3-workflow`(`minimax-h3-a`~`minimax-h3-e`) + +`POST /v1/videos/generations`,字段:`model`、`prompt`、`seconds`、`resolution`、`aspect_ratio`、 +`input_reference[]`、`first_frame`、`last_frame`、`audio`。 + +## 响应与错误 + +创建成功后,插件从上游响应里按顺序取 `id`、`task_id`、`taskId`、`data.id` 得到统一 `taskId`, +轮询时按 `status`、`state`、`data.status` 取状态,`completed`/`succeeded` 归一为 `succeeded`, +`failed`/`expired`/`error` 归一为 `failed`,其余按进行中处理。 + +视频地址按 `metadata.url`、`metadata.video_url`、`video_url`、`videoUrl`、`result_url`、`url`、 +`data.metadata.url`、`data.video_url`、`output.url` 依次取值;上游返回的是临时地址 +(`resultEphemeral`),由宿主立即下载并转存为项目资源。 + +错误信息取 `error.message`、`message`、`fail_reason`,错误码路径为 `error.message` 与 `error.code`。 +HTTP 失败、业务错误码或状态进入 `failed` 时,任务以失败结束并把上游原文回传给用户。 + + +## Manifest 完整接口定义 + +以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。 + +```json +{ + "apiVersion": "yingce.plugin/v2", + "id": "lxmone-video-suite", + "name": "万有引力视频套件", + "version": "1.0.0", + "author": "Yingce / 万有引力", + "description": "万有引力(lxmone.xyz)视频协议套件:Wan 3.0(现有渠道与 S 渠道)、Seedance 2 / 2.5、SD 2.0 / 2.5 / Mini、Grok Imagine、MiniMax H3(Max 与 A-E 独立工作流)。", + "permissions": [ + "generation.run", + "media.read" + ], + "configuration": { + "fields": [ + { + "name": "apiKey", + "type": "secret", + "label": "API Key", + "required": true + } + ] + }, + "contributes": { + "providers": [ + { + "id": "lxmone-wan-videos", + "label": "万有引力 Wan 3.0 视频(现有渠道)", + "description": "wan3.0-video / wan3.0-video-prime:POST /v1/videos,顶层 reference_images、reference_videos、reference_audios。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "seconds": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 10 + } + }, + "size": { + "$coalesce": [ + { + "$upper": { + "$ref": "request.resolution" + } + }, + "720P" + ] + }, + "aspect_ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "reference_images": { + "$omitEmpty": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "url": { + "$ref": "media.value" + }, + "role": { + "$coalesce": [ + { + "$ref": "media.role" + }, + "reference_image" + ] + } + } + } + } + }, + "reference_videos": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.videos" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "reference_audios": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "prompt_extend": false + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + }, + { + "id": "lxmone-wan-channel-s", + "label": "万有引力 Wan 3.0 视频(S 渠道)", + "description": "wan3.0-video-s / wan3.0-video-prime-s:POST /v1/videos,原生 input.media 与 parameters。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "input": { + "prompt": { + "$ref": "request.prompt" + }, + "media": { + "$omitEmpty": { + "$concatArrays": [ + { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "type": { + "$coalesce": [ + { + "$ref": "media.role" + }, + "reference_image" + ] + }, + "url": { + "$ref": "media.value" + } + } + } + }, + { + "$map": { + "from": { + "$ref": "request.videos" + }, + "as": "media", + "in": { + "type": "reference_video", + "url": { + "$ref": "media.value" + } + } + } + }, + { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "type": "reference_audio", + "url": { + "$ref": "media.value" + } + } + } + } + ] + } + } + }, + "parameters": { + "resolution": { + "$coalesce": [ + { + "$upper": { + "$ref": "request.resolution" + } + }, + "720P" + ] + }, + "ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "duration": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 5 + } + }, + "audio": { + "$ref": "request.generateAudio" + }, + "prompt_extend": false + } + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + }, + { + "id": "lxmone-seedance-videos", + "label": "万有引力 Seedance 视频", + "description": "seedance-2-pro/fast/mini、seedance-2.5-pro、seedance2.0-a~f、seedance2.5-a。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "duration_seconds": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 5 + } + }, + "resolution": { + "$lower": { + "$coalesce": [ + { + "$ref": "request.resolution" + }, + "720p" + ] + } + }, + "aspect_ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "reference_images": { + "$omitEmpty": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "reference_videos": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.videos" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "reference_audios": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + }, + { + "id": "lxmone-h3-max-videos", + "label": "万有引力 MiniMax H3 Max 视频", + "description": "minimax-h3-max:POST /v1/videos,seconds、resolution 与 reference_images、reference_videos、reference_audios 扁平 URL 数组。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "seconds": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 5 + } + }, + "resolution": { + "$lower": { + "$coalesce": [ + { + "$ref": "request.resolution" + }, + "768p" + ] + } + }, + "aspect_ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "reference_images": { + "$omitEmpty": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "reference_videos": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.videos" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "reference_audios": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + }, + { + "id": "lxmone-sd-videos", + "label": "万有引力 SD 视频", + "description": "sd-2.0-a/b/c、sd-2.5-a、sd-2.5:POST /v1/videos,duration 与 images、videos、audio 数组。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "duration": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 5 + } + }, + "resolution": { + "$lower": { + "$coalesce": [ + { + "$ref": "request.resolution" + }, + "720p" + ] + } + }, + "aspect_ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "images": { + "$omitEmpty": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "videos": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.videos" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "audio": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + }, + { + "id": "lxmone-sd-mini-videos", + "label": "万有引力 SD Mini 视频", + "description": "sd-mini:POST /v1/videos,主图 image_url、额外参考图 reference_image_urls、音频 audio_urls。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "seconds": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 5 + } + }, + "resolution": { + "$lower": { + "$coalesce": [ + { + "$ref": "request.resolution" + }, + "720p" + ] + } + }, + "aspect_ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "image_url": { + "$omitEmpty": { + "$first": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + }, + "reference_image_urls": { + "$omitEmpty": { + "$map": { + "from": { + "$filter": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "where": { + "$gt": [ + { + "$ref": "media.order" + }, + 0 + ] + } + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "audio_urls": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + }, + { + "id": "lxmone-grok-videos", + "label": "万有引力 Grok Imagine 视频", + "description": "grok-imagine-video / 1.5 / 1.5S:POST /v1/videos/generations,duration、image、reference_images。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos/generations", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "duration": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 6 + } + }, + "aspect_ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "resolution": { + "$lower": { + "$coalesce": [ + { + "$ref": "request.resolution" + }, + "720p" + ] + } + }, + "image": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + "then": { + "url": { + "$first": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + }, + "else": null + } + } + }, + "reference_images": { + "$omitEmpty": { + "$if": { + "condition": { + "$gt": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + "then": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "url": { + "$ref": "media.value" + } + } + } + }, + "else": null + } + } + }, + "generate_audio": { + "$ref": "request.generateAudio" + } + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/generations/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + }, + { + "id": "lxmone-h3-workflow", + "label": "万有引力 MiniMax H3 工作流", + "description": "minimax-h3-a~e:POST /v1/videos/generations,input_reference、first_frame、last_frame、audio。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos/generations", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "seconds": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 5 + } + }, + "resolution": { + "$lower": { + "$coalesce": [ + { + "$ref": "request.resolution" + }, + "720p" + ] + } + }, + "aspect_ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "input_reference": { + "$omitEmpty": { + "$map": { + "from": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$and": [ + { + "$ne": [ + { + "$ref": "media.role" + }, + "first_frame" + ] + }, + { + "$ne": [ + { + "$ref": "media.role" + }, + "last_frame" + ] + } + ] + } + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "first_frame": { + "$omitEmpty": { + "$first": { + "$map": { + "from": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$eq": [ + { + "$ref": "media.role" + }, + "first_frame" + ] + } + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + }, + "last_frame": { + "$omitEmpty": { + "$first": { + "$map": { + "from": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$eq": [ + { + "$ref": "media.role" + }, + "last_frame" + ] + } + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + }, + "audio": { + "$omitEmpty": { + "$first": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.audios" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + } + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/generations/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + } + ] + }, + "documentation": "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>" +} +``` + diff --git a/plugin-packages/lxmone-video-suite/manifest.json b/plugin-packages/lxmone-video-suite/manifest.json new file mode 100644 index 000000000..595bf4887 --- /dev/null +++ b/plugin-packages/lxmone-video-suite/manifest.json @@ -0,0 +1,2289 @@ +{ + "apiVersion": "yingce.plugin/v2", + "id": "lxmone-video-suite", + "name": "万有引力视频套件", + "version": "1.0.0", + "author": "Yingce / 万有引力", + "description": "万有引力(lxmone.xyz)视频协议套件:Wan 3.0(现有渠道与 S 渠道)、Seedance 2 / 2.5、SD 2.0 / 2.5 / Mini、Grok Imagine、MiniMax H3(Max 与 A-E 独立工作流)。", + "permissions": [ + "generation.run", + "media.read" + ], + "configuration": { + "fields": [ + { + "name": "apiKey", + "type": "secret", + "label": "API Key", + "required": true + } + ] + }, + "contributes": { + "providers": [ + { + "id": "lxmone-wan-videos", + "label": "万有引力 Wan 3.0 视频(现有渠道)", + "description": "wan3.0-video / wan3.0-video-prime:POST /v1/videos,顶层 reference_images、reference_videos、reference_audios。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "seconds": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 10 + } + }, + "size": { + "$coalesce": [ + { + "$upper": { + "$ref": "request.resolution" + } + }, + "720P" + ] + }, + "aspect_ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "reference_images": { + "$omitEmpty": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "url": { + "$ref": "media.value" + }, + "role": { + "$coalesce": [ + { + "$ref": "media.role" + }, + "reference_image" + ] + } + } + } + } + }, + "reference_videos": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.videos" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "reference_audios": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "prompt_extend": false + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + }, + { + "id": "lxmone-wan-channel-s", + "label": "万有引力 Wan 3.0 视频(S 渠道)", + "description": "wan3.0-video-s / wan3.0-video-prime-s:POST /v1/videos,原生 input.media 与 parameters。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "input": { + "prompt": { + "$ref": "request.prompt" + }, + "media": { + "$omitEmpty": { + "$concatArrays": [ + { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "type": { + "$coalesce": [ + { + "$ref": "media.role" + }, + "reference_image" + ] + }, + "url": { + "$ref": "media.value" + } + } + } + }, + { + "$map": { + "from": { + "$ref": "request.videos" + }, + "as": "media", + "in": { + "type": "reference_video", + "url": { + "$ref": "media.value" + } + } + } + }, + { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "type": "reference_audio", + "url": { + "$ref": "media.value" + } + } + } + } + ] + } + } + }, + "parameters": { + "resolution": { + "$coalesce": [ + { + "$upper": { + "$ref": "request.resolution" + } + }, + "720P" + ] + }, + "ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "duration": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 5 + } + }, + "audio": { + "$ref": "request.generateAudio" + }, + "prompt_extend": false + } + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + }, + { + "id": "lxmone-seedance-videos", + "label": "万有引力 Seedance 视频", + "description": "seedance-2-pro/fast/mini、seedance-2.5-pro、seedance2.0-a~f、seedance2.5-a。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "duration_seconds": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 5 + } + }, + "resolution": { + "$lower": { + "$coalesce": [ + { + "$ref": "request.resolution" + }, + "720p" + ] + } + }, + "aspect_ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "reference_images": { + "$omitEmpty": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "reference_videos": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.videos" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "reference_audios": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + }, + { + "id": "lxmone-h3-max-videos", + "label": "万有引力 MiniMax H3 Max 视频", + "description": "minimax-h3-max:POST /v1/videos,seconds、resolution 与 reference_images、reference_videos、reference_audios 扁平 URL 数组。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "seconds": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 5 + } + }, + "resolution": { + "$lower": { + "$coalesce": [ + { + "$ref": "request.resolution" + }, + "768p" + ] + } + }, + "aspect_ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "reference_images": { + "$omitEmpty": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "reference_videos": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.videos" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "reference_audios": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + }, + { + "id": "lxmone-sd-videos", + "label": "万有引力 SD 视频", + "description": "sd-2.0-a/b/c、sd-2.5-a、sd-2.5:POST /v1/videos,duration 与 images、videos、audio 数组。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "duration": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 5 + } + }, + "resolution": { + "$lower": { + "$coalesce": [ + { + "$ref": "request.resolution" + }, + "720p" + ] + } + }, + "aspect_ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "images": { + "$omitEmpty": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "videos": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.videos" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "audio": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + }, + { + "id": "lxmone-sd-mini-videos", + "label": "万有引力 SD Mini 视频", + "description": "sd-mini:POST /v1/videos,主图 image_url、额外参考图 reference_image_urls、音频 audio_urls。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "seconds": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 5 + } + }, + "resolution": { + "$lower": { + "$coalesce": [ + { + "$ref": "request.resolution" + }, + "720p" + ] + } + }, + "aspect_ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "image_url": { + "$omitEmpty": { + "$first": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + }, + "reference_image_urls": { + "$omitEmpty": { + "$map": { + "from": { + "$filter": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "where": { + "$gt": [ + { + "$ref": "media.order" + }, + 0 + ] + } + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "audio_urls": { + "$omitEmpty": { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + }, + { + "id": "lxmone-grok-videos", + "label": "万有引力 Grok Imagine 视频", + "description": "grok-imagine-video / 1.5 / 1.5S:POST /v1/videos/generations,duration、image、reference_images。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos/generations", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "duration": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 6 + } + }, + "aspect_ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "resolution": { + "$lower": { + "$coalesce": [ + { + "$ref": "request.resolution" + }, + "720p" + ] + } + }, + "image": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + "then": { + "url": { + "$first": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + }, + "else": null + } + } + }, + "reference_images": { + "$omitEmpty": { + "$if": { + "condition": { + "$gt": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + "then": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "url": { + "$ref": "media.value" + } + } + } + }, + "else": null + } + } + }, + "generate_audio": { + "$ref": "request.generateAudio" + } + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/generations/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + }, + { + "id": "lxmone-h3-workflow", + "label": "万有引力 MiniMax H3 工作流", + "description": "minimax-h3-a~e:POST /v1/videos/generations,input_reference、first_frame、last_frame、audio。", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://lxmone.xyz/v1", + "requiresPublicMediaUrls": true, + "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": "reference_images", + "description": "参考图片,宿主发布为公开 URL 后提交。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference_videos", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference_audios", + "description": "参考音频。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "seconds/duration/duration_seconds", + "description": "输出时长,单位秒。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "aspect_ratio", + "description": "画幅比例。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/size", + "description": "输出分辨率档位。" + }, + { + "name": "generateAudio", + "type": "boolean", + "required": false, + "mapping": "audio/generate_audio", + "description": "是否生成或保留音频。" + } + ], + "create": { + "method": "POST", + "path": "/v1/videos/generations", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "seconds": { + "$if": { + "condition": { + "$gt": [ + { + "$ref": "request.duration" + }, + 0 + ] + }, + "then": { + "$ref": "request.duration" + }, + "else": 5 + } + }, + "resolution": { + "$lower": { + "$coalesce": [ + { + "$ref": "request.resolution" + }, + "720p" + ] + } + }, + "aspect_ratio": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "input_reference": { + "$omitEmpty": { + "$map": { + "from": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$and": [ + { + "$ne": [ + { + "$ref": "media.role" + }, + "first_frame" + ] + }, + { + "$ne": [ + { + "$ref": "media.role" + }, + "last_frame" + ] + } + ] + } + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "first_frame": { + "$omitEmpty": { + "$first": { + "$map": { + "from": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$eq": [ + { + "$ref": "media.role" + }, + "first_frame" + ] + } + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + }, + "last_frame": { + "$omitEmpty": { + "$first": { + "$map": { + "from": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$eq": [ + { + "$ref": "media.role" + }, + "last_frame" + ] + } + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + }, + "audio": { + "$omitEmpty": { + "$first": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.audios" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + } + } + }, + "poll": { + "method": "GET", + "path": "/v1/videos/generations/{{taskId}}", + "contentType": "application/json" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.request_id" + }, + { + "$ref": "response.data.id" + }, + "taskId" + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.status" + }, + { + "$ref": "response.state" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + }, + { + "$ref": "response.fail_reason" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.metadata.url" + }, + { + "$ref": "response.metadata.video_url" + }, + { + "$ref": "response.video_url" + }, + { + "$ref": "response.videoUrl" + }, + { + "$ref": "response.result_url" + }, + { + "$ref": "response.url" + }, + { + "$ref": "response.data.metadata.url" + }, + { + "$ref": "response.data.video_url" + }, + { + "$ref": "response.output.url" + } + ] + }, + "errorPaths": [ + "error.message", + "error.code" + ], + "resultEphemeral": true + } + } + ] + }, + "documentation": "# 万有引力视频套件(lxmone.xyz)\n\n该目录是万有引力中转站(`https://lxmone.xyz/v1`)视频协议的官方声明式插件源码。\n后端从生成的 `lxmone-video-suite.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。\n\n协议按中转站公开接口文档 `https://lxmone.xyz/docs/` 的 `/v1/videos` 与 `/v1/videos/generations`\n两套请求体形状编写,拆成 8 个 provider,覆盖该站点当前开放的全部视频模型:\n\n| Provider ID | 覆盖模型 | 创建接口 |\n| --- | --- | --- |\n| `lxmone-wan-videos` | `wan3.0-video`、`wan3.0-video-prime` | `POST /v1/videos` |\n| `lxmone-wan-channel-s` | `wan3.0-video-s`、`wan3.0-video-prime-s` | `POST /v1/videos` |\n| `lxmone-seedance-videos` | `seedance-2-pro/fast/mini`、`seedance-2.5-pro`、`seedance2.0-a`~`seedance2.0-f`、`seedance2.5-a` | `POST /v1/videos` |\n| `lxmone-h3-max-videos` | `minimax-h3-max` | `POST /v1/videos` |\n| `lxmone-sd-videos` | `sd-2.0-a`、`sd-2.0-b`、`sd-2.0-c`、`sd-2.5-a`、`sd-2.5` | `POST /v1/videos` |\n| `lxmone-sd-mini-videos` | `sd-mini` | `POST /v1/videos` |\n| `lxmone-grok-videos` | `grok-imagine-video`、`grok-imagine-video-1.5`、`grok-imagine-video-1.5S` | `POST /v1/videos/generations` |\n| `lxmone-h3-workflow` | `minimax-h3-a`~`minimax-h3-e` | `POST /v1/videos/generations` |\n\n完整字段、请求模板与响应映射见 [docs/interface.md](docs/interface.md)。\n\n## 构建\n\n```sh\nnode plugin-packages/embed-documentation.mjs lxmone-video-suite\nsh plugin-packages/build-packages.sh\n```\n\n---\n\n# 万有引力视频套件接口字段\n\n## 协议身份\n\n- 插件 ID:`lxmone-video-suite`。\n- 能力:`video`。\n- 默认 Base URL:`https://lxmone.xyz/v1`。\n- 鉴权驱动:`bearer`。\n- 创建接口:`POST /v1/videos` 或 `POST /v1/videos/generations`(按模型所属 provider 决定)。\n- 查询接口:`GET /v1/videos/{{taskId}}` 或 `GET /v1/videos/generations/{{taskId}}`。\n- 参考媒体要求公开可访问的 HTTPS URL(`requiresPublicMediaUrls`)。\n\n以上形状来自中转站公开文档 `https://lxmone.xyz/docs/`:同一批模型的 `model`、`prompt`、时长、\n分辨率与画幅字段名在不同渠道并不一致,因此按上游请求体拆成 8 个 provider,而不是一个通用协议。\n\n## 配置字段\n\n| 字段 | 类型 | 必填 | 含义 |\n| --- | --- | --- | --- |\n| `apiKey` | secret | 是 | 万有引力中转站的 API Key,随请求发送 `Authorization: Bearer `。 |\n\n## 统一字段映射\n\n宿主向插件提交统一的视频生成请求,插件按 provider 映射到上游字段:\n\n| 统一字段 | 类型 | 必填 | 上游映射 | 说明 |\n| --- | --- | --- | --- | --- |\n| `model` | string | 是 | `model` | 上游模型 ID,透传,不做改名或别名。 |\n| `prompt` | string | 是 | `prompt` / `input.prompt` | 视频提示词。 |\n| `images` | media[] | 否 | `reference_images` / `images` / `image_url` / `input.media[]` / `input_reference` | 参考图;带 role 的 `first_frame`、`last_frame` 会落到支持首尾帧的 provider 专属字段。 |\n| `videos` | media[] | 否 | `reference_videos` / `videos` / `input.media[]` | 参考视频。 |\n| `audios` | media[] | 否 | `reference_audios` / `audio` / `audio_urls` / `input.media[]` | 参考音频。 |\n| `duration` | integer | 否 | `seconds` / `duration` / `duration_seconds` | 时长秒数;缺省由各 provider 填默认值。 |\n| `aspectRatio` | string | 否 | `aspect_ratio` / `parameters.ratio` | 画幅比例,默认 `16:9`。 |\n| `resolution` | string | 否 | `resolution` / `size` / `parameters.resolution` | 分辨率档位;上游要求大写 `720P` 形式的 provider 会自动转换。 |\n| `generateAudio` | boolean | 否 | `audio` / `generate_audio` | 是否生成或保留音频。 |\n\n## 各 provider 的创建体\n\n### `lxmone-wan-videos`(`wan3.0-video`、`wan3.0-video-prime`)\n\n`POST /v1/videos`,顶层字段:`model`、`prompt`、`seconds`、`size`、`aspect_ratio`、\n`reference_images[{url, role}]`、`reference_videos[]`、`reference_audios[]`、`prompt_extend`。\n\n### `lxmone-wan-channel-s`(`wan3.0-video-s`、`wan3.0-video-prime-s`)\n\n`POST /v1/videos`,`input.media[{type, url}]` 与 `input.prompt`,参数放在\n`parameters{resolution, ratio, duration, audio, prompt_extend}`。\n\n### `lxmone-seedance-videos`(`seedance-2-pro/fast/mini`、`seedance-2.5-pro`、`seedance2.0-a`~`f`、`seedance2.5-a`)\n\n`POST /v1/videos`,字段:`model`、`prompt`、`duration_seconds`、`resolution`、`aspect_ratio`、\n`reference_images[]`、`reference_videos[]`、`reference_audios[]`。\n\n### `lxmone-h3-max-videos`(`minimax-h3-max`)\n\n官方文档要求 H3 Max 走 `POST /v1/videos`(不要提交到 `/v1/videos/generations`,也要避开\n`input_reference` 与 `audio` 这组独立工作流字段),字段为:`model`、`prompt`、`seconds`、\n`resolution`、`aspect_ratio`、`reference_images[]`、`reference_videos[]`、`reference_audios[]`。\n上游把客户端传入的 720p 映射为 768p,参考素材上限为图 / 视频 / 音频各 12 段。\n\n### `lxmone-sd-videos`(`sd-2.0-a`、`sd-2.0-b`、`sd-2.0-c`、`sd-2.5-a`、`sd-2.5`)\n\n`POST /v1/videos`,字段:`model`、`prompt`、`duration`、`resolution`、`aspect_ratio`、\n`images[]`、`videos[]`、`audio[]`。\n\n### `lxmone-sd-mini-videos`(`sd-mini`)\n\n`POST /v1/videos`,字段:`model`、`prompt`、`seconds`、`resolution`、`aspect_ratio`、\n`image_url`、`reference_image_urls[]`、`audio_urls[]`。\n\n### `lxmone-grok-videos`(`grok-imagine-video`、`grok-imagine-video-1.5`、`grok-imagine-video-1.5S`)\n\n`POST /v1/videos/generations`,字段:`model`、`prompt`、`duration`、`aspect_ratio`、`resolution`、\n`generate_audio`,以及互斥的 `image{url}`(首帧)或 `reference_images[{url}]`(参考图)。\n\n### `lxmone-h3-workflow`(`minimax-h3-a`~`minimax-h3-e`)\n\n`POST /v1/videos/generations`,字段:`model`、`prompt`、`seconds`、`resolution`、`aspect_ratio`、\n`input_reference[]`、`first_frame`、`last_frame`、`audio`。\n\n## 响应与错误\n\n创建成功后,插件从上游响应里按顺序取 `id`、`task_id`、`taskId`、`data.id` 得到统一 `taskId`,\n轮询时按 `status`、`state`、`data.status` 取状态,`completed`/`succeeded` 归一为 `succeeded`,\n`failed`/`expired`/`error` 归一为 `failed`,其余按进行中处理。\n\n视频地址按 `metadata.url`、`metadata.video_url`、`video_url`、`videoUrl`、`result_url`、`url`、\n`data.metadata.url`、`data.video_url`、`output.url` 依次取值;上游返回的是临时地址\n(`resultEphemeral`),由宿主立即下载并转存为项目资源。\n\n错误信息取 `error.message`、`message`、`fail_reason`,错误码路径为 `error.message` 与 `error.code`。\nHTTP 失败、业务错误码或状态进入 `failed` 时,任务以失败结束并把上游原文回传给用户。\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"lxmone-video-suite\",\n \"name\": \"万有引力视频套件\",\n \"version\": \"1.0.0\",\n \"author\": \"Yingce / 万有引力\",\n \"description\": \"万有引力(lxmone.xyz)视频协议套件:Wan 3.0(现有渠道与 S 渠道)、Seedance 2 / 2.5、SD 2.0 / 2.5 / Mini、Grok Imagine、MiniMax H3(Max 与 A-E 独立工作流)。\",\n \"permissions\": [\n \"generation.run\",\n \"media.read\"\n ],\n \"configuration\": {\n \"fields\": [\n {\n \"name\": \"apiKey\",\n \"type\": \"secret\",\n \"label\": \"API Key\",\n \"required\": true\n }\n ]\n },\n \"contributes\": {\n \"providers\": [\n {\n \"id\": \"lxmone-wan-videos\",\n \"label\": \"万有引力 Wan 3.0 视频(现有渠道)\",\n \"description\": \"wan3.0-video / wan3.0-video-prime:POST /v1/videos,顶层 reference_images、reference_videos、reference_audios。\",\n \"capabilities\": [\n \"video\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://lxmone.xyz/v1\",\n \"requiresPublicMediaUrls\": true,\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"上游模型 ID。\"\n },\n {\n \"name\": \"prompt\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"prompt\",\n \"description\": \"视频提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_images\",\n \"description\": \"参考图片,宿主发布为公开 URL 后提交。\"\n },\n {\n \"name\": \"videos\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_videos\",\n \"description\": \"参考视频。\"\n },\n {\n \"name\": \"audios\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_audios\",\n \"description\": \"参考音频。\"\n },\n {\n \"name\": \"duration\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"seconds/duration/duration_seconds\",\n \"description\": \"输出时长,单位秒。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"aspect_ratio\",\n \"description\": \"画幅比例。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution/size\",\n \"description\": \"输出分辨率档位。\"\n },\n {\n \"name\": \"generateAudio\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"audio/generate_audio\",\n \"description\": \"是否生成或保留音频。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/v1/videos\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"seconds\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$ref\": \"request.duration\"\n },\n 0\n ]\n },\n \"then\": {\n \"$ref\": \"request.duration\"\n },\n \"else\": 10\n }\n },\n \"size\": {\n \"$coalesce\": [\n {\n \"$upper\": {\n \"$ref\": \"request.resolution\"\n }\n },\n \"720P\"\n ]\n },\n \"aspect_ratio\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"16:9\"\n ]\n },\n \"reference_images\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"url\": {\n \"$ref\": \"media.value\"\n },\n \"role\": {\n \"$coalesce\": [\n {\n \"$ref\": \"media.role\"\n },\n \"reference_image\"\n ]\n }\n }\n }\n }\n },\n \"reference_videos\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.videos\"\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"reference_audios\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.audios\"\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"prompt_extend\": false\n }\n },\n \"poll\": {\n \"method\": \"GET\",\n \"path\": \"/v1/videos/{{taskId}}\",\n \"contentType\": \"application/json\"\n },\n \"response\": {\n \"taskId\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.id\"\n },\n {\n \"$ref\": \"response.task_id\"\n },\n {\n \"$ref\": \"response.taskId\"\n },\n {\n \"$ref\": \"response.request_id\"\n },\n {\n \"$ref\": \"response.data.id\"\n },\n \"taskId\"\n ]\n },\n \"status\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.status\"\n },\n {\n \"$ref\": \"response.state\"\n },\n {\n \"$ref\": \"response.data.status\"\n },\n \"pending\"\n ]\n },\n \"message\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.error.message\"\n },\n {\n \"$ref\": \"response.message\"\n },\n {\n \"$ref\": \"response.fail_reason\"\n }\n ]\n },\n \"videos\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.metadata.url\"\n },\n {\n \"$ref\": \"response.metadata.video_url\"\n },\n {\n \"$ref\": \"response.video_url\"\n },\n {\n \"$ref\": \"response.videoUrl\"\n },\n {\n \"$ref\": \"response.result_url\"\n },\n {\n \"$ref\": \"response.url\"\n },\n {\n \"$ref\": \"response.data.metadata.url\"\n },\n {\n \"$ref\": \"response.data.video_url\"\n },\n {\n \"$ref\": \"response.output.url\"\n }\n ]\n },\n \"errorPaths\": [\n \"error.message\",\n \"error.code\"\n ],\n \"resultEphemeral\": true\n }\n },\n {\n \"id\": \"lxmone-wan-channel-s\",\n \"label\": \"万有引力 Wan 3.0 视频(S 渠道)\",\n \"description\": \"wan3.0-video-s / wan3.0-video-prime-s:POST /v1/videos,原生 input.media 与 parameters。\",\n \"capabilities\": [\n \"video\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://lxmone.xyz/v1\",\n \"requiresPublicMediaUrls\": true,\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"上游模型 ID。\"\n },\n {\n \"name\": \"prompt\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"prompt\",\n \"description\": \"视频提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_images\",\n \"description\": \"参考图片,宿主发布为公开 URL 后提交。\"\n },\n {\n \"name\": \"videos\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_videos\",\n \"description\": \"参考视频。\"\n },\n {\n \"name\": \"audios\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_audios\",\n \"description\": \"参考音频。\"\n },\n {\n \"name\": \"duration\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"seconds/duration/duration_seconds\",\n \"description\": \"输出时长,单位秒。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"aspect_ratio\",\n \"description\": \"画幅比例。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution/size\",\n \"description\": \"输出分辨率档位。\"\n },\n {\n \"name\": \"generateAudio\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"audio/generate_audio\",\n \"description\": \"是否生成或保留音频。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/v1/videos\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"input\": {\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"media\": {\n \"$omitEmpty\": {\n \"$concatArrays\": [\n {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"type\": {\n \"$coalesce\": [\n {\n \"$ref\": \"media.role\"\n },\n \"reference_image\"\n ]\n },\n \"url\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.videos\"\n },\n \"as\": \"media\",\n \"in\": {\n \"type\": \"reference_video\",\n \"url\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.audios\"\n },\n \"as\": \"media\",\n \"in\": {\n \"type\": \"reference_audio\",\n \"url\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n ]\n }\n }\n },\n \"parameters\": {\n \"resolution\": {\n \"$coalesce\": [\n {\n \"$upper\": {\n \"$ref\": \"request.resolution\"\n }\n },\n \"720P\"\n ]\n },\n \"ratio\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"16:9\"\n ]\n },\n \"duration\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$ref\": \"request.duration\"\n },\n 0\n ]\n },\n \"then\": {\n \"$ref\": \"request.duration\"\n },\n \"else\": 5\n }\n },\n \"audio\": {\n \"$ref\": \"request.generateAudio\"\n },\n \"prompt_extend\": false\n }\n }\n },\n \"poll\": {\n \"method\": \"GET\",\n \"path\": \"/v1/videos/{{taskId}}\",\n \"contentType\": \"application/json\"\n },\n \"response\": {\n \"taskId\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.id\"\n },\n {\n \"$ref\": \"response.task_id\"\n },\n {\n \"$ref\": \"response.taskId\"\n },\n {\n \"$ref\": \"response.request_id\"\n },\n {\n \"$ref\": \"response.data.id\"\n },\n \"taskId\"\n ]\n },\n \"status\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.status\"\n },\n {\n \"$ref\": \"response.state\"\n },\n {\n \"$ref\": \"response.data.status\"\n },\n \"pending\"\n ]\n },\n \"message\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.error.message\"\n },\n {\n \"$ref\": \"response.message\"\n },\n {\n \"$ref\": \"response.fail_reason\"\n }\n ]\n },\n \"videos\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.metadata.url\"\n },\n {\n \"$ref\": \"response.metadata.video_url\"\n },\n {\n \"$ref\": \"response.video_url\"\n },\n {\n \"$ref\": \"response.videoUrl\"\n },\n {\n \"$ref\": \"response.result_url\"\n },\n {\n \"$ref\": \"response.url\"\n },\n {\n \"$ref\": \"response.data.metadata.url\"\n },\n {\n \"$ref\": \"response.data.video_url\"\n },\n {\n \"$ref\": \"response.output.url\"\n }\n ]\n },\n \"errorPaths\": [\n \"error.message\",\n \"error.code\"\n ],\n \"resultEphemeral\": true\n }\n },\n {\n \"id\": \"lxmone-seedance-videos\",\n \"label\": \"万有引力 Seedance 视频\",\n \"description\": \"seedance-2-pro/fast/mini、seedance-2.5-pro、seedance2.0-a~f、seedance2.5-a。\",\n \"capabilities\": [\n \"video\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://lxmone.xyz/v1\",\n \"requiresPublicMediaUrls\": true,\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"上游模型 ID。\"\n },\n {\n \"name\": \"prompt\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"prompt\",\n \"description\": \"视频提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_images\",\n \"description\": \"参考图片,宿主发布为公开 URL 后提交。\"\n },\n {\n \"name\": \"videos\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_videos\",\n \"description\": \"参考视频。\"\n },\n {\n \"name\": \"audios\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_audios\",\n \"description\": \"参考音频。\"\n },\n {\n \"name\": \"duration\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"seconds/duration/duration_seconds\",\n \"description\": \"输出时长,单位秒。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"aspect_ratio\",\n \"description\": \"画幅比例。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution/size\",\n \"description\": \"输出分辨率档位。\"\n },\n {\n \"name\": \"generateAudio\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"audio/generate_audio\",\n \"description\": \"是否生成或保留音频。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/v1/videos\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"duration_seconds\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$ref\": \"request.duration\"\n },\n 0\n ]\n },\n \"then\": {\n \"$ref\": \"request.duration\"\n },\n \"else\": 5\n }\n },\n \"resolution\": {\n \"$lower\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.resolution\"\n },\n \"720p\"\n ]\n }\n },\n \"aspect_ratio\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"16:9\"\n ]\n },\n \"reference_images\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"reference_videos\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.videos\"\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"reference_audios\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.audios\"\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n }\n },\n \"poll\": {\n \"method\": \"GET\",\n \"path\": \"/v1/videos/{{taskId}}\",\n \"contentType\": \"application/json\"\n },\n \"response\": {\n \"taskId\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.id\"\n },\n {\n \"$ref\": \"response.task_id\"\n },\n {\n \"$ref\": \"response.taskId\"\n },\n {\n \"$ref\": \"response.request_id\"\n },\n {\n \"$ref\": \"response.data.id\"\n },\n \"taskId\"\n ]\n },\n \"status\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.status\"\n },\n {\n \"$ref\": \"response.state\"\n },\n {\n \"$ref\": \"response.data.status\"\n },\n \"pending\"\n ]\n },\n \"message\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.error.message\"\n },\n {\n \"$ref\": \"response.message\"\n },\n {\n \"$ref\": \"response.fail_reason\"\n }\n ]\n },\n \"videos\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.metadata.url\"\n },\n {\n \"$ref\": \"response.metadata.video_url\"\n },\n {\n \"$ref\": \"response.video_url\"\n },\n {\n \"$ref\": \"response.videoUrl\"\n },\n {\n \"$ref\": \"response.result_url\"\n },\n {\n \"$ref\": \"response.url\"\n },\n {\n \"$ref\": \"response.data.metadata.url\"\n },\n {\n \"$ref\": \"response.data.video_url\"\n },\n {\n \"$ref\": \"response.output.url\"\n }\n ]\n },\n \"errorPaths\": [\n \"error.message\",\n \"error.code\"\n ],\n \"resultEphemeral\": true\n }\n },\n {\n \"id\": \"lxmone-h3-max-videos\",\n \"label\": \"万有引力 MiniMax H3 Max 视频\",\n \"description\": \"minimax-h3-max:POST /v1/videos,seconds、resolution 与 reference_images、reference_videos、reference_audios 扁平 URL 数组。\",\n \"capabilities\": [\n \"video\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://lxmone.xyz/v1\",\n \"requiresPublicMediaUrls\": true,\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"上游模型 ID。\"\n },\n {\n \"name\": \"prompt\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"prompt\",\n \"description\": \"视频提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_images\",\n \"description\": \"参考图片,宿主发布为公开 URL 后提交。\"\n },\n {\n \"name\": \"videos\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_videos\",\n \"description\": \"参考视频。\"\n },\n {\n \"name\": \"audios\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_audios\",\n \"description\": \"参考音频。\"\n },\n {\n \"name\": \"duration\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"seconds/duration/duration_seconds\",\n \"description\": \"输出时长,单位秒。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"aspect_ratio\",\n \"description\": \"画幅比例。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution/size\",\n \"description\": \"输出分辨率档位。\"\n },\n {\n \"name\": \"generateAudio\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"audio/generate_audio\",\n \"description\": \"是否生成或保留音频。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/v1/videos\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"seconds\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$ref\": \"request.duration\"\n },\n 0\n ]\n },\n \"then\": {\n \"$ref\": \"request.duration\"\n },\n \"else\": 5\n }\n },\n \"resolution\": {\n \"$lower\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.resolution\"\n },\n \"768p\"\n ]\n }\n },\n \"aspect_ratio\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"16:9\"\n ]\n },\n \"reference_images\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"reference_videos\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.videos\"\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"reference_audios\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.audios\"\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n }\n },\n \"poll\": {\n \"method\": \"GET\",\n \"path\": \"/v1/videos/{{taskId}}\",\n \"contentType\": \"application/json\"\n },\n \"response\": {\n \"taskId\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.id\"\n },\n {\n \"$ref\": \"response.task_id\"\n },\n {\n \"$ref\": \"response.taskId\"\n },\n {\n \"$ref\": \"response.request_id\"\n },\n {\n \"$ref\": \"response.data.id\"\n },\n \"taskId\"\n ]\n },\n \"status\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.status\"\n },\n {\n \"$ref\": \"response.state\"\n },\n {\n \"$ref\": \"response.data.status\"\n },\n \"pending\"\n ]\n },\n \"message\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.error.message\"\n },\n {\n \"$ref\": \"response.message\"\n },\n {\n \"$ref\": \"response.fail_reason\"\n }\n ]\n },\n \"videos\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.metadata.url\"\n },\n {\n \"$ref\": \"response.metadata.video_url\"\n },\n {\n \"$ref\": \"response.video_url\"\n },\n {\n \"$ref\": \"response.videoUrl\"\n },\n {\n \"$ref\": \"response.result_url\"\n },\n {\n \"$ref\": \"response.url\"\n },\n {\n \"$ref\": \"response.data.metadata.url\"\n },\n {\n \"$ref\": \"response.data.video_url\"\n },\n {\n \"$ref\": \"response.output.url\"\n }\n ]\n },\n \"errorPaths\": [\n \"error.message\",\n \"error.code\"\n ],\n \"resultEphemeral\": true\n }\n },\n {\n \"id\": \"lxmone-sd-videos\",\n \"label\": \"万有引力 SD 视频\",\n \"description\": \"sd-2.0-a/b/c、sd-2.5-a、sd-2.5:POST /v1/videos,duration 与 images、videos、audio 数组。\",\n \"capabilities\": [\n \"video\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://lxmone.xyz/v1\",\n \"requiresPublicMediaUrls\": true,\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"上游模型 ID。\"\n },\n {\n \"name\": \"prompt\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"prompt\",\n \"description\": \"视频提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_images\",\n \"description\": \"参考图片,宿主发布为公开 URL 后提交。\"\n },\n {\n \"name\": \"videos\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_videos\",\n \"description\": \"参考视频。\"\n },\n {\n \"name\": \"audios\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_audios\",\n \"description\": \"参考音频。\"\n },\n {\n \"name\": \"duration\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"seconds/duration/duration_seconds\",\n \"description\": \"输出时长,单位秒。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"aspect_ratio\",\n \"description\": \"画幅比例。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution/size\",\n \"description\": \"输出分辨率档位。\"\n },\n {\n \"name\": \"generateAudio\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"audio/generate_audio\",\n \"description\": \"是否生成或保留音频。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/v1/videos\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"duration\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$ref\": \"request.duration\"\n },\n 0\n ]\n },\n \"then\": {\n \"$ref\": \"request.duration\"\n },\n \"else\": 5\n }\n },\n \"resolution\": {\n \"$lower\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.resolution\"\n },\n \"720p\"\n ]\n }\n },\n \"aspect_ratio\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"16:9\"\n ]\n },\n \"images\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"videos\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.videos\"\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"audio\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.audios\"\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n }\n },\n \"poll\": {\n \"method\": \"GET\",\n \"path\": \"/v1/videos/{{taskId}}\",\n \"contentType\": \"application/json\"\n },\n \"response\": {\n \"taskId\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.id\"\n },\n {\n \"$ref\": \"response.task_id\"\n },\n {\n \"$ref\": \"response.taskId\"\n },\n {\n \"$ref\": \"response.request_id\"\n },\n {\n \"$ref\": \"response.data.id\"\n },\n \"taskId\"\n ]\n },\n \"status\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.status\"\n },\n {\n \"$ref\": \"response.state\"\n },\n {\n \"$ref\": \"response.data.status\"\n },\n \"pending\"\n ]\n },\n \"message\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.error.message\"\n },\n {\n \"$ref\": \"response.message\"\n },\n {\n \"$ref\": \"response.fail_reason\"\n }\n ]\n },\n \"videos\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.metadata.url\"\n },\n {\n \"$ref\": \"response.metadata.video_url\"\n },\n {\n \"$ref\": \"response.video_url\"\n },\n {\n \"$ref\": \"response.videoUrl\"\n },\n {\n \"$ref\": \"response.result_url\"\n },\n {\n \"$ref\": \"response.url\"\n },\n {\n \"$ref\": \"response.data.metadata.url\"\n },\n {\n \"$ref\": \"response.data.video_url\"\n },\n {\n \"$ref\": \"response.output.url\"\n }\n ]\n },\n \"errorPaths\": [\n \"error.message\",\n \"error.code\"\n ],\n \"resultEphemeral\": true\n }\n },\n {\n \"id\": \"lxmone-sd-mini-videos\",\n \"label\": \"万有引力 SD Mini 视频\",\n \"description\": \"sd-mini:POST /v1/videos,主图 image_url、额外参考图 reference_image_urls、音频 audio_urls。\",\n \"capabilities\": [\n \"video\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://lxmone.xyz/v1\",\n \"requiresPublicMediaUrls\": true,\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"上游模型 ID。\"\n },\n {\n \"name\": \"prompt\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"prompt\",\n \"description\": \"视频提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_images\",\n \"description\": \"参考图片,宿主发布为公开 URL 后提交。\"\n },\n {\n \"name\": \"videos\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_videos\",\n \"description\": \"参考视频。\"\n },\n {\n \"name\": \"audios\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_audios\",\n \"description\": \"参考音频。\"\n },\n {\n \"name\": \"duration\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"seconds/duration/duration_seconds\",\n \"description\": \"输出时长,单位秒。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"aspect_ratio\",\n \"description\": \"画幅比例。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution/size\",\n \"description\": \"输出分辨率档位。\"\n },\n {\n \"name\": \"generateAudio\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"audio/generate_audio\",\n \"description\": \"是否生成或保留音频。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/v1/videos\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"seconds\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$ref\": \"request.duration\"\n },\n 0\n ]\n },\n \"then\": {\n \"$ref\": \"request.duration\"\n },\n \"else\": 5\n }\n },\n \"resolution\": {\n \"$lower\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.resolution\"\n },\n \"720p\"\n ]\n }\n },\n \"aspect_ratio\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"16:9\"\n ]\n },\n \"image_url\": {\n \"$omitEmpty\": {\n \"$first\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n },\n \"reference_image_urls\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$filter\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"where\": {\n \"$gt\": [\n {\n \"$ref\": \"media.order\"\n },\n 0\n ]\n }\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"audio_urls\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.audios\"\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n }\n },\n \"poll\": {\n \"method\": \"GET\",\n \"path\": \"/v1/videos/{{taskId}}\",\n \"contentType\": \"application/json\"\n },\n \"response\": {\n \"taskId\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.id\"\n },\n {\n \"$ref\": \"response.task_id\"\n },\n {\n \"$ref\": \"response.taskId\"\n },\n {\n \"$ref\": \"response.request_id\"\n },\n {\n \"$ref\": \"response.data.id\"\n },\n \"taskId\"\n ]\n },\n \"status\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.status\"\n },\n {\n \"$ref\": \"response.state\"\n },\n {\n \"$ref\": \"response.data.status\"\n },\n \"pending\"\n ]\n },\n \"message\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.error.message\"\n },\n {\n \"$ref\": \"response.message\"\n },\n {\n \"$ref\": \"response.fail_reason\"\n }\n ]\n },\n \"videos\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.metadata.url\"\n },\n {\n \"$ref\": \"response.metadata.video_url\"\n },\n {\n \"$ref\": \"response.video_url\"\n },\n {\n \"$ref\": \"response.videoUrl\"\n },\n {\n \"$ref\": \"response.result_url\"\n },\n {\n \"$ref\": \"response.url\"\n },\n {\n \"$ref\": \"response.data.metadata.url\"\n },\n {\n \"$ref\": \"response.data.video_url\"\n },\n {\n \"$ref\": \"response.output.url\"\n }\n ]\n },\n \"errorPaths\": [\n \"error.message\",\n \"error.code\"\n ],\n \"resultEphemeral\": true\n }\n },\n {\n \"id\": \"lxmone-grok-videos\",\n \"label\": \"万有引力 Grok Imagine 视频\",\n \"description\": \"grok-imagine-video / 1.5 / 1.5S:POST /v1/videos/generations,duration、image、reference_images。\",\n \"capabilities\": [\n \"video\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://lxmone.xyz/v1\",\n \"requiresPublicMediaUrls\": true,\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"上游模型 ID。\"\n },\n {\n \"name\": \"prompt\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"prompt\",\n \"description\": \"视频提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_images\",\n \"description\": \"参考图片,宿主发布为公开 URL 后提交。\"\n },\n {\n \"name\": \"videos\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_videos\",\n \"description\": \"参考视频。\"\n },\n {\n \"name\": \"audios\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_audios\",\n \"description\": \"参考音频。\"\n },\n {\n \"name\": \"duration\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"seconds/duration/duration_seconds\",\n \"description\": \"输出时长,单位秒。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"aspect_ratio\",\n \"description\": \"画幅比例。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution/size\",\n \"description\": \"输出分辨率档位。\"\n },\n {\n \"name\": \"generateAudio\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"audio/generate_audio\",\n \"description\": \"是否生成或保留音频。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/v1/videos/generations\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"duration\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$ref\": \"request.duration\"\n },\n 0\n ]\n },\n \"then\": {\n \"$ref\": \"request.duration\"\n },\n \"else\": 6\n }\n },\n \"aspect_ratio\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"16:9\"\n ]\n },\n \"resolution\": {\n \"$lower\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.resolution\"\n },\n \"720p\"\n ]\n }\n },\n \"image\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 1\n ]\n },\n \"then\": {\n \"url\": {\n \"$first\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n },\n \"else\": null\n }\n }\n },\n \"reference_images\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 1\n ]\n },\n \"then\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"url\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"else\": null\n }\n }\n },\n \"generate_audio\": {\n \"$ref\": \"request.generateAudio\"\n }\n }\n },\n \"poll\": {\n \"method\": \"GET\",\n \"path\": \"/v1/videos/generations/{{taskId}}\",\n \"contentType\": \"application/json\"\n },\n \"response\": {\n \"taskId\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.id\"\n },\n {\n \"$ref\": \"response.task_id\"\n },\n {\n \"$ref\": \"response.taskId\"\n },\n {\n \"$ref\": \"response.request_id\"\n },\n {\n \"$ref\": \"response.data.id\"\n },\n \"taskId\"\n ]\n },\n \"status\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.status\"\n },\n {\n \"$ref\": \"response.state\"\n },\n {\n \"$ref\": \"response.data.status\"\n },\n \"pending\"\n ]\n },\n \"message\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.error.message\"\n },\n {\n \"$ref\": \"response.message\"\n },\n {\n \"$ref\": \"response.fail_reason\"\n }\n ]\n },\n \"videos\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.metadata.url\"\n },\n {\n \"$ref\": \"response.metadata.video_url\"\n },\n {\n \"$ref\": \"response.video_url\"\n },\n {\n \"$ref\": \"response.videoUrl\"\n },\n {\n \"$ref\": \"response.result_url\"\n },\n {\n \"$ref\": \"response.url\"\n },\n {\n \"$ref\": \"response.data.metadata.url\"\n },\n {\n \"$ref\": \"response.data.video_url\"\n },\n {\n \"$ref\": \"response.output.url\"\n }\n ]\n },\n \"errorPaths\": [\n \"error.message\",\n \"error.code\"\n ],\n \"resultEphemeral\": true\n }\n },\n {\n \"id\": \"lxmone-h3-workflow\",\n \"label\": \"万有引力 MiniMax H3 工作流\",\n \"description\": \"minimax-h3-a~e:POST /v1/videos/generations,input_reference、first_frame、last_frame、audio。\",\n \"capabilities\": [\n \"video\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://lxmone.xyz/v1\",\n \"requiresPublicMediaUrls\": true,\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"上游模型 ID。\"\n },\n {\n \"name\": \"prompt\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"prompt\",\n \"description\": \"视频提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_images\",\n \"description\": \"参考图片,宿主发布为公开 URL 后提交。\"\n },\n {\n \"name\": \"videos\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_videos\",\n \"description\": \"参考视频。\"\n },\n {\n \"name\": \"audios\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference_audios\",\n \"description\": \"参考音频。\"\n },\n {\n \"name\": \"duration\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"seconds/duration/duration_seconds\",\n \"description\": \"输出时长,单位秒。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"aspect_ratio\",\n \"description\": \"画幅比例。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution/size\",\n \"description\": \"输出分辨率档位。\"\n },\n {\n \"name\": \"generateAudio\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"audio/generate_audio\",\n \"description\": \"是否生成或保留音频。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/v1/videos/generations\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"seconds\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$ref\": \"request.duration\"\n },\n 0\n ]\n },\n \"then\": {\n \"$ref\": \"request.duration\"\n },\n \"else\": 5\n }\n },\n \"resolution\": {\n \"$lower\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.resolution\"\n },\n \"720p\"\n ]\n }\n },\n \"aspect_ratio\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"16:9\"\n ]\n },\n \"input_reference\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$filter\": {\n \"from\": {\n \"$ref\": \"request.images\"\n },\n \"as\": \"media\",\n \"where\": {\n \"$and\": [\n {\n \"$ne\": [\n {\n \"$ref\": \"media.role\"\n },\n \"first_frame\"\n ]\n },\n {\n \"$ne\": [\n {\n \"$ref\": \"media.role\"\n },\n \"last_frame\"\n ]\n }\n ]\n }\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"first_frame\": {\n \"$omitEmpty\": {\n \"$first\": {\n \"$map\": {\n \"from\": {\n \"$filter\": {\n \"from\": {\n \"$ref\": \"request.images\"\n },\n \"as\": \"media\",\n \"where\": {\n \"$eq\": [\n {\n \"$ref\": \"media.role\"\n },\n \"first_frame\"\n ]\n }\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n },\n \"last_frame\": {\n \"$omitEmpty\": {\n \"$first\": {\n \"$map\": {\n \"from\": {\n \"$filter\": {\n \"from\": {\n \"$ref\": \"request.images\"\n },\n \"as\": \"media\",\n \"where\": {\n \"$eq\": [\n {\n \"$ref\": \"media.role\"\n },\n \"last_frame\"\n ]\n }\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n },\n \"audio\": {\n \"$omitEmpty\": {\n \"$first\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.audios\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n }\n }\n },\n \"poll\": {\n \"method\": \"GET\",\n \"path\": \"/v1/videos/generations/{{taskId}}\",\n \"contentType\": \"application/json\"\n },\n \"response\": {\n \"taskId\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.id\"\n },\n {\n \"$ref\": \"response.task_id\"\n },\n {\n \"$ref\": \"response.taskId\"\n },\n {\n \"$ref\": \"response.request_id\"\n },\n {\n \"$ref\": \"response.data.id\"\n },\n \"taskId\"\n ]\n },\n \"status\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.status\"\n },\n {\n \"$ref\": \"response.state\"\n },\n {\n \"$ref\": \"response.data.status\"\n },\n \"pending\"\n ]\n },\n \"message\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.error.message\"\n },\n {\n \"$ref\": \"response.message\"\n },\n {\n \"$ref\": \"response.fail_reason\"\n }\n ]\n },\n \"videos\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.metadata.url\"\n },\n {\n \"$ref\": \"response.metadata.video_url\"\n },\n {\n \"$ref\": \"response.video_url\"\n },\n {\n \"$ref\": \"response.videoUrl\"\n },\n {\n \"$ref\": \"response.result_url\"\n },\n {\n \"$ref\": \"response.url\"\n },\n {\n \"$ref\": \"response.data.metadata.url\"\n },\n {\n \"$ref\": \"response.data.video_url\"\n },\n {\n \"$ref\": \"response.output.url\"\n }\n ]\n },\n \"errorPaths\": [\n \"error.message\",\n \"error.code\"\n ],\n \"resultEphemeral\": true\n }\n }\n ]\n },\n \"documentation\": \"<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>\"\n}\n```\n\n" +} diff --git a/plugin-packages/metaso-h3.yingce-plugin b/plugin-packages/metaso-h3.yingce-plugin new file mode 100644 index 0000000000000000000000000000000000000000..6b4ece702d7c61651e5058214e459df3fb08bccd GIT binary patch literal 8173 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 b>>2O>BGn=C$SD80g7|ON{)?aw|JnU7C=ixw literal 0 HcmV?d00001 diff --git a/plugin-packages/metaso-h3/README.md b/plugin-packages/metaso-h3/README.md new file mode 100644 index 000000000..47a8aa668 --- /dev/null +++ b/plugin-packages/metaso-h3/README.md @@ -0,0 +1,22 @@ +# METASO MiniMax H3 + +秘塔 METASO MiniMax H3 文生视频 / 图生视频协议插件(异步任务模式)。 + +## Provider + +| Provider | 能力 | 端点 | +|---|---|---| +| metaso-h3 | video | POST /api/minimax/v2/video_generation(创建)
GET /api/minimax/v2/query/video_generation/{taskId}(轮询) | + +## 配置 + +在渠道中填写 API Key(METASO 控制台创建,形如 `mk-xxxxxxxx`),baseUrl 使用 `https://metaso.cn`。 + +## 说明 + +- 模型:`MiniMax-H3`(支持 768P / 2K,4-15 秒,文生图/图生视频/多模态参考),`MiniMax-H3-Max`(480P / 768P,5-15 秒)。 +- 文生视频:仅文本提示词,ratio 必填(21:9 / 16:9 / 4:3 / 1:1 / 3:4 / 9:16)。 +- 图生视频:图片 role 为 first_frame / last_frame(首尾帧)。 +- 多模态参考:图片 reference_image / 视频 reference_video / 音频 reference_audio。 +- 异步任务:创建后返回 task_id,轮询查询接口直到 status=succeeded,成片 URL 在 task.content.url。 +- 鉴权:`Authorization: Bearer mk-xxx`。 diff --git a/plugin-packages/metaso-h3/docs/interface.md b/plugin-packages/metaso-h3/docs/interface.md new file mode 100644 index 000000000..57f64e02c --- /dev/null +++ b/plugin-packages/metaso-h3/docs/interface.md @@ -0,0 +1,437 @@ +# METASO MiniMax H3 接口说明 + +## 鉴权 + +`Authorization: Bearer mk-xxxxxxxxxxxxxxxxxxxxxxxx` + +## 创建视频生成任务 + +POST `https://metaso.cn/api/minimax/v2/video_generation` + +请求体: + +```json +{ + "model": "MiniMax-H3", + "content": [ + { "type": "text", "text": "视频描述" }, + { "type": "image_url", "image_url": { "url": "https://..." }, "role": "first_frame" }, + { "type": "video_url", "video_url": { "url": "https://..." }, "role": "reference_video" }, + { "type": "audio_url", "audio_url": { "url": "https://..." }, "role": "reference_audio" } + ], + "resolution": "768P", + "duration": 5, + "ratio": "16:9" +} +``` + +- content 必须包含一个非空 text 项。 +- 首尾帧(first_frame/last_frame)与多模态参考(reference_*)互斥。 +- 图片 ≤ 9 张、视频 ≤ 3 段、音频 ≤ 3 段;请求体总大小 ≤ 64 MB。 + +响应: + +```json +{ "task_id": "424010985738629" } +``` + +## 查询任务 + +GET `https://metaso.cn/api/minimax/v2/query/video_generation/{task_id}` + +响应: + +```json +{ + "task": { + "id": "424010985738629", + "status": "succeeded", + "content": { "url": "https://.../output.mp4" }, + "resolution": "2K", + "duration": 5, + "usage": { "total_seconds": 5, "output_seconds": 5 } + } +} +``` + +status 取值:queued / running / succeeded / failed / cancelled。 + +## 错误 + +创建与查询均返回 OpenAI 风格错误体,含 `error.type` / `error.message` / `request_id`;余额不足为 402,速率限制为 429。 + + +## Manifest 完整接口定义 + +以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。 + +```json +{ + "apiVersion": "yingce.plugin/v2", + "id": "metaso-h3", + "name": "METASO MiniMax H3", + "version": "1.0.0", + "author": "METASO", + "description": "秘塔 METASO MiniMax H3 文生视频/图生视频协议插件(异步任务)。", + "permissions": [ + "generation.run", + "media.read" + ], + "configuration": { + "fields": [ + { + "name": "apiKey", + "type": "secret", + "label": "API Key", + "required": true + } + ] + }, + "contributes": { + "providers": [ + { + "id": "metaso-h3", + "label": "METASO MiniMax H3", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://metaso.cn", + "auth": { + "type": "bearer", + "field": "apiKey" + }, + "requiresPublicMediaUrls": true, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "视频模型 ID(MiniMax-H3 / MiniMax-H3-Max)。" + }, + { + "name": "prompt", + "type": "string", + "required": true, + "mapping": "prompt/content/input", + "description": "视频提示词。" + }, + { + "name": "images", + "type": "media[]", + "required": false, + "mapping": "first/last/reference image", + "description": "显式 role 图片输入(首帧/尾帧/参考图)。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference video", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference audio/voice", + "description": "参考音频或音色。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "duration/seconds", + "description": "时长秒数(4-15)。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "ratio/aspect_ratio/size", + "description": "画幅比例(16:9 等)。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution", + "description": "分辨率档位(768P / 2K)。" + }, + { + "name": "watermark", + "type": "boolean", + "required": false, + "mapping": "watermark", + "description": "AIGC 水印开关。" + }, + { + "name": "providerOptions", + "type": "object", + "required": false, + "mapping": "provider-specific fields", + "description": "插件命名空间内的厂商扩展字段。" + } + ], + "create": { + "method": "POST", + "path": "/api/minimax/v2/video_generation", + "contentType": "application/json", + "body": { + "model": { + "$coalesce": [ + { + "$ref": "request.model" + }, + "MiniMax-H3" + ] + }, + "content": { + "$concatArrays": [ + { + "$if": { + "condition": { + "$gt": [ + { + "$len": { + "$ref": "request.prompt" + } + }, + 0 + ] + }, + "then": [ + { + "type": "text", + "text": { + "$ref": "request.prompt" + } + } + ], + "else": [] + } + }, + { + "$map": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "in": { + "type": "image_url", + "image_url": { + "url": { + "$coalesce": [ + { + "$ref": "media.url" + }, + { + "$ref": "media.dataUrl" + } + ] + } + }, + "role": { + "$omitEmpty": { + "$ref": "media.role" + } + } + } + } + }, + { + "$map": { + "from": { + "$ref": "request.videos" + }, + "as": "media", + "in": { + "type": "video_url", + "video_url": { + "url": { + "$coalesce": [ + { + "$ref": "media.url" + }, + { + "$ref": "media.dataUrl" + } + ] + } + }, + "role": { + "$omitEmpty": { + "$ref": "media.role" + } + } + } + } + }, + { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "type": "audio_url", + "audio_url": { + "url": { + "$coalesce": [ + { + "$ref": "media.url" + }, + { + "$ref": "media.dataUrl" + } + ] + } + }, + "role": { + "$omitEmpty": { + "$ref": "media.role" + } + } + } + } + } + ] + }, + "resolution": { + "$omitEmpty": { + "$coalesce": [ + { + "$ref": "request.resolution" + }, + { + "$ref": "request.providerOptions.metaso-h3.resolution" + }, + "768P" + ] + } + }, + "duration": { + "$omitEmpty": { + "$coalesce": [ + { + "$ref": "request.duration" + }, + { + "$ref": "request.providerOptions.metaso-h3.duration" + }, + 5 + ] + } + }, + "ratio": { + "$omitEmpty": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + { + "$ref": "request.providerOptions.metaso-h3.ratio" + }, + "16:9" + ] + } + }, + "aigc_watermark": { + "$omitEmpty": { + "$ref": "request.providerOptions.metaso-h3.aigc_watermark" + } + } + } + }, + "poll": { + "method": "GET", + "path": "/api/minimax/v2/query/video_generation/{{taskId}}" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.data.task_id" + } + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.task.status" + }, + { + "$ref": "response.status" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.task.error.message" + }, + { + "$ref": "response.task.fail_reason" + }, + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.task.content.url" + }, + { + "$ref": "response.task.content.video_url" + }, + { + "$ref": "response.content.url" + }, + { + "$ref": "response.data.url" + } + ] + }, + "usage": { + "$ref": "response.task.usage" + }, + "errorPaths": [ + "task.error.type", + "error.type", + "error.code" + ], + "resultEphemeral": true, + "messagePaths": [ + "task.error.message", + "error.message" + ] + } + } + ] + }, + "documentation": "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>" +} +``` + diff --git a/plugin-packages/metaso-h3/manifest.json b/plugin-packages/metaso-h3/manifest.json new file mode 100644 index 000000000..0e70020df --- /dev/null +++ b/plugin-packages/metaso-h3/manifest.json @@ -0,0 +1,367 @@ +{ + "apiVersion": "yingce.plugin/v2", + "id": "metaso-h3", + "name": "METASO MiniMax H3", + "version": "1.0.0", + "author": "METASO", + "description": "秘塔 METASO MiniMax H3 文生视频/图生视频协议插件(异步任务)。", + "permissions": [ + "generation.run", + "media.read" + ], + "configuration": { + "fields": [ + { + "name": "apiKey", + "type": "secret", + "label": "API Key", + "required": true + } + ] + }, + "contributes": { + "providers": [ + { + "id": "metaso-h3", + "label": "METASO MiniMax H3", + "capabilities": [ + "video" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://metaso.cn", + "auth": { + "type": "bearer", + "field": "apiKey" + }, + "requiresPublicMediaUrls": true, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "视频模型 ID(MiniMax-H3 / MiniMax-H3-Max)。" + }, + { + "name": "prompt", + "type": "string", + "required": true, + "mapping": "prompt/content/input", + "description": "视频提示词。" + }, + { + "name": "images", + "type": "media[]", + "required": false, + "mapping": "first/last/reference image", + "description": "显式 role 图片输入(首帧/尾帧/参考图)。" + }, + { + "name": "videos", + "type": "media[]", + "required": false, + "mapping": "reference video", + "description": "参考视频。" + }, + { + "name": "audios", + "type": "media[]", + "required": false, + "mapping": "reference audio/voice", + "description": "参考音频或音色。" + }, + { + "name": "duration", + "type": "integer", + "required": false, + "mapping": "duration/seconds", + "description": "时长秒数(4-15)。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "ratio/aspect_ratio/size", + "description": "画幅比例(16:9 等)。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution", + "description": "分辨率档位(768P / 2K)。" + }, + { + "name": "watermark", + "type": "boolean", + "required": false, + "mapping": "watermark", + "description": "AIGC 水印开关。" + }, + { + "name": "providerOptions", + "type": "object", + "required": false, + "mapping": "provider-specific fields", + "description": "插件命名空间内的厂商扩展字段。" + } + ], + "create": { + "method": "POST", + "path": "/api/minimax/v2/video_generation", + "contentType": "application/json", + "body": { + "model": { + "$coalesce": [ + { + "$ref": "request.model" + }, + "MiniMax-H3" + ] + }, + "content": { + "$concatArrays": [ + { + "$if": { + "condition": { + "$gt": [ + { + "$len": { + "$ref": "request.prompt" + } + }, + 0 + ] + }, + "then": [ + { + "type": "text", + "text": { + "$ref": "request.prompt" + } + } + ], + "else": [] + } + }, + { + "$map": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "in": { + "type": "image_url", + "image_url": { + "url": { + "$coalesce": [ + { + "$ref": "media.url" + }, + { + "$ref": "media.dataUrl" + } + ] + } + }, + "role": { + "$omitEmpty": { + "$ref": "media.role" + } + } + } + } + }, + { + "$map": { + "from": { + "$ref": "request.videos" + }, + "as": "media", + "in": { + "type": "video_url", + "video_url": { + "url": { + "$coalesce": [ + { + "$ref": "media.url" + }, + { + "$ref": "media.dataUrl" + } + ] + } + }, + "role": { + "$omitEmpty": { + "$ref": "media.role" + } + } + } + } + }, + { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "type": "audio_url", + "audio_url": { + "url": { + "$coalesce": [ + { + "$ref": "media.url" + }, + { + "$ref": "media.dataUrl" + } + ] + } + }, + "role": { + "$omitEmpty": { + "$ref": "media.role" + } + } + } + } + } + ] + }, + "resolution": { + "$omitEmpty": { + "$coalesce": [ + { + "$ref": "request.resolution" + }, + { + "$ref": "request.providerOptions.metaso-h3.resolution" + }, + "768P" + ] + } + }, + "duration": { + "$omitEmpty": { + "$coalesce": [ + { + "$ref": "request.duration" + }, + { + "$ref": "request.providerOptions.metaso-h3.duration" + }, + 5 + ] + } + }, + "ratio": { + "$omitEmpty": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + { + "$ref": "request.providerOptions.metaso-h3.ratio" + }, + "16:9" + ] + } + }, + "aigc_watermark": { + "$omitEmpty": { + "$ref": "request.providerOptions.metaso-h3.aigc_watermark" + } + } + } + }, + "poll": { + "method": "GET", + "path": "/api/minimax/v2/query/video_generation/{{taskId}}" + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.task_id" + }, + { + "$ref": "response.taskId" + }, + { + "$ref": "response.data.task_id" + } + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.task.status" + }, + { + "$ref": "response.status" + }, + { + "$ref": "response.data.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.task.error.message" + }, + { + "$ref": "response.task.fail_reason" + }, + { + "$ref": "response.error.message" + }, + { + "$ref": "response.message" + } + ] + }, + "videos": { + "$coalesce": [ + { + "$ref": "response.task.content.url" + }, + { + "$ref": "response.task.content.video_url" + }, + { + "$ref": "response.content.url" + }, + { + "$ref": "response.data.url" + } + ] + }, + "usage": { + "$ref": "response.task.usage" + }, + "errorPaths": [ + "task.error.type", + "error.type", + "error.code" + ], + "resultEphemeral": true, + "messagePaths": [ + "task.error.message", + "error.message" + ] + } + } + ] + }, + "documentation": "# METASO MiniMax H3\n\n秘塔 METASO MiniMax H3 文生视频 / 图生视频协议插件(异步任务模式)。\n\n## Provider\n\n| Provider | 能力 | 端点 |\n|---|---|---|\n| metaso-h3 | video | POST /api/minimax/v2/video_generation(创建)
GET /api/minimax/v2/query/video_generation/{taskId}(轮询) |\n\n## 配置\n\n在渠道中填写 API Key(METASO 控制台创建,形如 `mk-xxxxxxxx`),baseUrl 使用 `https://metaso.cn`。\n\n## 说明\n\n- 模型:`MiniMax-H3`(支持 768P / 2K,4-15 秒,文生图/图生视频/多模态参考),`MiniMax-H3-Max`(480P / 768P,5-15 秒)。\n- 文生视频:仅文本提示词,ratio 必填(21:9 / 16:9 / 4:3 / 1:1 / 3:4 / 9:16)。\n- 图生视频:图片 role 为 first_frame / last_frame(首尾帧)。\n- 多模态参考:图片 reference_image / 视频 reference_video / 音频 reference_audio。\n- 异步任务:创建后返回 task_id,轮询查询接口直到 status=succeeded,成片 URL 在 task.content.url。\n- 鉴权:`Authorization: Bearer mk-xxx`。\n\n---\n\n# METASO MiniMax H3 接口说明\n\n## 鉴权\n\n`Authorization: Bearer mk-xxxxxxxxxxxxxxxxxxxxxxxx`\n\n## 创建视频生成任务\n\nPOST `https://metaso.cn/api/minimax/v2/video_generation`\n\n请求体:\n\n```json\n{\n \"model\": \"MiniMax-H3\",\n \"content\": [\n { \"type\": \"text\", \"text\": \"视频描述\" },\n { \"type\": \"image_url\", \"image_url\": { \"url\": \"https://...\" }, \"role\": \"first_frame\" },\n { \"type\": \"video_url\", \"video_url\": { \"url\": \"https://...\" }, \"role\": \"reference_video\" },\n { \"type\": \"audio_url\", \"audio_url\": { \"url\": \"https://...\" }, \"role\": \"reference_audio\" }\n ],\n \"resolution\": \"768P\",\n \"duration\": 5,\n \"ratio\": \"16:9\"\n}\n```\n\n- content 必须包含一个非空 text 项。\n- 首尾帧(first_frame/last_frame)与多模态参考(reference_*)互斥。\n- 图片 ≤ 9 张、视频 ≤ 3 段、音频 ≤ 3 段;请求体总大小 ≤ 64 MB。\n\n响应:\n\n```json\n{ \"task_id\": \"424010985738629\" }\n```\n\n## 查询任务\n\nGET `https://metaso.cn/api/minimax/v2/query/video_generation/{task_id}`\n\n响应:\n\n```json\n{\n \"task\": {\n \"id\": \"424010985738629\",\n \"status\": \"succeeded\",\n \"content\": { \"url\": \"https://.../output.mp4\" },\n \"resolution\": \"2K\",\n \"duration\": 5,\n \"usage\": { \"total_seconds\": 5, \"output_seconds\": 5 }\n }\n}\n```\n\nstatus 取值:queued / running / succeeded / failed / cancelled。\n\n## 错误\n\n创建与查询均返回 OpenAI 风格错误体,含 `error.type` / `error.message` / `request_id`;余额不足为 402,速率限制为 429。\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"metaso-h3\",\n \"name\": \"METASO MiniMax H3\",\n \"version\": \"1.0.0\",\n \"author\": \"METASO\",\n \"description\": \"秘塔 METASO MiniMax H3 文生视频/图生视频协议插件(异步任务)。\",\n \"permissions\": [\n \"generation.run\",\n \"media.read\"\n ],\n \"configuration\": {\n \"fields\": [\n {\n \"name\": \"apiKey\",\n \"type\": \"secret\",\n \"label\": \"API Key\",\n \"required\": true\n }\n ]\n },\n \"contributes\": {\n \"providers\": [\n {\n \"id\": \"metaso-h3\",\n \"label\": \"METASO MiniMax H3\",\n \"capabilities\": [\n \"video\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://metaso.cn\",\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"requiresPublicMediaUrls\": true,\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"视频模型 ID(MiniMax-H3 / MiniMax-H3-Max)。\"\n },\n {\n \"name\": \"prompt\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"prompt/content/input\",\n \"description\": \"视频提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"first/last/reference image\",\n \"description\": \"显式 role 图片输入(首帧/尾帧/参考图)。\"\n },\n {\n \"name\": \"videos\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference video\",\n \"description\": \"参考视频。\"\n },\n {\n \"name\": \"audios\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference audio/voice\",\n \"description\": \"参考音频或音色。\"\n },\n {\n \"name\": \"duration\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"duration/seconds\",\n \"description\": \"时长秒数(4-15)。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"ratio/aspect_ratio/size\",\n \"description\": \"画幅比例(16:9 等)。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution\",\n \"description\": \"分辨率档位(768P / 2K)。\"\n },\n {\n \"name\": \"watermark\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"watermark\",\n \"description\": \"AIGC 水印开关。\"\n },\n {\n \"name\": \"providerOptions\",\n \"type\": \"object\",\n \"required\": false,\n \"mapping\": \"provider-specific fields\",\n \"description\": \"插件命名空间内的厂商扩展字段。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/api/minimax/v2/video_generation\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.model\"\n },\n \"MiniMax-H3\"\n ]\n },\n \"content\": {\n \"$concatArrays\": [\n {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$ref\": \"request.prompt\"\n }\n },\n 0\n ]\n },\n \"then\": [\n {\n \"type\": \"text\",\n \"text\": {\n \"$ref\": \"request.prompt\"\n }\n }\n ],\n \"else\": []\n }\n },\n {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.images\"\n },\n \"as\": \"media\",\n \"in\": {\n \"type\": \"image_url\",\n \"image_url\": {\n \"url\": {\n \"$coalesce\": [\n {\n \"$ref\": \"media.url\"\n },\n {\n \"$ref\": \"media.dataUrl\"\n }\n ]\n }\n },\n \"role\": {\n \"$omitEmpty\": {\n \"$ref\": \"media.role\"\n }\n }\n }\n }\n },\n {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.videos\"\n },\n \"as\": \"media\",\n \"in\": {\n \"type\": \"video_url\",\n \"video_url\": {\n \"url\": {\n \"$coalesce\": [\n {\n \"$ref\": \"media.url\"\n },\n {\n \"$ref\": \"media.dataUrl\"\n }\n ]\n }\n },\n \"role\": {\n \"$omitEmpty\": {\n \"$ref\": \"media.role\"\n }\n }\n }\n }\n },\n {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.audios\"\n },\n \"as\": \"media\",\n \"in\": {\n \"type\": \"audio_url\",\n \"audio_url\": {\n \"url\": {\n \"$coalesce\": [\n {\n \"$ref\": \"media.url\"\n },\n {\n \"$ref\": \"media.dataUrl\"\n }\n ]\n }\n },\n \"role\": {\n \"$omitEmpty\": {\n \"$ref\": \"media.role\"\n }\n }\n }\n }\n }\n ]\n },\n \"resolution\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.resolution\"\n },\n {\n \"$ref\": \"request.providerOptions.metaso-h3.resolution\"\n },\n \"768P\"\n ]\n }\n },\n \"duration\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.duration\"\n },\n {\n \"$ref\": \"request.providerOptions.metaso-h3.duration\"\n },\n 5\n ]\n }\n },\n \"ratio\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n {\n \"$ref\": \"request.providerOptions.metaso-h3.ratio\"\n },\n \"16:9\"\n ]\n }\n },\n \"aigc_watermark\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.metaso-h3.aigc_watermark\"\n }\n }\n }\n },\n \"poll\": {\n \"method\": \"GET\",\n \"path\": \"/api/minimax/v2/query/video_generation/{{taskId}}\"\n },\n \"response\": {\n \"taskId\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.task_id\"\n },\n {\n \"$ref\": \"response.taskId\"\n },\n {\n \"$ref\": \"response.data.task_id\"\n }\n ]\n },\n \"status\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.task.status\"\n },\n {\n \"$ref\": \"response.status\"\n },\n {\n \"$ref\": \"response.data.status\"\n },\n \"pending\"\n ]\n },\n \"message\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.task.error.message\"\n },\n {\n \"$ref\": \"response.task.fail_reason\"\n },\n {\n \"$ref\": \"response.error.message\"\n },\n {\n \"$ref\": \"response.message\"\n }\n ]\n },\n \"videos\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.task.content.url\"\n },\n {\n \"$ref\": \"response.task.content.video_url\"\n },\n {\n \"$ref\": \"response.content.url\"\n },\n {\n \"$ref\": \"response.data.url\"\n }\n ]\n },\n \"usage\": {\n \"$ref\": \"response.task.usage\"\n },\n \"errorPaths\": [\n \"task.error.type\",\n \"error.type\",\n \"error.code\"\n ],\n \"resultEphemeral\": true,\n \"messagePaths\": [\n \"task.error.message\",\n \"error.message\"\n ]\n }\n }\n ]\n },\n \"documentation\": \"<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>\"\n}\n```\n\n" +} diff --git a/plugin-packages/openai-images.yingce-plugin b/plugin-packages/openai-images.yingce-plugin index 806624b25f31199a45431a74f4f1169c20155b8e..0305559ca6424147926e75a333841a89537b53fd 100644 GIT binary patch delta 9273 zcmZXaRZyKlo2_x*xI=IW5?mA9H^JTA-7UZu+%32Vhv4q+?ry;euz}#t$W{fHoLXD)@+n4^oM zT%%dV}twXS2pA!zP!=PGlp@iV}wmfF=mx150{)1UpTEW>Ue+*i`%G7RaFrzst z5l4KT^2Fwsr~P8~?j~OTQ>BCj7_c?LOu4|7uA188HchV+0xrp@R;TVZ(1}cXYMc3F z1M=e5)a-GMPG?}+FI~*y#?3$El)Ta1!j}JdeH~RpqdpmPdU)=#w^UgBP|-E7_Ao5T zKy7O(pu!U0T8n1#RJ?}3uW2L-=k?edC(`tDvQ62-TSlQZdhiMnVMDLzFXV8m6ZGoS z`uEcHbxGatt&RP*vP)PCsR^nLVk^qq=~)X}b<7&f_GlXrC{ zVxBy1cc$!$A#pAJ?O4T2_6aHI@n%JF4~Lu}BCuOJ9IoP5p#L|i5q!=Jy?9)CHhj8- zNlRO!ARl{pLM{!<(?$YxTf*>YRwZ;A#3X-Tk~8abUrV(v1dEAHK;Q4%61hjIShyFM z`Y-dW)RncGtjYivEuG-+z`__d3Sqy;8CCDgt*j+p4GOyywU1`|xP$V(g@>*}zG~td z5hwd4yX2Hj1AF;mBAma5L`)($%ZR3Xwb6#MQ|c}E>+3v-Yla5)E>i`9yT3m-L|4q! z8p{j#Gl;YkRhX2W0y03^*99r0Ow53fKcZy^_5_Qt;}{lElI|XUxJFmYy`d-{Bf%-A z^an6ehu1hQ(<+}~7v-lAJZJhIMW#nj8Ze;d9y~`@Ry7^e zq|}%l6S>#3_Swx`^p29<{uuo9GtG}WDbAZAZls^uG1wuT9r*gHZ<2uqj+e{_Usmvc zERqVtb6Vrt{#n+ML}?RwN!6d4xObjEE}1qq@GBp<2lIlB-I|Am;MeBQLv< zM1`e);!YXaWT@Jh^wf%d6s40_3PL+bt|;s@&J%1%SQT^7iZ+8~9EY(h_|@2t;Qhzf zodBHB&UL7j0!(6bm?h!@48IGY5=VAQ*QGz;2|b+GLbpLK8>?(g)8i-Bd)tyw41edN zlmrCLNmD^ziF17XGneq@r7))R?b9H>_{@T@GMK-vuV%<)67wwCxIHzJF-eW-gDj?E zd_8WH^1i@b-X$M?8y_uGSwZPvwv@l(iS|Rx8*RVWI-#FAp7E*DDYHVyud3>ZQ?n4+fzs#IcwaNkd}YpG_wmp}fJ+OaTR@$oz;@ zw8svd-+|gp+?z$s!>8Tp^|*<}qchGd+SS$(ub1rBM{H#KI1*1!cy4a7I_#wTTb-h= z*O`~6A_G7DYWwtQx|=eBJLH_~qtygHC$FAFMwl$wt8a!wpZX2j#qk5%vWSBwIlkUoo)Th{X z?gQb#s)pz9Cto{>^c4;z$%p;MO`?Ogx1?duXCdIUE0 z0Wuzge~OBM*{JEcoL1o`6_CeDAX1I;OZnDzo9taaXe?UQ_JsHTGlm;g7aP5YDMkX> zT;;akKgPqfu$o1qd9jfireE+FY%MUo_i{p}V%a&B0re$I{O?nS®R(VTp(b;Y6I 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 diff --git a/plugin-packages/openai-images/docs/interface.md b/plugin-packages/openai-images/docs/interface.md index 2cce8744a..0d01706b8 100644 --- a/plugin-packages/openai-images/docs/interface.md +++ b/plugin-packages/openai-images/docs/interface.md @@ -40,8 +40,6 @@ | `create.contentType` | `"application/json"` | | `create.body.model` | `{"$ref":"request.model"}` | | `create.body.prompt` | `{"$ref":"request.prompt"}` | -| `create.body.images` | `{"$omitEmpty":{"$if":{"condition":{"$gt":[{"$len":{"$ref":"request.images"}},0]},"then":{"$map":{"from":{"$filter":{"from":{"$sortByOrder":{"$ref":"request.images"}},"as":"media","where":{"$ne":[{"$ref":"media.role"},"mask"]}}},"as":"media","in":{"image_url":{"$ref":"media.value"}}}},"else":null}}}` | -| `create.body.mask` | `{"$omitEmpty":{"$if":{"condition":{"$gt":[{"$len":{"$filter":{"from":{"$ref":"request.images"},"as":"media","where":{"$eq":[{"$ref":"media.role"},"mask"]}}}},0]},"then":{"image_url":{"$first":{"$map":{"from":{"$filter":{"from":{"$sortByOrder":{"$ref":"request.images"}},"as":"media","where":{"$eq":[{"$ref":"media.role"},"mask"]}}},"as":"media","in":{"$ref":"media.value"}}}}},"else":null}}}` | | `create.body.n` | `{"$omitEmpty":{"$if":{"condition":{"$gt":[{"$ref":"request.imageCount"},0]},"then":{"$ref":"request.imageCount"},"else":1}}}` | | `create.body.size` | `{"$omitEmpty":{"$if":{"condition":{"$in":[{"$lower":{"$trim":{"$ref":"request.aspectRatio"}}},["","auto"]]},"then":null,"else":{"$ref":"request.aspectRatio"}}}}` | | `create.body.quality` | `{"$omitEmpty":{"$switch":{"cases":[{"when":{"$in":[{"$lower":{"$trim":{"$ref":"request.quality"}}},["1k"]]},"then":"low"},{"when":{"$in":[{"$lower":{"$trim":{"$ref":"request.quality"}}},["2k"]]},"then":"medium"},{"when":{"$in":[{"$lower":{"$trim":{"$ref":"request.quality"}}},["4k"]]},"then":"high"},{"when":{"$in":[{"$lower":{"$trim":{"$ref":"request.quality"}}},["","auto"]]},"then":null}],"default":{"$ref":"request.quality"}}}}` | @@ -49,9 +47,15 @@ | `create.body.output_format` | `{"$omitEmpty":{"$coalesce":[{"$ref":"request.providerOptions.openai-image.output_format"},"png"]}}` | | `create.body.output_compression` | `{"$omitEmpty":{"$ref":"request.providerOptions.openai-image.output_compression"}}` | | `create.body.moderation` | `{"$omitEmpty":{"$ref":"request.providerOptions.openai-image.moderation"}}` | -| `create.body.response_format` | `{"$omitEmpty":{"$ref":"request.providerOptions.openai-image.response_format"}}` | +| `create.body.response_format` | `{"$omitEmpty":{"$coalesce":[{"$ref":"request.providerOptions.openai-image.response_format"},"b64_json"]}}` | | `create.body.style` | `{"$omitEmpty":{"$ref":"request.providerOptions.openai-image.style"}}` | | `create.body.user` | `{"$omitEmpty":{"$ref":"request.providerOptions.openai-image.user"}}` | +| `create.files[0].name` | `"image"` | +| `create.files[0].source` | `{"$filter":{"from":{"$ref":"request.images"},"as":"media","where":{"$ne":[{"$ref":"media.role"},"mask"]}}}` | +| `create.files[0].filename` | `"source.png"` | +| `create.files[1].name` | `"mask"` | +| `create.files[1].source` | `{"$filter":{"from":{"$ref":"request.images"},"as":"media","where":{"$eq":[{"$ref":"media.role"},"mask"]}}}` | +| `create.files[1].filename` | `"mask.png"` | ## Provider 扩展键 @@ -81,7 +85,7 @@ ## 兼容边界 -无参考图走 JSON generations;有参考图或蒙版走 JSON edits,并按官方 images 数组传入多张 image_url。quality 的 1k/2k/4k 映射为 OpenAI low/medium/high。 +无参考图走 JSON generations;有参考图或蒙版走 multipart edits。quality 的 1k/2k/4k 映射为 OpenAI low/medium/high。 ## Manifest 完整接口定义 @@ -127,7 +131,7 @@ "agent" ], "baseUrl": "https://api.openai.com", - "requiresPublicMediaUrls": true, + "requiresPublicMediaUrls": false, "auth": { "type": "bearer", "field": "apiKey" @@ -210,6 +214,22 @@ } }, "contentType": "application/json", + "contentTypeTemplate": { + "$if": { + "condition": { + "$gt": [ + { + "$len": { + "$ref": "request.images" + } + }, + 0 + ] + }, + "then": "multipart/form-data", + "else": "application/json" + } + }, "body": { "model": { "$ref": "request.model" @@ -217,111 +237,6 @@ "prompt": { "$ref": "request.prompt" }, - "images": { - "$omitEmpty": { - "$if": { - "condition": { - "$gt": [ - { - "$len": { - "$ref": "request.images" - } - }, - 0 - ] - }, - "then": { - "$map": { - "from": { - "$filter": { - "from": { - "$sortByOrder": { - "$ref": "request.images" - } - }, - "as": "media", - "where": { - "$ne": [ - { - "$ref": "media.role" - }, - "mask" - ] - } - } - }, - "as": "media", - "in": { - "image_url": { - "$ref": "media.value" - } - } - } - }, - "else": null - } - } - }, - "mask": { - "$omitEmpty": { - "$if": { - "condition": { - "$gt": [ - { - "$len": { - "$filter": { - "from": { - "$ref": "request.images" - }, - "as": "media", - "where": { - "$eq": [ - { - "$ref": "media.role" - }, - "mask" - ] - } - } - } - }, - 0 - ] - }, - "then": { - "image_url": { - "$first": { - "$map": { - "from": { - "$filter": { - "from": { - "$sortByOrder": { - "$ref": "request.images" - } - }, - "as": "media", - "where": { - "$eq": [ - { - "$ref": "media.role" - }, - "mask" - ] - } - } - }, - "as": "media", - "in": { - "$ref": "media.value" - } - } - } - } - }, - "else": null - } - } - }, "n": { "$omitEmpty": { "$if": { @@ -490,7 +405,12 @@ }, "response_format": { "$omitEmpty": { - "$ref": "request.providerOptions.openai-image.response_format" + "$coalesce": [ + { + "$ref": "request.providerOptions.openai-image.response_format" + }, + "b64_json" + ] } }, "style": { @@ -503,7 +423,49 @@ "$ref": "request.providerOptions.openai-image.user" } } - } + }, + "files": [ + { + "name": "image", + "source": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$ne": [ + { + "$ref": "media.role" + }, + "mask" + ] + } + } + }, + "filename": "source.png" + }, + { + "name": "mask", + "source": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$eq": [ + { + "$ref": "media.role" + }, + "mask" + ] + } + } + }, + "filename": "mask.png" + } + ] }, "response": { "status": "succeeded", diff --git a/plugin-packages/openai-images/manifest.json b/plugin-packages/openai-images/manifest.json index 16d42fe95..289314634 100644 --- a/plugin-packages/openai-images/manifest.json +++ b/plugin-packages/openai-images/manifest.json @@ -5,7 +5,7 @@ "version": "2.0.0", "author": "OpenAI / 影策", "description": "OpenAI Images 独立请求协议插件。", - "documentation": "# OpenAI Images\n\n该目录是独立官方协议插件源码。后端从生成的 `openai-images.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。\n\n完整接口见 [docs/interface.md](docs/interface.md)。\n\n---\n\n# OpenAI Images 接口字段\n\n## 协议身份\n\n- 插件 ID:`openai-images`。\n- Provider ID:`openai-image`。\n- 能力:`image`。\n- 默认 Base URL:`https://api.openai.com`。\n- 鉴权驱动:`bearer`。\n- 创建:`POST /v1/images/generations`。\n- 生命周期:同步响应。\n\n## 配置字段\n\n| 字段 | 类型 | 必填 | 含义 |\n| --- | --- | --- | --- |\n| `apiKey` | secret | 是 | API Key |\n\n## 统一字段映射\n\n| 统一字段 | 类型 | 必填 | 上游映射 | 说明 |\n| --- | --- | --- | --- | --- |\n| `model` | string | 是 | `model` | 图片模型 ID。 |\n| `prompt` | string | 是 | `prompt` | 图片提示词。 |\n| `images` | media[] | 否 | `provider image/reference fields` | 参考图或编辑源图,role 由业务层确定。 |\n| `imageCount` | integer | 否 | `n/sample_count` | 输出数量。 |\n| `aspectRatio` | string | 否 | `size/aspect_ratio` | 比例或尺寸,语义按协议说明。 |\n| `resolution` | string | 否 | `resolution/imageSize` | 分辨率档位。 |\n| `quality` | string | 否 | `quality` | 质量档位。 |\n| `providerOptions` | object | 否 | `provider-specific fields` | 插件命名空间内的厂商扩展字段。 |\n\n## 上游请求模板逐字段清单\n\n下表由插件请求模板生成,覆盖 body、query、headers 和 multipart 文件声明中的每个字段。\n\n| 上游位置 | 值或转换表达式 |\n| --- | --- |\n| `create.method` | `\"POST\"` |\n| `create.path` | `\"/v1/images/generations\"` |\n| `create.contentType` | `\"application/json\"` |\n| `create.body.model` | `{\"$ref\":\"request.model\"}` |\n| `create.body.prompt` | `{\"$ref\":\"request.prompt\"}` |\n| `create.body.images` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$gt\":[{\"$len\":{\"$ref\":\"request.images\"}},0]},\"then\":{\"$map\":{\"from\":{\"$filter\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.images\"}},\"as\":\"media\",\"where\":{\"$ne\":[{\"$ref\":\"media.role\"},\"mask\"]}}},\"as\":\"media\",\"in\":{\"image_url\":{\"$ref\":\"media.value\"}}}},\"else\":null}}}` |\n| `create.body.mask` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$gt\":[{\"$len\":{\"$filter\":{\"from\":{\"$ref\":\"request.images\"},\"as\":\"media\",\"where\":{\"$eq\":[{\"$ref\":\"media.role\"},\"mask\"]}}}},0]},\"then\":{\"image_url\":{\"$first\":{\"$map\":{\"from\":{\"$filter\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.images\"}},\"as\":\"media\",\"where\":{\"$eq\":[{\"$ref\":\"media.role\"},\"mask\"]}}},\"as\":\"media\",\"in\":{\"$ref\":\"media.value\"}}}}},\"else\":null}}}` |\n| `create.body.n` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$gt\":[{\"$ref\":\"request.imageCount\"},0]},\"then\":{\"$ref\":\"request.imageCount\"},\"else\":1}}}` |\n| `create.body.size` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.aspectRatio\"}}},[\"\",\"auto\"]]},\"then\":null,\"else\":{\"$ref\":\"request.aspectRatio\"}}}}` |\n| `create.body.quality` | `{\"$omitEmpty\":{\"$switch\":{\"cases\":[{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"1k\"]]},\"then\":\"low\"},{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"2k\"]]},\"then\":\"medium\"},{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"4k\"]]},\"then\":\"high\"},{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"\",\"auto\"]]},\"then\":null}],\"default\":{\"$ref\":\"request.quality\"}}}}` |\n| `create.body.background` | `{\"$omitEmpty\":{\"$coalesce\":[{\"$ref\":\"request.providerOptions.openai-image.background\"},{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"request.extra.transparentBackground\"},\"true\"]},\"then\":\"transparent\",\"else\":null}}]}}` |\n| `create.body.output_format` | `{\"$omitEmpty\":{\"$coalesce\":[{\"$ref\":\"request.providerOptions.openai-image.output_format\"},\"png\"]}}` |\n| `create.body.output_compression` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.output_compression\"}}` |\n| `create.body.moderation` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.moderation\"}}` |\n| `create.body.response_format` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.response_format\"}}` |\n| `create.body.style` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.style\"}}` |\n| `create.body.user` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.user\"}}` |\n\n## Provider 扩展键\n\n- `providerOptions.openai-image.background`\n- `providerOptions.openai-image.moderation`\n- `providerOptions.openai-image.output_compression`\n- `providerOptions.openai-image.output_format`\n- `providerOptions.openai-image.response_format`\n- `providerOptions.openai-image.style`\n- `providerOptions.openai-image.user`\n\n动态模型或工作流允许使用文档声明的完整 `parameters/input/extra_body` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。\n\n## 响应映射逐字段清单\n\n| 映射位置 | 上游路径或转换表达式 |\n| --- | --- |\n| `response.status` | `\"succeeded\"` |\n| `response.images` | `{\"$map\":{\"from\":{\"$ref\":\"response.data\"},\"as\":\"item\",\"in\":{\"url\":{\"$omitEmpty\":{\"$ref\":\"item.url\"}},\"dataUrl\":{\"$if\":{\"condition\":{\"$ref\":\"item.b64_json\"},\"then\":{\"$concat\":[\"data:image/png;base64,\",{\"$ref\":\"item.b64_json\"}]},\"else\":null}}}}}` |\n| `response.usage` | `{\"$ref\":\"response.usage\"}` |\n| `response.errorPaths[0]` | `\"error.code\"` |\n| `response.messagePaths[0]` | `\"error.message\"` |\n\n## 响应与错误\n\n插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。\n\n## 兼容边界\n\n无参考图走 JSON generations;有参考图或蒙版走 JSON edits,并按官方 images 数组传入多张 image_url。quality 的 1k/2k/4k 映射为 OpenAI low/medium/high。\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"openai-images\",\n \"name\": \"OpenAI Images\",\n \"version\": \"2.0.0\",\n \"author\": \"OpenAI / 影策\",\n \"description\": \"OpenAI Images 独立请求协议插件。\",\n \"documentation\": \"<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>\",\n \"permissions\": [\n \"generation.run\",\n \"media.read\"\n ],\n \"configuration\": {\n \"fields\": [\n {\n \"name\": \"apiKey\",\n \"type\": \"secret\",\n \"label\": \"API Key\",\n \"required\": true\n }\n ]\n },\n \"contributes\": {\n \"providers\": [\n {\n \"id\": \"openai-image\",\n \"label\": \"OpenAI Images\",\n \"capabilities\": [\n \"image\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://api.openai.com\",\n \"requiresPublicMediaUrls\": true,\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"图片模型 ID。\"\n },\n {\n \"name\": \"prompt\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"prompt\",\n \"description\": \"图片提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"provider image/reference fields\",\n \"description\": \"参考图或编辑源图,role 由业务层确定。\"\n },\n {\n \"name\": \"imageCount\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"n/sample_count\",\n \"description\": \"输出数量。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"size/aspect_ratio\",\n \"description\": \"比例或尺寸,语义按协议说明。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution/imageSize\",\n \"description\": \"分辨率档位。\"\n },\n {\n \"name\": \"quality\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"quality\",\n \"description\": \"质量档位。\"\n },\n {\n \"name\": \"providerOptions\",\n \"type\": \"object\",\n \"required\": false,\n \"mapping\": \"provider-specific fields\",\n \"description\": \"插件命名空间内的厂商扩展字段。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/v1/images/generations\",\n \"pathTemplate\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 0\n ]\n },\n \"then\": \"/v1/images/edits\",\n \"else\": \"/v1/images/generations\"\n }\n },\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"images\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 0\n ]\n },\n \"then\": {\n \"$map\": {\n \"from\": {\n \"$filter\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"where\": {\n \"$ne\": [\n {\n \"$ref\": \"media.role\"\n },\n \"mask\"\n ]\n }\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"image_url\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"else\": null\n }\n }\n },\n \"mask\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$filter\": {\n \"from\": {\n \"$ref\": \"request.images\"\n },\n \"as\": \"media\",\n \"where\": {\n \"$eq\": [\n {\n \"$ref\": \"media.role\"\n },\n \"mask\"\n ]\n }\n }\n }\n },\n 0\n ]\n },\n \"then\": {\n \"image_url\": {\n \"$first\": {\n \"$map\": {\n \"from\": {\n \"$filter\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"where\": {\n \"$eq\": [\n {\n \"$ref\": \"media.role\"\n },\n \"mask\"\n ]\n }\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n },\n \"else\": null\n }\n }\n },\n \"n\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$ref\": \"request.imageCount\"\n },\n 0\n ]\n },\n \"then\": {\n \"$ref\": \"request.imageCount\"\n },\n \"else\": 1\n }\n }\n },\n \"size\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.aspectRatio\"\n }\n }\n },\n [\n \"\",\n \"auto\"\n ]\n ]\n },\n \"then\": null,\n \"else\": {\n \"$ref\": \"request.aspectRatio\"\n }\n }\n }\n },\n \"quality\": {\n \"$omitEmpty\": {\n \"$switch\": {\n \"cases\": [\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"1k\"\n ]\n ]\n },\n \"then\": \"low\"\n },\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"2k\"\n ]\n ]\n },\n \"then\": \"medium\"\n },\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"4k\"\n ]\n ]\n },\n \"then\": \"high\"\n },\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"\",\n \"auto\"\n ]\n ]\n },\n \"then\": null\n }\n ],\n \"default\": {\n \"$ref\": \"request.quality\"\n }\n }\n }\n },\n \"background\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.openai-image.background\"\n },\n {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"request.extra.transparentBackground\"\n },\n \"true\"\n ]\n },\n \"then\": \"transparent\",\n \"else\": null\n }\n }\n ]\n }\n },\n \"output_format\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.openai-image.output_format\"\n },\n \"png\"\n ]\n }\n },\n \"output_compression\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.output_compression\"\n }\n },\n \"moderation\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.moderation\"\n }\n },\n \"response_format\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.response_format\"\n }\n },\n \"style\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.style\"\n }\n },\n \"user\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.user\"\n }\n }\n }\n },\n \"response\": {\n \"status\": \"succeeded\",\n \"images\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"response.data\"\n },\n \"as\": \"item\",\n \"in\": {\n \"url\": {\n \"$omitEmpty\": {\n \"$ref\": \"item.url\"\n }\n },\n \"dataUrl\": {\n \"$if\": {\n \"condition\": {\n \"$ref\": \"item.b64_json\"\n },\n \"then\": {\n \"$concat\": [\n \"data:image/png;base64,\",\n {\n \"$ref\": \"item.b64_json\"\n }\n ]\n },\n \"else\": null\n }\n }\n }\n }\n },\n \"usage\": {\n \"$ref\": \"response.usage\"\n },\n \"errorPaths\": [\n \"error.code\"\n ],\n \"messagePaths\": [\n \"error.message\"\n ]\n }\n }\n ]\n }\n}\n```\n\n", + "documentation": "# OpenAI Images\n\n该目录是独立官方协议插件源码。后端从生成的 `openai-images.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。\n\n完整接口见 [docs/interface.md](docs/interface.md)。\n\n---\n\n# OpenAI Images 接口字段\n\n## 协议身份\n\n- 插件 ID:`openai-images`。\n- Provider ID:`openai-image`。\n- 能力:`image`。\n- 默认 Base URL:`https://api.openai.com`。\n- 鉴权驱动:`bearer`。\n- 创建:`POST /v1/images/generations`。\n- 生命周期:同步响应。\n\n## 配置字段\n\n| 字段 | 类型 | 必填 | 含义 |\n| --- | --- | --- | --- |\n| `apiKey` | secret | 是 | API Key |\n\n## 统一字段映射\n\n| 统一字段 | 类型 | 必填 | 上游映射 | 说明 |\n| --- | --- | --- | --- | --- |\n| `model` | string | 是 | `model` | 图片模型 ID。 |\n| `prompt` | string | 是 | `prompt` | 图片提示词。 |\n| `images` | media[] | 否 | `provider image/reference fields` | 参考图或编辑源图,role 由业务层确定。 |\n| `imageCount` | integer | 否 | `n/sample_count` | 输出数量。 |\n| `aspectRatio` | string | 否 | `size/aspect_ratio` | 比例或尺寸,语义按协议说明。 |\n| `resolution` | string | 否 | `resolution/imageSize` | 分辨率档位。 |\n| `quality` | string | 否 | `quality` | 质量档位。 |\n| `providerOptions` | object | 否 | `provider-specific fields` | 插件命名空间内的厂商扩展字段。 |\n\n## 上游请求模板逐字段清单\n\n下表由插件请求模板生成,覆盖 body、query、headers 和 multipart 文件声明中的每个字段。\n\n| 上游位置 | 值或转换表达式 |\n| --- | --- |\n| `create.method` | `\"POST\"` |\n| `create.path` | `\"/v1/images/generations\"` |\n| `create.contentType` | `\"application/json\"` |\n| `create.body.model` | `{\"$ref\":\"request.model\"}` |\n| `create.body.prompt` | `{\"$ref\":\"request.prompt\"}` |\n| `create.body.n` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$gt\":[{\"$ref\":\"request.imageCount\"},0]},\"then\":{\"$ref\":\"request.imageCount\"},\"else\":1}}}` |\n| `create.body.size` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.aspectRatio\"}}},[\"\",\"auto\"]]},\"then\":null,\"else\":{\"$ref\":\"request.aspectRatio\"}}}}` |\n| `create.body.quality` | `{\"$omitEmpty\":{\"$switch\":{\"cases\":[{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"1k\"]]},\"then\":\"low\"},{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"2k\"]]},\"then\":\"medium\"},{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"4k\"]]},\"then\":\"high\"},{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"\",\"auto\"]]},\"then\":null}],\"default\":{\"$ref\":\"request.quality\"}}}}` |\n| `create.body.background` | `{\"$omitEmpty\":{\"$coalesce\":[{\"$ref\":\"request.providerOptions.openai-image.background\"},{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"request.extra.transparentBackground\"},\"true\"]},\"then\":\"transparent\",\"else\":null}}]}}` |\n| `create.body.output_format` | `{\"$omitEmpty\":{\"$coalesce\":[{\"$ref\":\"request.providerOptions.openai-image.output_format\"},\"png\"]}}` |\n| `create.body.output_compression` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.output_compression\"}}` |\n| `create.body.moderation` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.moderation\"}}` |\n| `create.body.response_format` | `{\"$omitEmpty\":{\"$coalesce\":[{\"$ref\":\"request.providerOptions.openai-image.response_format\"},\"b64_json\"]}}` |\n| `create.body.style` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.style\"}}` |\n| `create.body.user` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.user\"}}` |\n| `create.files[0].name` | `\"image\"` |\n| `create.files[0].source` | `{\"$filter\":{\"from\":{\"$ref\":\"request.images\"},\"as\":\"media\",\"where\":{\"$ne\":[{\"$ref\":\"media.role\"},\"mask\"]}}}` |\n| `create.files[0].filename` | `\"source.png\"` |\n| `create.files[1].name` | `\"mask\"` |\n| `create.files[1].source` | `{\"$filter\":{\"from\":{\"$ref\":\"request.images\"},\"as\":\"media\",\"where\":{\"$eq\":[{\"$ref\":\"media.role\"},\"mask\"]}}}` |\n| `create.files[1].filename` | `\"mask.png\"` |\n\n## Provider 扩展键\n\n- `providerOptions.openai-image.background`\n- `providerOptions.openai-image.moderation`\n- `providerOptions.openai-image.output_compression`\n- `providerOptions.openai-image.output_format`\n- `providerOptions.openai-image.response_format`\n- `providerOptions.openai-image.style`\n- `providerOptions.openai-image.user`\n\n动态模型或工作流允许使用文档声明的完整 `parameters/input/extra_body` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。\n\n## 响应映射逐字段清单\n\n| 映射位置 | 上游路径或转换表达式 |\n| --- | --- |\n| `response.status` | `\"succeeded\"` |\n| `response.images` | `{\"$map\":{\"from\":{\"$ref\":\"response.data\"},\"as\":\"item\",\"in\":{\"url\":{\"$omitEmpty\":{\"$ref\":\"item.url\"}},\"dataUrl\":{\"$if\":{\"condition\":{\"$ref\":\"item.b64_json\"},\"then\":{\"$concat\":[\"data:image/png;base64,\",{\"$ref\":\"item.b64_json\"}]},\"else\":null}}}}}` |\n| `response.usage` | `{\"$ref\":\"response.usage\"}` |\n| `response.errorPaths[0]` | `\"error.code\"` |\n| `response.messagePaths[0]` | `\"error.message\"` |\n\n## 响应与错误\n\n插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。\n\n## 兼容边界\n\n无参考图走 JSON generations;有参考图或蒙版走 multipart edits。quality 的 1k/2k/4k 映射为 OpenAI low/medium/high。\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"openai-images\",\n \"name\": \"OpenAI Images\",\n \"version\": \"2.0.0\",\n \"author\": \"OpenAI / 影策\",\n \"description\": \"OpenAI Images 独立请求协议插件。\",\n \"documentation\": \"<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>\",\n \"permissions\": [\n \"generation.run\",\n \"media.read\"\n ],\n \"configuration\": {\n \"fields\": [\n {\n \"name\": \"apiKey\",\n \"type\": \"secret\",\n \"label\": \"API Key\",\n \"required\": true\n }\n ]\n },\n \"contributes\": {\n \"providers\": [\n {\n \"id\": \"openai-image\",\n \"label\": \"OpenAI Images\",\n \"capabilities\": [\n \"image\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://api.openai.com\",\n \"requiresPublicMediaUrls\": false,\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"图片模型 ID。\"\n },\n {\n \"name\": \"prompt\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"prompt\",\n \"description\": \"图片提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"provider image/reference fields\",\n \"description\": \"参考图或编辑源图,role 由业务层确定。\"\n },\n {\n \"name\": \"imageCount\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"n/sample_count\",\n \"description\": \"输出数量。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"size/aspect_ratio\",\n \"description\": \"比例或尺寸,语义按协议说明。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution/imageSize\",\n \"description\": \"分辨率档位。\"\n },\n {\n \"name\": \"quality\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"quality\",\n \"description\": \"质量档位。\"\n },\n {\n \"name\": \"providerOptions\",\n \"type\": \"object\",\n \"required\": false,\n \"mapping\": \"provider-specific fields\",\n \"description\": \"插件命名空间内的厂商扩展字段。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/v1/images/generations\",\n \"pathTemplate\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 0\n ]\n },\n \"then\": \"/v1/images/edits\",\n \"else\": \"/v1/images/generations\"\n }\n },\n \"contentType\": \"application/json\",\n \"contentTypeTemplate\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 0\n ]\n },\n \"then\": \"multipart/form-data\",\n \"else\": \"application/json\"\n }\n },\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"n\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$ref\": \"request.imageCount\"\n },\n 0\n ]\n },\n \"then\": {\n \"$ref\": \"request.imageCount\"\n },\n \"else\": 1\n }\n }\n },\n \"size\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.aspectRatio\"\n }\n }\n },\n [\n \"\",\n \"auto\"\n ]\n ]\n },\n \"then\": null,\n \"else\": {\n \"$ref\": \"request.aspectRatio\"\n }\n }\n }\n },\n \"quality\": {\n \"$omitEmpty\": {\n \"$switch\": {\n \"cases\": [\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"1k\"\n ]\n ]\n },\n \"then\": \"low\"\n },\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"2k\"\n ]\n ]\n },\n \"then\": \"medium\"\n },\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"4k\"\n ]\n ]\n },\n \"then\": \"high\"\n },\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"\",\n \"auto\"\n ]\n ]\n },\n \"then\": null\n }\n ],\n \"default\": {\n \"$ref\": \"request.quality\"\n }\n }\n }\n },\n \"background\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.openai-image.background\"\n },\n {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"request.extra.transparentBackground\"\n },\n \"true\"\n ]\n },\n \"then\": \"transparent\",\n \"else\": null\n }\n }\n ]\n }\n },\n \"output_format\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.openai-image.output_format\"\n },\n \"png\"\n ]\n }\n },\n \"output_compression\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.output_compression\"\n }\n },\n \"moderation\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.moderation\"\n }\n },\n \"response_format\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.openai-image.response_format\"\n },\n \"b64_json\"\n ]\n }\n },\n \"style\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.style\"\n }\n },\n \"user\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.user\"\n }\n }\n },\n \"files\": [\n {\n \"name\": \"image\",\n \"source\": {\n \"$filter\": {\n \"from\": {\n \"$ref\": \"request.images\"\n },\n \"as\": \"media\",\n \"where\": {\n \"$ne\": [\n {\n \"$ref\": \"media.role\"\n },\n \"mask\"\n ]\n }\n }\n },\n \"filename\": \"source.png\"\n },\n {\n \"name\": \"mask\",\n \"source\": {\n \"$filter\": {\n \"from\": {\n \"$ref\": \"request.images\"\n },\n \"as\": \"media\",\n \"where\": {\n \"$eq\": [\n {\n \"$ref\": \"media.role\"\n },\n \"mask\"\n ]\n }\n }\n },\n \"filename\": \"mask.png\"\n }\n ]\n },\n \"response\": {\n \"status\": \"succeeded\",\n \"images\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"response.data\"\n },\n \"as\": \"item\",\n \"in\": {\n \"url\": {\n \"$omitEmpty\": {\n \"$ref\": \"item.url\"\n }\n },\n \"dataUrl\": {\n \"$if\": {\n \"condition\": {\n \"$ref\": \"item.b64_json\"\n },\n \"then\": {\n \"$concat\": [\n \"data:image/png;base64,\",\n {\n \"$ref\": \"item.b64_json\"\n }\n ]\n },\n \"else\": null\n }\n }\n }\n }\n },\n \"usage\": {\n \"$ref\": \"response.usage\"\n },\n \"errorPaths\": [\n \"error.code\"\n ],\n \"messagePaths\": [\n \"error.message\"\n ]\n }\n }\n ]\n }\n}\n```\n\n", "permissions": [ "generation.run", "media.read" @@ -36,7 +36,7 @@ "agent" ], "baseUrl": "https://api.openai.com", - "requiresPublicMediaUrls": true, + "requiresPublicMediaUrls": false, "auth": { "type": "bearer", "field": "apiKey" @@ -119,6 +119,22 @@ } }, "contentType": "application/json", + "contentTypeTemplate": { + "$if": { + "condition": { + "$gt": [ + { + "$len": { + "$ref": "request.images" + } + }, + 0 + ] + }, + "then": "multipart/form-data", + "else": "application/json" + } + }, "body": { "model": { "$ref": "request.model" @@ -126,111 +142,6 @@ "prompt": { "$ref": "request.prompt" }, - "images": { - "$omitEmpty": { - "$if": { - "condition": { - "$gt": [ - { - "$len": { - "$ref": "request.images" - } - }, - 0 - ] - }, - "then": { - "$map": { - "from": { - "$filter": { - "from": { - "$sortByOrder": { - "$ref": "request.images" - } - }, - "as": "media", - "where": { - "$ne": [ - { - "$ref": "media.role" - }, - "mask" - ] - } - } - }, - "as": "media", - "in": { - "image_url": { - "$ref": "media.value" - } - } - } - }, - "else": null - } - } - }, - "mask": { - "$omitEmpty": { - "$if": { - "condition": { - "$gt": [ - { - "$len": { - "$filter": { - "from": { - "$ref": "request.images" - }, - "as": "media", - "where": { - "$eq": [ - { - "$ref": "media.role" - }, - "mask" - ] - } - } - } - }, - 0 - ] - }, - "then": { - "image_url": { - "$first": { - "$map": { - "from": { - "$filter": { - "from": { - "$sortByOrder": { - "$ref": "request.images" - } - }, - "as": "media", - "where": { - "$eq": [ - { - "$ref": "media.role" - }, - "mask" - ] - } - } - }, - "as": "media", - "in": { - "$ref": "media.value" - } - } - } - } - }, - "else": null - } - } - }, "n": { "$omitEmpty": { "$if": { @@ -399,7 +310,12 @@ }, "response_format": { "$omitEmpty": { - "$ref": "request.providerOptions.openai-image.response_format" + "$coalesce": [ + { + "$ref": "request.providerOptions.openai-image.response_format" + }, + "b64_json" + ] } }, "style": { @@ -412,7 +328,49 @@ "$ref": "request.providerOptions.openai-image.user" } } - } + }, + "files": [ + { + "name": "image", + "source": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$ne": [ + { + "$ref": "media.role" + }, + "mask" + ] + } + } + }, + "filename": "source.png" + }, + { + "name": "mask", + "source": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$eq": [ + { + "$ref": "media.role" + }, + "mask" + ] + } + } + }, + "filename": "mask.png" + } + ] }, "response": { "status": "succeeded", diff --git a/plugin-packages/src-a6api/README.md b/plugin-packages/src-a6api/README.md new file mode 100644 index 000000000..7bb76ee60 --- /dev/null +++ b/plugin-packages/src-a6api/README.md @@ -0,0 +1,20 @@ +# A6api + +A6api 模型网关协议插件。统一入口 `https://api.a6api.com`,OpenAI 兼容(`Authorization: Bearer `)。 + +## Providers + +| Provider | 能力 | 端点 | +|---|---|---| +| a6api-chat | text | POST /chat/completions | +| a6api-image | image | POST /images/generations、POST /images/edits | + +## 配置 + +在渠道中填写 API Key(A6api 控制台创建,形如 `sk-xxxxxxxx`),baseUrl 使用 `https://api.a6api.com`。 + +## 说明 + +- 模型名以 A6api 模型市场当前列表为准。 +- 图像编辑(带参考图)走 `/images/edits` multipart 请求。 +- 错误体为 OpenAI 风格,携带 `request_id`。 \ No newline at end of file diff --git a/plugin-packages/src-a6api/docs/interface.md b/plugin-packages/src-a6api/docs/interface.md new file mode 100644 index 000000000..48bade060 --- /dev/null +++ b/plugin-packages/src-a6api/docs/interface.md @@ -0,0 +1,29 @@ +# A6api 接口说明 + +## 鉴权 + +`Authorization: Bearer sk-xxxxxxxxxxxxxxxxxxxxxxxx` + +## 文本对话 + +POST `https://api.a6api.com/chat/completions` + +请求体 OpenAI Chat Completions 格式:`model`、`messages`、`temperature`、`max_tokens`、`tools`、`tool_choice`、`response_format`、`stream` 等。 + +响应 `choices[0].message.content` 为正文,`choices[0].message.reasoning_content` 为推理内容,`usage` 为用量。 + +## 图像生成 + +POST `https://api.a6api.com/images/generations` + +请求体:`model`、`prompt`、`n`、`size`、`quality`、`response_format`。 + +响应 `data[].url` 或 `data[].b64_json`。 + +## 图像编辑 + +POST `https://api.a6api.com/images/edits`(multipart,带参考图时使用) + +## 模型列表 + +GET `https://api.a6api.com/models` \ No newline at end of file diff --git a/plugin-packages/src-a6api/manifest.json b/plugin-packages/src-a6api/manifest.json new file mode 100644 index 000000000..8bb12ca02 --- /dev/null +++ b/plugin-packages/src-a6api/manifest.json @@ -0,0 +1,143 @@ +{ + "apiVersion": "yingce.plugin/v2", + "id": "a6api", + "name": "A6api", + "version": "1.0.0", + "author": "A6api", + "description": "A6api 模型网关协议插件:OpenAI 兼容对话与图像生成。", + "permissions": ["generation.run", "media.read"], + "configuration": { + "fields": [ + { "name": "apiKey", "type": "secret", "label": "API Key", "required": true } + ] + }, + "contributes": { + "providers": [ + { + "id": "a6api-chat", + "label": "A6api Chat", + "capabilities": ["text"], + "scopes": ["admin.system-channel", "user.custom-channel", "canvas", "creation", "agent"], + "baseUrl": "https://api.a6api.com", + "auth": { "type": "bearer", "field": "apiKey" }, + "parameters": [ + { "name": "model", "type": "string", "required": true, "mapping": "model", "description": "A6api 模型市场中的模型 ID。" }, + { "name": "messages", "type": "message[]", "required": true, "mapping": "provider message container", "description": "包含历史消息和当前用户输入。" }, + { "name": "instructions", "type": "string", "required": false, "mapping": "system/instructions", "description": "系统指令。" }, + { "name": "temperature", "type": "number", "required": false, "mapping": "temperature", "description": "采样温度。" }, + { "name": "top_p", "type": "number", "required": false, "mapping": "top_p", "description": "核采样参数。" }, + { "name": "max_tokens", "type": "integer", "required": false, "mapping": "max_tokens/max_output_tokens", "description": "最大输出 token。" }, + { "name": "tools", "type": "array", "required": false, "mapping": "tools/toolConfig", "description": "工具定义。" }, + { "name": "tool_choice", "type": "object|string", "required": false, "mapping": "tool_choice", "description": "工具选择策略。" }, + { "name": "response_format", "type": "object", "required": false, "mapping": "response_format/text", "description": "结构化输出配置。" }, + { "name": "stream", "type": "boolean", "required": false, "mapping": "stream", "description": "流式开关;后台任务当前以最终响应归一。" }, + { "name": "providerOptions", "type": "object", "required": false, "mapping": "provider-specific fields", "description": "插件命名空间内的厂商扩展字段。" } + ], + "create": { + "method": "POST", + "path": "/chat/completions", + "contentType": "application/json", + "body": { + "model": { "$ref": "request.model" }, + "messages": { "$ref": "request.messages" }, + "temperature": { "$omitEmpty": { "$ref": "request.providerOptions.a6api-chat.temperature" } }, + "top_p": { "$omitEmpty": { "$ref": "request.providerOptions.a6api-chat.top_p" } }, + "max_tokens": { "$omitEmpty": { "$coalesce": [ { "$ref": "request.extra.max_tokens" }, { "$ref": "request.providerOptions.a6api-chat.max_tokens" } ] } }, + "tools": { "$omitEmpty": { "$ref": "request.providerOptions.a6api-chat.tools" } }, + "tool_choice": { "$omitEmpty": { "$ref": "request.providerOptions.a6api-chat.tool_choice" } }, + "response_format": { "$omitEmpty": { "$ref": "request.providerOptions.a6api-chat.response_format" } }, + "stream": { "$omitEmpty": { "$ref": "request.providerOptions.a6api-chat.stream" } }, + "stop": { "$omitEmpty": { "$ref": "request.providerOptions.a6api-chat.stop" } }, + "seed": { "$omitEmpty": { "$ref": "request.providerOptions.a6api-chat.seed" } }, + "frequency_penalty": { "$omitEmpty": { "$ref": "request.providerOptions.a6api-chat.frequency_penalty" } }, + "presence_penalty": { "$omitEmpty": { "$ref": "request.providerOptions.a6api-chat.presence_penalty" } }, + "user": { "$omitEmpty": { "$ref": "request.providerOptions.a6api-chat.user" } } + } + }, + "response": { + "status": "succeeded", + "textPaths": ["choices.0.message.content", "choices.0.text"], + "reasoningPaths": ["choices.0.message.reasoning_content"], + "usage": { "$ref": "response.usage" }, + "errorPaths": ["error.code"], + "messagePaths": ["error.message"] + } + }, + { + "id": "a6api-image", + "label": "A6api Image", + "capabilities": ["image"], + "scopes": ["admin.system-channel", "user.custom-channel", "canvas", "creation", "agent"], + "baseUrl": "https://api.a6api.com", + "auth": { "type": "bearer", "field": "apiKey" }, + "parameters": [ + { "name": "model", "type": "string", "required": true, "mapping": "model", "description": "A6api 模型市场中的图像模型 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": "/images/generations", + "pathTemplate": { + "$if": { + "condition": { "$gt": [ { "$len": { "$ref": "request.images" } }, 0 ] }, + "then": "/images/edits", + "else": "/images/generations" + } + }, + "contentType": "application/json", + "contentTypeTemplate": { + "$if": { + "condition": { "$gt": [ { "$len": { "$ref": "request.images" } }, 0 ] }, + "then": "multipart/form-data", + "else": "application/json" + } + }, + "body": { + "model": { "$ref": "request.model" }, + "prompt": { "$ref": "request.prompt" }, + "n": { "$omitEmpty": { "$if": { "condition": { "$gt": [ { "$ref": "request.imageCount" }, 0 ] }, "then": { "$ref": "request.imageCount" }, "else": 1 } } }, + "size": { "$omitEmpty": { "$ref": "request.aspectRatio" } }, + "quality": { "$omitEmpty": { "$ref": "request.quality" } }, + "output_format": { "$omitEmpty": { "$coalesce": [ { "$ref": "request.providerOptions.a6api-image.output_format" }, "png" ] } }, + "response_format": { "$omitEmpty": { "$coalesce": [ { "$ref": "request.providerOptions.a6api-image.response_format" }, "b64_json" ] } }, + "user": { "$omitEmpty": { "$ref": "request.providerOptions.a6api-image.user" } } + }, + "files": [ + { + "name": "image", + "source": { "$filter": { "from": { "$ref": "request.images" }, "as": "media", "where": { "$ne": [ { "$ref": "media.role" }, "mask" ] } } }, + "filename": "source.png" + }, + { + "name": "mask", + "source": { "$filter": { "from": { "$ref": "request.images" }, "as": "media", "where": { "$eq": [ { "$ref": "media.role" }, "mask" ] } } }, + "filename": "mask.png" + } + ] + }, + "response": { + "status": "succeeded", + "images": { + "$map": { + "from": { "$ref": "response.data" }, + "as": "item", + "in": { + "url": { "$omitEmpty": { "$ref": "item.url" } }, + "dataUrl": { "$if": { "condition": { "$ref": "item.b64_json" }, "then": { "$concat": [ "data:image/png;base64,", { "$ref": "item.b64_json" } ] }, "else": null } } + } + } + }, + "usage": { "$ref": "response.usage" }, + "errorPaths": ["error.code"], + "messagePaths": ["error.message"] + } + } + ] + } +} \ No newline at end of file diff --git a/plugin-packages/src-antigravity-proxy/README.md b/plugin-packages/src-antigravity-proxy/README.md new file mode 100644 index 000000000..81962c2b8 --- /dev/null +++ b/plugin-packages/src-antigravity-proxy/README.md @@ -0,0 +1,41 @@ +# Antigravity Proxy + +Antigravity 中转渠道插件:OpenAI Chat Completions 兼容对话协议。 + +## 协议身份 + +- 插件 ID:`antigravity-proxy` +- Provider ID:`antigravity-chat` +- 能力:`text` +- 默认 Base URL:`http://103.242.14.110:8317` +- 鉴权驱动:`bearer` +- 创建:`POST /chat/completions` +- 生命周期:同步响应 + +## 配置字段 + +| 字段 | 类型 | 必填 | 含义 | +| --- | --- | --- | --- | +| `apiKey` | secret | 是 | 中转渠道 API Key | + +## 模型 + +- `gemini-3.8-flash-high` +- `gemini-pro-agent` + +## 统一字段映射 + +| 统一字段 | 类型 | 必填 | 上游映射 | +| --- | --- | --- | --- | +| `model` | string | 是 | `model` | +| `messages` | message[] | 是 | `messages` | +| `instructions` | string | 否 | `system/instructions` | +| `temperature` | number | 否 | `temperature` | +| `top_p` | number | 否 | `top_p` | +| `max_tokens` | integer | 否 | `max_tokens` | +| `tools` | array | 否 | `tools` | +| `tool_choice` | object\|string | 否 | `tool_choice` | +| `response_format` | object | 否 | `response_format` | +| `stream` | boolean | 否 | `stream` | + +完整接口见 [docs/interface.md](docs/interface.md)。 diff --git a/plugin-packages/src-antigravity-proxy/docs/interface.md b/plugin-packages/src-antigravity-proxy/docs/interface.md new file mode 100644 index 000000000..5a19a4f61 --- /dev/null +++ b/plugin-packages/src-antigravity-proxy/docs/interface.md @@ -0,0 +1,70 @@ +# Antigravity Proxy 接口字段 + +## 协议身份 + +- 插件 ID:`antigravity-proxy` +- Provider ID:`antigravity-chat` +- 能力:`text` +- 默认 Base URL:`http://103.242.14.110:8317` +- 鉴权驱动:`bearer` +- 创建:`POST /chat/completions` +- 生命周期:同步响应 + +## 配置字段 + +| 字段 | 类型 | 必填 | 含义 | +| --- | --- | --- | --- | +| `apiKey` | secret | 是 | 中转渠道 API Key | + +## 统一字段映射 + +| 统一字段 | 类型 | 必填 | 上游映射 | 说明 | +| --- | --- | --- | --- | --- | +| `model` | string | 是 | `model` | 上游模型 ID。 | +| `messages` | message[] | 是 | `messages` | 包含历史消息和当前用户输入。 | +| `instructions` | string | 否 | `system/instructions` | 系统指令。 | +| `temperature` | number | 否 | `temperature` | 采样温度。 | +| `top_p` | number | 否 | `top_p` | 核采样参数。 | +| `max_tokens` | integer | 否 | `max_tokens/max_output_tokens` | 最大输出 token。 | +| `tools` | array | 否 | `tools/toolConfig` | 工具定义。 | +| `tool_choice` | object\|string | 否 | `tool_choice` | 工具选择策略。 | +| `response_format` | object | 否 | `response_format/text` | 结构化输出配置。 | +| `stream` | boolean | 否 | `stream` | 流式开关。 | + +## 上游请求模板 + +| 上游位置 | 值或转换表达式 | +| --- | --- | +| `create.method` | `"POST"` | +| `create.path` | `"/chat/completions"` | +| `create.contentType` | `"application/json"` | +| `create.body.model` | `{"$ref":"request.model"}` | +| `create.body.messages` | `{"$ref":"request.messages"}` | +| `create.body.temperature` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.temperature"}}` | +| `create.body.top_p` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.top_p"}}` | +| `create.body.max_tokens` | `{"$omitEmpty":{"$coalesce":[{"$ref":"request.extra.max_tokens"},{"$ref":"request.providerOptions.antigravity-chat.max_tokens"}]}}` | +| `create.body.tools` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.tools"}}` | +| `create.body.tool_choice` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.tool_choice"}}` | +| `create.body.response_format` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.response_format"}}` | +| `create.body.stream` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.stream"}}` | +| `create.body.stop` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.stop"}}` | +| `create.body.seed` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.seed"}}` | +| `create.body.frequency_penalty` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.frequency_penalty"}}` | +| `create.body.presence_penalty` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.presence_penalty"}}` | +| `create.body.user` | `{"$omitEmpty":{"$ref":"request.providerOptions.antigravity-chat.user"}}` | + +## 响应映射 + +| 映射位置 | 上游路径或转换表达式 | +| --- | --- | +| `response.status` | `"succeeded"` | +| `response.textPaths[0]` | `"choices.0.message.content"` | +| `response.textPaths[1]` | `"choices.0.text"` | +| `response.reasoningPaths[0]` | `"choices.0.message.reasoning_content"` | +| `response.usage` | `{"$ref":"response.usage"}` | +| `response.errorPaths[0]` | `"error.code"` | +| `response.messagePaths[0]` | `"error.message"` | + +## 兼容边界 + +该包只代表 Antigravity 中转渠道的 OpenAI Chat Completions 线协议;其他端点或网关包装必须使用独立插件。 diff --git a/plugin-packages/src-antigravity-proxy/manifest.json b/plugin-packages/src-antigravity-proxy/manifest.json new file mode 100644 index 000000000..290fd46b2 --- /dev/null +++ b/plugin-packages/src-antigravity-proxy/manifest.json @@ -0,0 +1,68 @@ +{ + "apiVersion": "yingce.plugin/v2", + "id": "antigravity-proxy", + "name": "Antigravity Proxy", + "version": "1.0.0", + "author": "Yingce", + "description": "Antigravity 中转渠道插件:OpenAI 兼容对话(gemini-3.8-flash-high / gemini-pro-agent)。", + "permissions": ["generation.run", "media.read"], + "configuration": { + "fields": [ + { "name": "apiKey", "type": "secret", "label": "API Key", "required": true } + ] + }, + "contributes": { + "providers": [ + { + "id": "antigravity-chat", + "label": "Antigravity Chat", + "capabilities": ["text"], + "scopes": ["admin.system-channel", "user.custom-channel", "canvas", "creation", "agent"], + "baseUrl": "http://103.242.14.110:8317", + "auth": { "type": "bearer", "field": "apiKey" }, + "parameters": [ + { "name": "model", "type": "string", "required": true, "mapping": "model", "description": "模型 ID(gemini-3.8-flash-high / gemini-pro-agent)。" }, + { "name": "messages", "type": "message[]", "required": true, "mapping": "provider message container", "description": "包含历史消息和当前用户输入。" }, + { "name": "instructions", "type": "string", "required": false, "mapping": "system/instructions", "description": "系统指令。" }, + { "name": "temperature", "type": "number", "required": false, "mapping": "temperature", "description": "采样温度。" }, + { "name": "top_p", "type": "number", "required": false, "mapping": "top_p", "description": "核采样参数。" }, + { "name": "max_tokens", "type": "integer", "required": false, "mapping": "max_tokens/max_output_tokens", "description": "最大输出 token。" }, + { "name": "tools", "type": "array", "required": false, "mapping": "tools/toolConfig", "description": "工具定义。" }, + { "name": "tool_choice", "type": "object|string", "required": false, "mapping": "tool_choice", "description": "工具选择策略。" }, + { "name": "response_format", "type": "object", "required": false, "mapping": "response_format/text", "description": "结构化输出配置。" }, + { "name": "stream", "type": "boolean", "required": false, "mapping": "stream", "description": "流式开关;后台任务当前以最终响应归一。" }, + { "name": "providerOptions", "type": "object", "required": false, "mapping": "provider-specific fields", "description": "插件命名空间内的厂商扩展字段。" } + ], + "create": { + "method": "POST", + "path": "/chat/completions", + "contentType": "application/json", + "body": { + "model": { "$ref": "request.model" }, + "messages": { "$ref": "request.messages" }, + "temperature": { "$omitEmpty": { "$ref": "request.providerOptions.antigravity-chat.temperature" } }, + "top_p": { "$omitEmpty": { "$ref": "request.providerOptions.antigravity-chat.top_p" } }, + "max_tokens": { "$omitEmpty": { "$coalesce": [ { "$ref": "request.extra.max_tokens" }, { "$ref": "request.providerOptions.antigravity-chat.max_tokens" } ] } }, + "tools": { "$omitEmpty": { "$ref": "request.providerOptions.antigravity-chat.tools" } }, + "tool_choice": { "$omitEmpty": { "$ref": "request.providerOptions.antigravity-chat.tool_choice" } }, + "response_format": { "$omitEmpty": { "$ref": "request.providerOptions.antigravity-chat.response_format" } }, + "stream": { "$omitEmpty": { "$ref": "request.providerOptions.antigravity-chat.stream" } }, + "stop": { "$omitEmpty": { "$ref": "request.providerOptions.antigravity-chat.stop" } }, + "seed": { "$omitEmpty": { "$ref": "request.providerOptions.antigravity-chat.seed" } }, + "frequency_penalty": { "$omitEmpty": { "$ref": "request.providerOptions.antigravity-chat.frequency_penalty" } }, + "presence_penalty": { "$omitEmpty": { "$ref": "request.providerOptions.antigravity-chat.presence_penalty" } }, + "user": { "$omitEmpty": { "$ref": "request.providerOptions.antigravity-chat.user" } } + } + }, + "response": { + "status": "succeeded", + "textPaths": ["choices.0.message.content", "choices.0.text"], + "reasoningPaths": ["choices.0.message.reasoning_content"], + "usage": { "$ref": "response.usage" }, + "errorPaths": ["error.code"], + "messagePaths": ["error.message"] + } + } + ] + } +} diff --git a/plugin-packages/src-metaso-h3/README.md b/plugin-packages/src-metaso-h3/README.md new file mode 100644 index 000000000..47a8aa668 --- /dev/null +++ b/plugin-packages/src-metaso-h3/README.md @@ -0,0 +1,22 @@ +# METASO MiniMax H3 + +秘塔 METASO MiniMax H3 文生视频 / 图生视频协议插件(异步任务模式)。 + +## Provider + +| Provider | 能力 | 端点 | +|---|---|---| +| metaso-h3 | video | POST /api/minimax/v2/video_generation(创建)
GET /api/minimax/v2/query/video_generation/{taskId}(轮询) | + +## 配置 + +在渠道中填写 API Key(METASO 控制台创建,形如 `mk-xxxxxxxx`),baseUrl 使用 `https://metaso.cn`。 + +## 说明 + +- 模型:`MiniMax-H3`(支持 768P / 2K,4-15 秒,文生图/图生视频/多模态参考),`MiniMax-H3-Max`(480P / 768P,5-15 秒)。 +- 文生视频:仅文本提示词,ratio 必填(21:9 / 16:9 / 4:3 / 1:1 / 3:4 / 9:16)。 +- 图生视频:图片 role 为 first_frame / last_frame(首尾帧)。 +- 多模态参考:图片 reference_image / 视频 reference_video / 音频 reference_audio。 +- 异步任务:创建后返回 task_id,轮询查询接口直到 status=succeeded,成片 URL 在 task.content.url。 +- 鉴权:`Authorization: Bearer mk-xxx`。 diff --git a/plugin-packages/src-metaso-h3/docs/interface.md b/plugin-packages/src-metaso-h3/docs/interface.md new file mode 100644 index 000000000..85cdb615a --- /dev/null +++ b/plugin-packages/src-metaso-h3/docs/interface.md @@ -0,0 +1,61 @@ +# METASO MiniMax H3 接口说明 + +## 鉴权 + +`Authorization: Bearer mk-xxxxxxxxxxxxxxxxxxxxxxxx` + +## 创建视频生成任务 + +POST `https://metaso.cn/api/minimax/v2/video_generation` + +请求体: + +```json +{ + "model": "MiniMax-H3", + "content": [ + { "type": "text", "text": "视频描述" }, + { "type": "image_url", "image_url": { "url": "https://..." }, "role": "first_frame" }, + { "type": "video_url", "video_url": { "url": "https://..." }, "role": "reference_video" }, + { "type": "audio_url", "audio_url": { "url": "https://..." }, "role": "reference_audio" } + ], + "resolution": "768P", + "duration": 5, + "ratio": "16:9" +} +``` + +- content 必须包含一个非空 text 项。 +- 首尾帧(first_frame/last_frame)与多模态参考(reference_*)互斥。 +- 图片 ≤ 9 张、视频 ≤ 3 段、音频 ≤ 3 段;请求体总大小 ≤ 64 MB。 + +响应: + +```json +{ "task_id": "424010985738629" } +``` + +## 查询任务 + +GET `https://metaso.cn/api/minimax/v2/query/video_generation/{task_id}` + +响应: + +```json +{ + "task": { + "id": "424010985738629", + "status": "succeeded", + "content": { "url": "https://.../output.mp4" }, + "resolution": "2K", + "duration": 5, + "usage": { "total_seconds": 5, "output_seconds": 5 } + } +} +``` + +status 取值:queued / running / succeeded / failed / cancelled。 + +## 错误 + +创建与查询均返回 OpenAI 风格错误体,含 `error.type` / `error.message` / `request_id`;余额不足为 402,速率限制为 429。 diff --git a/plugin-packages/src-metaso-h3/manifest.json b/plugin-packages/src-metaso-h3/manifest.json new file mode 100644 index 000000000..1a051962a --- /dev/null +++ b/plugin-packages/src-metaso-h3/manifest.json @@ -0,0 +1,100 @@ +{ + "apiVersion": "yingce.plugin/v2", + "id": "metaso-h3", + "name": "METASO MiniMax H3", + "version": "1.0.0", + "author": "METASO", + "description": "秘塔 METASO MiniMax H3 文生视频/图生视频协议插件(异步任务)。", + "permissions": ["generation.run", "media.read"], + "configuration": { + "fields": [ + { "name": "apiKey", "type": "secret", "label": "API Key", "required": true } + ] + }, + "contributes": { + "providers": [ + { + "id": "metaso-h3", + "label": "METASO MiniMax H3", + "capabilities": ["video"], + "scopes": ["admin.system-channel", "user.custom-channel", "canvas", "creation", "agent"], + "baseUrl": "https://metaso.cn", + "auth": { "type": "bearer", "field": "apiKey" }, + "requiresPublicMediaUrls": true, + "parameters": [ + { "name": "model", "type": "string", "required": true, "mapping": "model", "description": "视频模型 ID(MiniMax-H3 / MiniMax-H3-Max)。" }, + { "name": "prompt", "type": "string", "required": true, "mapping": "prompt/content/input", "description": "视频提示词。" }, + { "name": "images", "type": "media[]", "required": false, "mapping": "first/last/reference image", "description": "显式 role 图片输入(首帧/尾帧/参考图)。" }, + { "name": "videos", "type": "media[]", "required": false, "mapping": "reference video", "description": "参考视频。" }, + { "name": "audios", "type": "media[]", "required": false, "mapping": "reference audio/voice", "description": "参考音频或音色。" }, + { "name": "duration", "type": "integer", "required": false, "mapping": "duration/seconds", "description": "时长秒数(4-15)。" }, + { "name": "aspectRatio", "type": "string", "required": false, "mapping": "ratio/aspect_ratio/size", "description": "画幅比例(16:9 等)。" }, + { "name": "resolution", "type": "string", "required": false, "mapping": "resolution", "description": "分辨率档位(768P / 2K)。" }, + { "name": "watermark", "type": "boolean", "required": false, "mapping": "watermark", "description": "AIGC 水印开关。" }, + { "name": "providerOptions", "type": "object", "required": false, "mapping": "provider-specific fields", "description": "插件命名空间内的厂商扩展字段。" } + ], + "create": { + "method": "POST", + "path": "/api/minimax/v2/video_generation", + "contentType": "application/json", + "body": { + "model": { "$coalesce": [ { "$ref": "request.model" }, "MiniMax-H3" ] }, + "content": { + "$concatArrays": [ + { "$if": { "condition": { "$gt": [ { "$len": { "$ref": "request.prompt" } }, 0 ] }, "then": [ { "type": "text", "text": { "$ref": "request.prompt" } } ], "else": [] } }, + { "$map": { "from": { "$ref": "request.images" }, "as": "media", "in": { "type": "image_url", "image_url": { "url": { "$coalesce": [ { "$ref": "media.url" }, { "$ref": "media.dataUrl" } ] } }, "role": { "$omitEmpty": { "$ref": "media.role" } } } } }, + { "$map": { "from": { "$ref": "request.videos" }, "as": "media", "in": { "type": "video_url", "video_url": { "url": { "$coalesce": [ { "$ref": "media.url" }, { "$ref": "media.dataUrl" } ] } }, "role": { "$omitEmpty": { "$ref": "media.role" } } } } }, + { "$map": { "from": { "$ref": "request.audios" }, "as": "media", "in": { "type": "audio_url", "audio_url": { "url": { "$coalesce": [ { "$ref": "media.url" }, { "$ref": "media.dataUrl" } ] } }, "role": { "$omitEmpty": { "$ref": "media.role" } } } } } + ] + }, + "resolution": { "$omitEmpty": { "$coalesce": [ { "$ref": "request.resolution" }, { "$ref": "request.providerOptions.metaso-h3.resolution" }, "768P" ] } }, + "duration": { "$omitEmpty": { "$coalesce": [ { "$ref": "request.duration" }, { "$ref": "request.providerOptions.metaso-h3.duration" }, 5 ] } }, + "ratio": { "$omitEmpty": { "$coalesce": [ { "$ref": "request.aspectRatio" }, { "$ref": "request.providerOptions.metaso-h3.ratio" }, "16:9" ] } }, + "aigc_watermark": { "$omitEmpty": { "$ref": "request.providerOptions.metaso-h3.aigc_watermark" } } + } + }, + "poll": { + "method": "GET", + "path": "/api/minimax/v2/query/video_generation/{{taskId}}" + }, + "response": { + "taskId": { + "$coalesce": [ + { "$ref": "response.task_id" }, + { "$ref": "response.taskId" }, + { "$ref": "response.data.task_id" } + ] + }, + "status": { + "$coalesce": [ + { "$ref": "response.task.status" }, + { "$ref": "response.status" }, + { "$ref": "response.data.status" }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { "$ref": "response.task.error.message" }, + { "$ref": "response.task.fail_reason" }, + { "$ref": "response.error.message" }, + { "$ref": "response.message" } + ] + }, + "videos": { + "$coalesce": [ + { "$ref": "response.task.content.url" }, + { "$ref": "response.task.content.video_url" }, + { "$ref": "response.content.url" }, + { "$ref": "response.data.url" } + ] + }, + "usage": { "$ref": "response.task.usage" }, + "errorPaths": ["task.error.type", "error.type", "error.code"], + "resultEphemeral": true, + "messagePaths": ["task.error.message", "error.message"] + } + } + ] + } +} diff --git a/scripts/launch-canvas.ps1 b/scripts/launch-canvas.ps1 new file mode 100644 index 000000000..24aaa2264 --- /dev/null +++ b/scripts/launch-canvas.ps1 @@ -0,0 +1,105 @@ +# 影策画布一键启动器 +# 启动后端(8080) + 前端(3000) + cloudflared 隧道(公网 canvas.yingce.cc.cd),打开浏览器 + +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" + +$repoRoot = "D:\yingce\open-ai-canvas-main" +$backendDir = Join-Path $repoRoot "backend" +$webDir = Join-Path $repoRoot "web" +$dataDir = Join-Path $repoRoot ".local\project-workbench-debug" +$goBuildCache = Join-Path $repoRoot ".local\cache\go-build" +$goModuleCache = Join-Path $repoRoot ".local\cache\go-mod" +$cloudflaredExe = "C:\Program Files (x86)\cloudflared\cloudflared.exe" +$bunExe = "C:\Users\Administrator\AppData\Roaming\npm\node_modules\bun\bin\bun.exe" +if (-not (Test-Path -LiteralPath $bunExe)) { $bunExe = "bun" } +$localUrl = "http://localhost:3000" +$healthUrl = "http://127.0.0.1:3000/api/health" + +function Test-PortListen([int]$Port) { + try { + return $null -ne (Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction Stop | Select-Object -First 1) + } catch { + return $false + } +} + +function Test-Http([string]$Url) { + try { + $r = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 3 + return $r.StatusCode -ge 200 -and $r.StatusCode -lt 400 + } catch { + return $false + } +} + +# 0) 已全部就绪则直接打开 +if ((Test-PortListen 8080) -and (Test-PortListen 3000)) { + Write-Host "影策画布已在运行,正在打开..." -ForegroundColor Green + Start-Process $localUrl + exit 0 +} + +# 1) 后端:8080 +if (-not (Test-PortListen 8080)) { + Write-Host "启动后端 (8080)..." -ForegroundColor Cyan + New-Item -ItemType Directory -Force -Path $dataDir, $goBuildCache, $goModuleCache | Out-Null + $env:CANVAS_BACKEND_ADDR = "127.0.0.1:8080" + $env:CANVAS_BACKEND_DATA_DIR = $dataDir + $env:CANVAS_PUBLIC_BASE_URL = "https://canvas.yingce.cc.cd" + $env:GOPROXY = "https://goproxy.cn,direct" + $env:GOCACHE = $goBuildCache + $env:GOMODCACHE = $goModuleCache + Start-Process -FilePath "go" -ArgumentList "run", "./cmd/server" ` + -WorkingDirectory $backendDir ` + -RedirectStandardOutput (Join-Path $repoRoot ".local\backend.stdout.log") ` + -RedirectStandardError (Join-Path $repoRoot ".local\backend.stderr.log") ` + -WindowStyle Hidden | Out-Null +} + +# 2) 前端:3000 +if (-not (Test-PortListen 3000)) { + Write-Host "启动前端 (3000)..." -ForegroundColor Cyan + $viteBinary = Join-Path $webDir "node_modules\.bin\vite" + if (-not (Test-Path -LiteralPath $viteBinary)) { + Write-Host "安装前端依赖..." -ForegroundColor Yellow + Push-Location $webDir + try { & bun install --frozen-lockfile } finally { Pop-Location } + } + $env:VITE_API_PROXY_TARGET = "http://127.0.0.1:8080" + Start-Process -FilePath $bunExe -ArgumentList "run", "dev" ` + -WorkingDirectory $webDir ` + -RedirectStandardOutput (Join-Path $repoRoot ".local\web.stdout.log") ` + -RedirectStandardError (Join-Path $repoRoot ".local\web.stderr.log") ` + -WindowStyle Hidden | Out-Null +} + +# 3) cloudflared 隧道(公网 canvas.yingce.cc.cd) +if (-not (Get-Process -Name cloudflared -ErrorAction SilentlyContinue)) { + if (Test-Path -LiteralPath $cloudflaredExe) { + Write-Host "启动公网隧道 cloudflared..." -ForegroundColor Cyan + Start-Process -FilePath $cloudflaredExe -ArgumentList "--config", "C:\Users\Administrator\.cloudflared\config.yml", "tunnel", "run" -WindowStyle Hidden | Out-Null + } else { + Write-Host "未找到 cloudflared,跳过公网隧道(本地仍可访问)" -ForegroundColor Yellow + } +} else { + Write-Host "公网隧道 cloudflared 已在运行" -ForegroundColor DarkGray +} + +# 4) 等待健康检查并打开浏览器 +$deadline = (Get-Date).AddMinutes(3) +do { + Start-Sleep -Seconds 2 + if (Test-Http $healthUrl) { + Write-Host "影策画布已启动,正在打开浏览器..." -ForegroundColor Green + Start-Sleep -Seconds 1 + Start-Process $localUrl + Write-Host "本地: http://localhost:3000" -ForegroundColor Green + Write-Host "公网: https://canvas.yingce.cc.cd" -ForegroundColor Green + exit 0 + } +} while ((Get-Date) -lt $deadline) + +Write-Host "启动超时,请查看日志:.local\backend.stderr.log / .local\web.stderr.log" -ForegroundColor Red diff --git a/scripts/start-canvas.ps1 b/scripts/start-canvas.ps1 new file mode 100644 index 000000000..2a3ec4bb1 --- /dev/null +++ b/scripts/start-canvas.ps1 @@ -0,0 +1,56 @@ +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$localStarter = Join-Path $PSScriptRoot "start-local.ps1" +$healthUrl = "http://127.0.0.1:3000/api/health" +$canvasUrl = "http://localhost:3000/canvas" + +function Test-CanvasHttp([string]$Url) { + try { + $response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 3 + return $response.StatusCode -ge 200 -and $response.StatusCode -lt 400 + } catch { + return $false + } +} + +function Test-ListeningPort([int]$Port) { + try { + return $null -ne (Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction Stop | Select-Object -First 1) + } catch { + return $false + } +} + +if (Test-CanvasHttp $healthUrl) { + Write-Host "影策画布已经在运行,正在打开..." -ForegroundColor Green + Start-Process $canvasUrl + exit 0 +} + +$occupiedPorts = @(3000, 8080) | Where-Object { Test-ListeningPort $_ } +if ($occupiedPorts.Count -gt 0) { + throw "端口 $($occupiedPorts -join ', ') 已被其他程序占用,无法安全启动影策。" +} + +if (-not (Test-Path -LiteralPath $localStarter)) { + throw "未找到项目启动脚本:$localStarter" +} + +Write-Host "正在启动影策前端和后端,请稍候..." -ForegroundColor Cyan +& $localStarter + +$deadline = (Get-Date).AddMinutes(3) +do { + Start-Sleep -Seconds 2 + if (Test-CanvasHttp $healthUrl) { + Write-Host "影策画布已启动,正在打开浏览器..." -ForegroundColor Green + Start-Process $canvasUrl + exit 0 + } +} while ((Get-Date) -lt $deadline) + +throw "影策服务已拉起,但 3 分钟内未通过健康检查。请查看打开的前端、后端窗口。" diff --git a/scripts/start-local.ps1 b/scripts/start-local.ps1 index 7a9e023d6..0eeb4e186 100644 --- a/scripts/start-local.ps1 +++ b/scripts/start-local.ps1 @@ -3,6 +3,9 @@ param() $ErrorActionPreference = "Stop" +. (Join-Path $PSScriptRoot "windows-proxy.ps1") +Import-CanvasWindowsProxy + $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path $backendDir = Join-Path $repoRoot "backend" $webDir = Join-Path $repoRoot "web" diff --git a/scripts/windows-proxy.ps1 b/scripts/windows-proxy.ps1 new file mode 100644 index 000000000..5d0c22c16 --- /dev/null +++ b/scripts/windows-proxy.ps1 @@ -0,0 +1,35 @@ +# Go uses proxy environment variables, not Windows Internet Settings. +# Inherit the user's enabled static proxy only when no explicit proxy was set. +function Import-CanvasWindowsProxy { + if ($env:HTTPS_PROXY -or $env:HTTP_PROXY -or $env:ALL_PROXY) { return } + $settings = Get-ItemProperty -LiteralPath 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction SilentlyContinue + if (-not $settings -or $settings.ProxyEnable -ne 1 -or -not $settings.ProxyServer) { return } + $proxies = @{} + foreach ($entry in ($settings.ProxyServer -split ';')) { + if ($entry -match '^\s*(https?)=(.+)$') { + $proxies[$Matches[1]] = $Matches[2].Trim() + } elseif ($entry -notmatch '=') { + $proxies['http'] = $entry.Trim() + $proxies['https'] = $entry.Trim() + } + } + foreach ($scheme in @('http', 'https')) { + $address = $proxies[$scheme] + if (-not $address) { continue } + if ($address -notmatch '^[a-z]+://') { $address = "http://$address" } + $parsed = $null + if (-not [Uri]::TryCreate($address, [UriKind]::Absolute, [ref]$parsed) -or $parsed.Scheme -notin @('http', 'https')) { + throw 'Windows proxy address is invalid. Check system proxy settings.' + } + [Environment]::SetEnvironmentVariable("$($scheme.ToUpper())_PROXY", $address, 'Process') + } + if ($env:HTTP_PROXY -or $env:HTTPS_PROXY) { + $bypass = @($env:NO_PROXY, 'localhost', '127.0.0.1', '::1') + foreach ($entry in ($settings.ProxyOverride -split ';')) { + $entry = $entry.Trim() + if ($entry -and $entry -ne '') { $bypass += $entry } + } + $env:NO_PROXY = ($bypass | Where-Object { $_ } | Select-Object -Unique) -join ',' + Write-Host 'Backend network: using the enabled Windows proxy.' -ForegroundColor Cyan + } +} diff --git a/web/src/components/canvas/canvas-batch-table-node.tsx b/web/src/components/canvas/canvas-batch-table-node.tsx index 5c9350d3c..e5e55cc97 100644 --- a/web/src/components/canvas/canvas-batch-table-node.tsx +++ b/web/src/components/canvas/canvas-batch-table-node.tsx @@ -1,25 +1,12 @@ -import { useCallback, useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent, type ReactNode } from "react"; -import { Button, Switch, Tooltip } from "antd"; -import { Image as ImageIcon, LoaderCircle, Minus, Play, Plus, Rows3, Trash2, Upload } from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState, type MutableRefObject, type PointerEvent as ReactPointerEvent, type ReactNode } from "react"; +import { Button, Input, Select, Switch, Tooltip } from "antd"; +import { GripVertical, Image as ImageIcon, LoaderCircle, Play, Plus, RefreshCw, Rows3, Trash2, Upload } from "lucide-react"; import { CachedResourceImage } from "@/components/cached-resource-image"; -import { CanvasResourceMentionTextarea } from "@/components/canvas/canvas-resource-mention-textarea"; -import { - BATCH_REFERENCE_HANDLE_GAP, - BATCH_REFERENCE_HANDLE_TOP, - MAX_BATCH_REFERENCE_COLUMNS, - MIN_BATCH_REFERENCE_COLUMNS, - batchPromptForRow, - batchReferenceColumns, - batchReferenceHandleId, - batchReferenceMentionToken, -} from "@/lib/canvas/canvas-batch-table"; -import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references"; +import { BATCH_REFERENCE_HANDLE_GAP, BATCH_REFERENCE_HANDLE_TOP, MAX_BATCH_REFERENCE_COLUMNS, MAX_BATCH_TEXT_COLUMNS, batchGridTemplateColumns, batchPromptForRow, batchReferenceColumns, batchReferenceHandleId, batchTextColumns, batchTextHandleId, batchTextHandleTop } from "@/lib/canvas/canvas-batch-table"; import type { CanvasTheme } from "@/lib/canvas-theme"; import type { CanvasBatchOperation, CanvasBatchRow, CanvasBatchTableData, CanvasConnection, CanvasGenerationBatch, CanvasGenerationBatchItem, CanvasNodeData } from "@/types/canvas"; -type ReferenceCell = { rowId: string; columnIndex: number }; - type Props = { node: CanvasNodeData; nodes: CanvasNodeData[]; @@ -34,11 +21,11 @@ type Props = { onGenerate: (rowIds?: string[]) => void; onRetryItem: (batchId: string, itemId: string) => void; onAddReferenceColumn: () => void; - onRemoveReferenceColumn?: () => void; - onFocusOutput?: (nodeId: string) => void; - onReorderReferenceColumns?: (fromColumnId: string, toColumnId: string) => void; - onMoveReferenceCell?: (sourceRowId: string, sourceColumnIndex: number, targetRowId: string, targetColumnIndex: number) => void; - onUploadReference?: (rowId: string, columnIndex: number, file: File) => void; + onAddTextColumn: () => void; + onReorderReferenceColumns: (fromColumnId: string, toColumnId: string) => void; + onMoveReferenceCell: (sourceRowId: string, sourceColumnIndex: number, targetRowId: string, targetColumnIndex: number) => void; + onReplaceReference: (node: CanvasNodeData) => void; + onUploadReference: (rowId: string, columnIndex: number, file: File) => void; onConnectStart: (event: ReactPointerEvent, handleId: string) => void; onConnectDrop?: (event: ReactPointerEvent, handleId: string) => void; readOnly?: boolean; @@ -49,415 +36,545 @@ const OPERATION_OPTIONS = [ { value: "creative", label: "创意生图" }, ] satisfies Array<{ value: CanvasBatchOperation; label: string }>; -const CONCURRENCY_OPTIONS = [1, 5, 10] as const; +type ReferenceCell = { rowId: string; columnIndex: number }; -export function CanvasBatchTableNodeContent({ node, nodes, connections, batch, theme, onPatchTable, onAddRow, onRemoveRow, onUpdateRow, onFillRows, onGenerate, onRetryItem, onAddReferenceColumn, onRemoveReferenceColumn, onReorderReferenceColumns, onMoveReferenceCell, onUploadReference, onFocusOutput, onConnectStart, onConnectDrop, readOnly = false }: Props) { +export function CanvasBatchTableNodeContent({ node, nodes, connections, batch, theme, onPatchTable, onAddRow, onRemoveRow, onUpdateRow, onFillRows, onGenerate, onRetryItem, onAddReferenceColumn, onAddTextColumn, onReorderReferenceColumns, onMoveReferenceCell, onReplaceReference, onUploadReference, onConnectStart, onConnectDrop, readOnly = false }: Props) { const table = node.metadata?.batchTable || { operation: "try_on" as const, concurrency: 10, rows: [] }; const referenceColumns = batchReferenceColumns(table); + const textColumns = batchTextColumns(table); const globalPrompt = table.globalPrompt || ""; const hasGlobalPrompt = Boolean(globalPrompt.trim()); const nodeById = useMemo(() => new Map(nodes.map((item) => [item.id, item])), [nodes]); const batchItemByRowId = useMemo(() => new Map((batch?.items || []).map((item) => [item.rowId, item])), [batch?.items]); - const connectedImageCount = useMemo(() => new Set(connections.filter((connection) => connection.toNodeId === node.id && connection.relation !== "batch-output").map((connection) => connection.fromNodeId)).size, [connections, node.id]); - const completed = table.rows.filter((row) => hasNodeMedia(row.outputNodeId ? nodeById.get(row.outputNodeId) : undefined)).length; - const unfinishedReadyCount = table.rows.filter((row) => rowReady(row, table, nodeById) && !hasNodeMedia(row.outputNodeId ? nodeById.get(row.outputNodeId) : undefined)).length; - const gridTemplateColumns = `64px repeat(${referenceColumns.length}, 88px) minmax(280px, 1fr) 88px 80px`; - const subtleSurface = `color-mix(in srgb, ${theme.node.text} 4%, transparent)`; - const inputSurface = theme.node.panel; - const fileInputRef = useRef(null); - const uploadTargetRef = useRef(null); + const connectedImageCount = useMemo(() => new Set(connections.filter((connection) => connection.toNodeId === node.id).map((connection) => connection.fromNodeId)).size, [connections, node.id]); + const completed = table.rows.filter((row) => Boolean(row.outputNodeId && nodeById.get(row.outputNodeId)?.metadata?.content)).length; + const gridTemplateColumns = batchGridTemplateColumns(referenceColumns.length, textColumns.length); const [draggingColumnId, setDraggingColumnId] = useState(null); const [draggingCell, setDraggingCell] = useState(null); + const [dropCell, setDropCell] = useState(null); const draggingCellRef = useRef(null); + const dropCellRef = useRef(null); const dragStartRef = useRef<{ x: number; y: number; pointerId: number; cell: ReferenceCell } | null>(null); + const lastPointerRef = useRef({ x: 0, y: 0 }); const suppressClickRef = useRef(false); + const uploadTargetRef = useRef<{ rowId: string; columnIndex: number } | null>(null); + const uploadInputRef = useRef(null); const clearReferenceDrag = useCallback(() => { dragStartRef.current = null; draggingCellRef.current = null; + dropCellRef.current = null; setDraggingCell(null); + setDropCell(null); }, []); const findReferenceCellAtPoint = useCallback((clientX: number, clientY: number) => { - const target = document.elementFromPoint(clientX, clientY)?.closest("[data-batch-reference-cell]"); - const rowId = target?.dataset.rowId; - const columnIndex = Number(target?.dataset.columnIndex); + const hit = document.elementsFromPoint(clientX, clientY) + .map((element) => element.closest("[data-batch-reference-cell]")) + .find((element): element is HTMLElement => Boolean(element)); + if (!hit) return null; + const rowId = hit.dataset.batchRowId; + const columnIndex = Number(hit.dataset.batchColumnIndex); return rowId && Number.isInteger(columnIndex) ? { rowId, columnIndex } : null; }, []); - useEffect(() => { - if (readOnly) return; - const handleMove = (event: PointerEvent) => { - const start = dragStartRef.current; - if (!start || start.pointerId !== event.pointerId) return; - const distance = Math.hypot(event.clientX - start.x, event.clientY - start.y); - if (!draggingCellRef.current && distance < 5) return; - if (!draggingCellRef.current) { - draggingCellRef.current = start.cell; - suppressClickRef.current = true; - setDraggingCell(start.cell); - } - }; - const handleUp = (event: PointerEvent) => { - const start = dragStartRef.current; - if (!start || start.pointerId !== event.pointerId) return; - const sourceCell = draggingCellRef.current; - const targetCell = sourceCell ? findReferenceCellAtPoint(event.clientX, event.clientY) : null; - if (sourceCell && targetCell) onMoveReferenceCell?.(sourceCell.rowId, sourceCell.columnIndex, targetCell.rowId, targetCell.columnIndex); - const didDrag = Boolean(sourceCell); - clearReferenceDrag(); - if (didDrag) { - suppressClickRef.current = true; - window.setTimeout(() => { suppressClickRef.current = false; }, 0); - } - }; - window.addEventListener("pointermove", handleMove); - window.addEventListener("pointerup", handleUp); - window.addEventListener("pointercancel", handleUp); - return () => { - window.removeEventListener("pointermove", handleMove); - window.removeEventListener("pointerup", handleUp); - window.removeEventListener("pointercancel", handleUp); - }; - }, [clearReferenceDrag, findReferenceCellAtPoint, onMoveReferenceCell, readOnly]); - - const startCellDrag = (event: ReactPointerEvent, rowId: string, columnIndex: number) => { - if (readOnly || event.button !== 0) return; - event.stopPropagation(); - dragStartRef.current = { x: event.clientX, y: event.clientY, pointerId: event.pointerId, cell: { rowId, columnIndex } }; - }; - - const pickReferenceFile = (rowId: string, columnIndex: number) => { - if (readOnly) return; - if (suppressClickRef.current) { - suppressClickRef.current = false; - return; + const updateReferenceDrag = useCallback((clientX: number, clientY: number) => { + const start = dragStartRef.current; + if (!start || readOnly) return; + const distance = Math.hypot(clientX - start.x, clientY - start.y); + if (!draggingCellRef.current && distance < 5) return; + if (!draggingCellRef.current) { + draggingCellRef.current = start.cell; + setDraggingCell(start.cell); + suppressClickRef.current = true; } - uploadTargetRef.current = { rowId, columnIndex }; - fileInputRef.current?.click(); - }; + const nextDropCell = findReferenceCellAtPoint(clientX, clientY); + dropCellRef.current = nextDropCell; + setDropCell(nextDropCell); + }, [findReferenceCellAtPoint, readOnly]); + + const finishReferenceDrag = useCallback((clientX: number, clientY: number, cancel = false) => { + const start = dragStartRef.current; + if (!start) return; + const sourceCell = draggingCellRef.current; + const targetCell = cancel ? null : dropCellRef.current || findReferenceCellAtPoint(clientX, clientY); + if (sourceCell && targetCell) { + onMoveReferenceCell(sourceCell.rowId, sourceCell.columnIndex, targetCell.rowId, targetCell.columnIndex); + } + clearReferenceDrag(); + window.setTimeout(() => { suppressClickRef.current = false; }, 0); + }, [clearReferenceDrag, findReferenceCellAtPoint, onMoveReferenceCell]); + + const cancelReferenceDrag = useCallback(() => { + if (!dragStartRef.current) return; + clearReferenceDrag(); + window.setTimeout(() => { suppressClickRef.current = false; }, 0); + }, [clearReferenceDrag]); + + useEffect(() => { + const handleWindowPointerMove = (event: PointerEvent) => { + const start = dragStartRef.current; + if (!start || event.pointerId !== start.pointerId) return; + const distance = Math.hypot(event.clientX - start.x, event.clientY - start.y); + if (distance >= 5) event.preventDefault(); + updateReferenceDrag(event.clientX, event.clientY); + }; + const handleWindowPointerUp = (event: PointerEvent) => { + const start = dragStartRef.current; + if (!start || event.pointerId !== start.pointerId) return; + finishReferenceDrag(event.clientX, event.clientY); + }; + const handleWindowPointerCancel = (event: PointerEvent) => { + const start = dragStartRef.current; + if (!start || event.pointerId !== start.pointerId) return; + cancelReferenceDrag(); + }; + const handleWindowMouseUp = () => { + if (!dragStartRef.current) return; + finishReferenceDrag(lastPointerRef.current.x, lastPointerRef.current.y); + }; + const handleWindowBlur = () => cancelReferenceDrag(); + window.addEventListener("pointermove", handleWindowPointerMove, { passive: false }); + window.addEventListener("pointerup", handleWindowPointerUp, true); + window.addEventListener("pointercancel", handleWindowPointerCancel, true); + window.addEventListener("mouseup", handleWindowMouseUp, true); + window.addEventListener("blur", handleWindowBlur); + return () => { + window.removeEventListener("pointermove", handleWindowPointerMove); + window.removeEventListener("pointerup", handleWindowPointerUp, true); + window.removeEventListener("pointercancel", handleWindowPointerCancel, true); + window.removeEventListener("mouseup", handleWindowMouseUp, true); + window.removeEventListener("blur", handleWindowBlur); + }; + }, [cancelReferenceDrag, finishReferenceDrag, updateReferenceDrag]); + + const handleReferencePointerDown = useCallback((event: ReactPointerEvent, rowId: string, columnIndex: number, hasMaterial: boolean) => { + if (readOnly || !hasMaterial) return; + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.setPointerCapture(event.pointerId); + lastPointerRef.current = { x: event.clientX, y: event.clientY }; + dragStartRef.current = { x: event.clientX, y: event.clientY, pointerId: event.pointerId, cell: { rowId, columnIndex } }; + suppressClickRef.current = false; + }, [readOnly]); + + const handleReferencePointerMove = useCallback((event: ReactPointerEvent) => { + event.stopPropagation(); + lastPointerRef.current = { x: event.clientX, y: event.clientY }; + updateReferenceDrag(event.clientX, event.clientY); + }, [updateReferenceDrag]); + + const handleReferencePointerUp = useCallback((event: ReactPointerEvent) => { + event.stopPropagation(); + if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId); + finishReferenceDrag(event.clientX, event.clientY); + }, [finishReferenceDrag]); + + const handleReferencePointerCancel = useCallback((event: ReactPointerEvent) => { + event.stopPropagation(); + if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId); + cancelReferenceDrag(); + }, [cancelReferenceDrag]); return ( -
- {!readOnly ? { - const file = event.currentTarget.files?.[0]; - const target = uploadTargetRef.current; - event.currentTarget.value = ""; - uploadTargetRef.current = null; - if (file && target) onUploadReference?.(target.rowId, target.columnIndex, file); - }} /> : null} - {!readOnly ? : null} - -
-
-
event.stopPropagation()}> - onPatchTable({ operation: operation as CanvasBatchOperation })} /> -
- 并发 - ({ value, label: String(value) }))} value={table.concurrency} onChange={(concurrency) => onPatchTable({ concurrency: Number(concurrency) })} /> -
-
- {referenceColumns.length}/{MAX_BATCH_REFERENCE_COLUMNS} 组参考 - {!readOnly ? ( - <> - - - - = MAX_BATCH_REFERENCE_COLUMNS ? "最多支持 6 组参考图" : `新增参考图 ${referenceColumns.length + 1}`}> - - - - ) : null} -
- 已连 {connectedImageCount} · 完成 {completed}/{table.rows.length} +
event.stopPropagation()} + onMouseDown={(event) => { + // Only the top padding is a node drag surface; keep child controls isolated. + if (event.target !== event.currentTarget) event.stopPropagation(); + }} + > + { + const file = event.currentTarget.files?.[0]; + const target = uploadTargetRef.current; + uploadTargetRef.current = null; + event.currentTarget.value = ""; + if (file && target) onUploadReference(target.rowId, target.columnIndex, file); + }} + /> + {!readOnly ? : null} +
event.stopPropagation()} + > + ({ value, label: String(value) }))} + disabled={readOnly} + onChange={(concurrency) => onPatchTable({ concurrency })} + className="w-20" + aria-label="并发数" + /> + + {referenceColumns.length} 组参考 · 已连 {connectedImageCount} 张图 · 完成 {completed}/{table.rows.length} + + {!readOnly ? ( +
+ 全局提示词 + onPatchTable({ globalPrompt: event.target.value })} + /> + {hasGlobalPrompt ? 已覆盖全部 : null}
- {!readOnly ? ( -
event.stopPropagation()}> - - - - - -
- ) : null} -
-
event.stopPropagation()}> - 全局提示词 - onPatchTable({ globalPrompt: event.target.value })} - /> -
+ ) : null} + {!readOnly ? ( +
+ + + +
+ ) : null}
-
event.stopPropagation()}> -
- 任务 +
event.stopPropagation()}> +
+ # + 任务 {referenceColumns.map((column) => ( - setDraggingColumnId(column.id)} - onDragEnd={() => setDraggingColumnId(null)} - onDragOver={(event) => { event.preventDefault(); }} - onDrop={() => { if (draggingColumnId) onReorderReferenceColumns?.(draggingColumnId, column.id); setDraggingColumnId(null); }} - > - {column.label} - + setDraggingColumnId(column.id)} onDragEnd={() => setDraggingColumnId(null)} onDrop={(targetColumnId) => { if (draggingColumnId) onReorderReferenceColumns(draggingColumnId, targetColumnId); setDraggingColumnId(null); }} /> ))} - 任务提示词 - 生成结果 - 操作 + {textColumns.map((column, index) => ( + {column.label} + ))} + 任务提示词 + 生成结果 +
- {table.rows.length ? ( table.rows.map((row, index) => { const output = row.outputNodeId ? nodeById.get(row.outputNodeId) : undefined; const item = batchItemByRowId.get(row.id); - const status = row.enabled ? rowStatus(item, output) : { label: "已停用", tone: "idle" as const, loading: false, retryable: false }; - const ready = rowReady(row, table, nodeById); - const completedRow = hasNodeMedia(output); - const references = batchRowMentionReferences(row, referenceColumns, nodeById); - const effectivePrompt = batchPromptForRow(table, row); - const disabledReason = status.loading ? "当前任务正在生成" : !row.enabled ? "请先启用这一行" : !effectivePrompt.trim() ? "请填写任务提示词" : table.operation === "try_on" && row.inputNodeIds.filter(Boolean).length < 2 ? "批量换装至少需要两张参考图" : !ready ? "请补齐有效参考图" : ""; + const status = rowStatus(item, output); return ( -
-
- {!readOnly ? onUpdateRow(row.id, { enabled })} /> : null} - {index + 1} +
+ {index + 1} +
+ onUpdateRow(row.id, { enabled })} aria-label={`任务 ${index + 1}`} /> + {row.enabled === false ? "关" : "开"}
{referenceColumns.map((column, columnIndex) => ( -
- pickReferenceFile(row.id, columnIndex)} - onUploadFile={(file) => onUploadReference?.(row.id, columnIndex, file)} - onPointerDown={(event) => startCellDrag(event, row.id, columnIndex)} - /> + { uploadTargetRef.current = { rowId: row.id, columnIndex }; uploadInputRef.current?.click(); }} + onPointerDown={handleReferencePointerDown} + onPointerMove={handleReferencePointerMove} + onPointerUp={handleReferencePointerUp} + onPointerCancel={handleReferencePointerCancel} + onLostPointerCapture={handleReferencePointerCancel} + /> + ))} + {textColumns.map((column, columnIndex) => ( +
+
))} -
- onUpdateRow(row.id, { prompt })} - onSubmit={!readOnly && ready && !status.loading ? () => onGenerate([row.id]) : undefined} - onPointerDown={(event) => event.stopPropagation()} - onWheel={(event) => event.stopPropagation()} - /> -
- {readOnly ? "输入 @ 插入参考图" : "输入 @ 插入参考图 · ⌘/Ctrl + Enter 生成此行"} + onUpdateRow(row.id, { prompt: event.target.value })} /> +
+ {output?.metadata?.content || output?.metadata?.storageKey ? ( + } + /> + ) : ( + + )} +
+
{status.label}
+ {!readOnly && status.retryable && batch && item ? ( + + ) : null}
-
-
- { if (row.outputNodeId) onFocusOutput?.(row.outputNodeId); }} - /> + {status.loading ? : null}
{!readOnly ? ( -
- -
- ) : } + +
); }) ) : ( -
-
-
-
还没有批量任务
-

把图片连接到左侧参考图端口后同步连线,或先添加一行手工配置。

- {!readOnly ? : null} -
-
+
连接图片后会自动生成任务;点击缩略图可替换素材,拖动参考图列可调整顺序。
)}
); } -function BatchChoiceGroup({ ariaLabel, options, value, onChange, theme, compact = false, disabled = false }: { ariaLabel: string; options: Array<{ value: string | number; label: string }>; value: string | number; onChange: (value: string | number) => void; theme: CanvasTheme; compact?: boolean; disabled?: boolean }) { +function ReferenceColumnHeader({ column, theme, readOnly, dragging, onDragStart, onDragEnd, onDrop }: { column: { id: string; label: string }; theme: CanvasTheme; readOnly: boolean; dragging: boolean; onDragStart: () => void; onDragEnd: () => void; onDrop: (columnId: string) => void }) { return ( -
- {options.map((option) => { - const selected = option.value === value; - return ; - })} -
+ { + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.setData("text/plain", column.id); + onDragStart(); + }} + onDragEnd={onDragEnd} + onDragOver={(event) => { if (!readOnly) { event.preventDefault(); event.dataTransfer.dropEffect = "move"; } }} + onDrop={(event) => { event.preventDefault(); onDrop(column.id); }} + > + {!readOnly ? : null} + {column.label} + ); } -function BatchReferenceHandles({ columns, theme, onConnectStart, onConnectDrop }: { columns: ReturnType; theme: CanvasTheme; onConnectStart: (event: ReactPointerEvent, handleId: string) => void; onConnectDrop?: (event: ReactPointerEvent, handleId: string) => void }) { - const commonStyle = { left: 0, width: 36, height: 36, transform: "translate(-50%, -50%)", transformOrigin: "center" }; - return <>{columns.map((column, index) => )}; +function BatchTextHandle(props: Omit, "column"> & { column: { id: string; label: string } }) { + return ; } -function BatchReferenceHandle({ column, index, theme, commonStyle, onConnectStart, onConnectDrop }: { column: { id: string; label: string }; index: number; theme: CanvasTheme; commonStyle: { left: number; width: number; height: number; transform: string; transformOrigin: string }; onConnectStart: (event: ReactPointerEvent, handleId: string) => void; onConnectDrop?: (event: ReactPointerEvent, handleId: string) => void }) { +function BatchReferenceHandles({ + columns, + textColumns, + theme, + onAdd, + onAddText, + onConnectStart, + onConnectDrop, +}: { + columns: ReturnType; + textColumns: ReturnType; + theme: CanvasTheme; + onAdd: () => void; + onAddText: () => void; + onConnectStart: (event: ReactPointerEvent, handleId: string) => void; + onConnectDrop?: (event: ReactPointerEvent, handleId: string) => void; +}) { + const commonStyle = { left: 0, width: 44, height: 44, transform: "translate(-50%, -50%)", transformOrigin: "center" }; + const referenceAddTop = BATCH_REFERENCE_HANDLE_TOP + columns.length * BATCH_REFERENCE_HANDLE_GAP; + const textTop = batchTextHandleTop(columns.length); + return ( + <> + {columns.map((column, index) => ( + + ))} + {textColumns.map((column, index) => ( + + ))} + {columns.length < MAX_BATCH_REFERENCE_COLUMNS ? ( + + + + ) : null} + {textColumns.length < MAX_BATCH_TEXT_COLUMNS ? ( + + + + ) : null} + + ); +} + +function BatchReferenceHandle({ + column, + top, + badge, + theme, + commonStyle, + onConnectStart, + onConnectDrop, + handlePrefix = "reference", +}: { + column: { id: string; label: string }; + top: number; + badge: string; + theme: CanvasTheme; + commonStyle: { left: number; width: number; height: number; transform: string; transformOrigin: string }; + onConnectStart: (event: ReactPointerEvent, handleId: string) => void; + onConnectDrop?: (event: ReactPointerEvent, handleId: string) => void; + handlePrefix?: "reference" | "text"; +}) { const [hovered, setHovered] = useState(false); const [offset, setOffset] = useState({ x: 0, y: 0 }); - const handleId = batchReferenceHandleId(column.id); - const reset = useCallback(() => { setHovered(false); setOffset({ x: 0, y: 0 }); }, []); + const handleId = handlePrefix === "text" ? batchTextHandleId(column.id) : batchReferenceHandleId(column.id); + const reset = useCallback(() => { + setHovered(false); + setOffset({ x: 0, y: 0 }); + }, []); const update = useCallback((event: ReactPointerEvent) => { const bounds = event.currentTarget.getBoundingClientRect(); const dx = event.clientX - (bounds.left + bounds.width / 2); const dy = event.clientY - (bounds.top + bounds.height / 2); - const limit = 10; + const limit = 16; setOffset({ x: Math.max(-limit, Math.min(limit, dx)), y: Math.max(-limit, Math.min(limit, dy)) }); }, []); return ( - - ); } - -function batchRowMentionReferences(row: CanvasBatchRow, columns: ReturnType, nodeById: Map): CanvasResourceReference[] { - return columns.flatMap((column, index) => { - const source = nodeById.get(row.inputNodeIds[index]); - if (!source) return []; - return [{ id: `${row.id}:${column.id}:${source.id}`, nodeId: source.id, kind: "image" as const, label: `参考图${index + 1}`, title: `${column.label} · ${source.title || "图片"}`, previewUrl: source.metadata?.previewContent || source.metadata?.content, storageKey: source.metadata?.storageKey, active: true, sourceType: source.type, mentionToken: batchReferenceMentionToken(index) }]; - }); +function TextNodeCell({ node, theme }: { node?: CanvasNodeData; theme: CanvasTheme }) { + const content = node?.metadata?.content || node?.metadata?.prompt; + return
{content || "未连接文字"}
; } -function ReferenceThumbnail({ node, label, theme, readOnly, rowId, columnIndex, isDraggingCell, onPickFile, onUploadFile, onPointerDown }: { node?: CanvasNodeData; label: string; theme: CanvasTheme; readOnly: boolean; rowId: string; columnIndex: number; isDraggingCell: boolean; onPickFile: () => void; onUploadFile: (file: File) => void; onPointerDown: (event: ReactPointerEvent) => void }) { - const filled = node && hasNodeMedia(node); +function ReferenceThumbnail({ node, theme, readOnly, rowId, columnIndex, columnId, draggingColumnId, isDraggingCell, isDropTarget, suppressClickRef, onReplace, onUpload, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onLostPointerCapture }: { + node?: CanvasNodeData; + theme: CanvasTheme; + readOnly: boolean; + rowId: string; + columnIndex: number; + columnId: string; + draggingColumnId: string | null; + isDraggingCell: boolean; + isDropTarget: boolean; + suppressClickRef: MutableRefObject; + onReplace: (node: CanvasNodeData) => void; + onUpload: () => void; + onPointerDown: (event: ReactPointerEvent, rowId: string, columnIndex: number, hasMaterial: boolean) => void; + onPointerMove: (event: ReactPointerEvent) => void; + onPointerUp: (event: ReactPointerEvent) => void; + onPointerCancel: (event: ReactPointerEvent) => void; + onLostPointerCapture: (event: ReactPointerEvent) => void; +}) { + const cellData = { "data-batch-reference-cell": true, "data-batch-row-id": rowId, "data-batch-column-index": String(columnIndex) }; + const dropStyle = isDropTarget ? { boxShadow: `0 0 0 2px ${theme.accent.primary}, 0 0 0 5px ${theme.accent.primary}33` } : undefined; + if (!node || (!node.metadata?.content && !node.metadata?.storageKey)) { + return ( + + + + ); + } const fallback = ; return ( - + ); } -function ResultThumbnail({ output, status, theme, onFocus }: { output?: CanvasNodeData; status: ReturnType; theme: CanvasTheme; onFocus: () => void }) { - const filled = hasNodeMedia(output); - const tone = statusColor(status.tone, theme.node.stroke); - const title = filled ? `${status.label} · 点击定位到画布节点` : status.label; +function EmptyThumbnail({ theme, sizeClass = "size-12" }: { theme: CanvasTheme; sizeClass?: string }): ReactNode { return ( - - - - ); -} - -function statusColor(tone: RowStatusTone, fallback: string) { - if (tone === "success") return "var(--status-success)"; - if (tone === "error") return "var(--status-error)"; - if (tone === "loading") return "var(--status-loading)"; - return fallback; -} - -function EmptyThumbnail({ theme, compact = false }: { theme: CanvasTheme; compact?: boolean }): ReactNode { - const sizeClass = compact ? "size-12" : "size-16"; - return ( -
- {compact ? : ( - - - 上传 - - )} +
+
); } -function rowReady(row: CanvasBatchRow, table: CanvasBatchTableData, nodeById: Map) { - if (!row.enabled || !batchPromptForRow(table, row).trim()) return false; - const inputNodeIds = row.inputNodeIds.filter(Boolean); - if (table.operation === "try_on" && inputNodeIds.length < 2) return false; - if (!inputNodeIds.length) return false; - return inputNodeIds.every((id) => hasNodeMedia(nodeById.get(id))); -} - -function hasNodeMedia(node?: CanvasNodeData) { - return Boolean(node?.metadata?.content || node?.metadata?.storageKey); -} - -type RowStatusTone = "success" | "error" | "loading" | "idle"; -type RowStatus = { label: string; tone: RowStatusTone; loading: boolean; retryable: boolean }; - -function rowStatus(item: CanvasGenerationBatchItem | undefined, output: CanvasNodeData | undefined): RowStatus { - if (hasNodeMedia(output)) return { label: "生成完成", tone: "success", loading: false, retryable: false }; - if (item?.status === "failed") return { label: item.errorDetails || "生成失败", tone: "error", loading: false, retryable: true }; - if (item?.status === "cancelled") return { label: "已停止", tone: "error", loading: false, retryable: false }; - if (item && ["waiting", "submitting", "queued", "running"].includes(item.status)) return { label: item.status === "waiting" ? "等待中" : item.status === "submitting" ? "正在提交" : item.status === "queued" ? "已排队" : "生成中", tone: "loading", loading: true, retryable: false }; - if (output?.metadata?.status === "error") return { label: output.metadata.errorDetails || "生成失败", tone: "error", loading: false, retryable: false }; - return { label: "待生成", tone: "idle", loading: false, retryable: false }; +function rowStatus(item: CanvasGenerationBatchItem | undefined, output: CanvasNodeData | undefined) { + if (output?.metadata?.content) return { label: "生成完成", loading: false, retryable: false }; + if (item?.status === "failed") return { label: item.errorDetails || "生成失败", loading: false, retryable: true }; + if (item?.status === "cancelled") return { label: "已停止", loading: false, retryable: false }; + if (item && ["waiting", "submitting", "queued", "running"].includes(item.status)) + return { label: item.status === "waiting" ? "等待中" : item.status === "submitting" ? "正在提交" : item.status === "queued" ? "已排队" : "生成中", loading: true, retryable: false }; + if (output?.metadata?.status === "error") return { label: output.metadata.errorDetails || "生成失败", loading: false, retryable: false }; + return { label: "待生成", loading: false, retryable: false }; } diff --git a/web/src/components/canvas/canvas-cloud-agent-panel.tsx b/web/src/components/canvas/canvas-cloud-agent-panel.tsx index 68c10ed06..3b7d050ef 100644 --- a/web/src/components/canvas/canvas-cloud-agent-panel.tsx +++ b/web/src/components/canvas/canvas-cloud-agent-panel.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type Dispatch, type SetStateAction } from "react"; +import { markdownPlainText } from "@/lib/markdown-plain-text"; import { Button, Dropdown, Input } from "antd"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { ArrowLeft, Bot, Check, ChevronRight, CircleDot, Clock3, Download, History, LoaderCircle, MessageSquarePlus, Settings2, ShieldCheck, Trash2, Sparkles, X } from "lucide-react"; diff --git a/web/src/components/canvas/infinite-canvas.tsx b/web/src/components/canvas/infinite-canvas.tsx index ddf48fa9a..74c5cf342 100644 --- a/web/src/components/canvas/infinite-canvas.tsx +++ b/web/src/components/canvas/infinite-canvas.tsx @@ -30,6 +30,7 @@ type InfiniteCanvasProps = { const CANVAS_WHEEL_IGNORE_SELECTOR = "[data-canvas-no-zoom],[data-canvas-wheel-scroll],.ant-modal,.ant-popover,.ant-dropdown,.ant-select-dropdown,.ant-picker-dropdown"; const CANVAS_POINTER_IGNORE_SELECTOR = "[data-canvas-no-zoom],[data-connection-create-menu],.ant-modal,.ant-popover,.ant-dropdown,.ant-select-dropdown,.ant-picker-dropdown"; +const CANVAS_INTERNAL_DRAG_SELECTOR = "[data-canvas-batch-table]"; const WHEEL_ZOOM_DELTA = 72; const TRACKPAD_PINCH_ZOOM_DELTA = 24; @@ -441,13 +442,38 @@ export function InfiniteCanvas({ interactive = true, containerRef, viewport, app if (!target?.closest("[data-node-id],[data-connection-id],[data-canvas-no-zoom]")) onCanvasDoubleClick?.(event); }} onContextMenu={onContextMenu} - onDragEnter={onFileDragEnter} - onDragLeave={onFileDragLeave} + onDragEnter={(event) => { + if (isCanvasInternalDragEvent(event)) { + event.preventDefault(); + onFileDragEnter?.(event); + return; + } + onFileDragEnter?.(event); + }} + onDragLeave={(event) => { + if (isCanvasInternalDragEvent(event)) { + event.preventDefault(); + onFileDragLeave?.(event); + return; + } + onFileDragLeave?.(event); + }} onDragOver={(event) => { + if (isCanvasInternalDragEvent(event)) { + event.preventDefault(); + onFileDragOver?.(event); + return; + } event.preventDefault(); onFileDragOver?.(event); }} - onDrop={onDrop} + onDrop={(event) => { + if (isCanvasInternalDragEvent(event)) { + event.preventDefault(); + return; + } + onDrop?.(event); + }} > {graphicsLayer} @@ -463,6 +489,11 @@ export function InfiniteCanvas({ interactive = true, containerRef, viewport, app ); } +function isCanvasInternalDragEvent(event: React.DragEvent) { + const target = event.target instanceof Element ? event.target : null; + return Boolean(target?.closest(CANVAS_INTERNAL_DRAG_SELECTOR)); +} + function CanvasGrid({ appearance, mode }: { appearance?: CanvasAppearance; mode: CanvasBackgroundMode }) { const colorTheme = useActiveTheme(); const gridColor = resolveCanvasGridColor(appearance, colorTheme, mode); diff --git a/web/src/constant/canvas.ts b/web/src/constant/canvas.ts index e940927cf..b9c7d721b 100644 --- a/web/src/constant/canvas.ts +++ b/web/src/constant/canvas.ts @@ -27,7 +27,7 @@ export const NODE_DEFAULT_SIZE = { [CanvasNodeType.Chart]: { width: 480, height: 320, title: "图表" }, [CanvasNodeType.ColorGrade]: { width: 420, height: 360, title: "调色" }, [CanvasNodeType.MediaConversion]: { width: 480, height: 460, title: "转换" }, - [CanvasNodeType.BatchTable]: { width: 1280, height: 560, title: "批量创作表" }, + [CanvasNodeType.BatchTable]: { width: 900, height: 520, title: "批量创作表" }, } satisfies Record; export const NODE_SPECS = { diff --git a/web/src/lib/canvas/canvas-batch-table.ts b/web/src/lib/canvas/canvas-batch-table.ts index 9d81df0ae..d7bddfd0b 100644 --- a/web/src/lib/canvas/canvas-batch-table.ts +++ b/web/src/lib/canvas/canvas-batch-table.ts @@ -5,17 +5,82 @@ import type { CanvasBatchOperation, CanvasBatchReferenceColumn, CanvasBatchRow, export const TRY_ON_BATCH_PROMPT = "参考图1是人物原图,参考图2是目标服装。保持人物身份、五官、姿态和背景不变,将人物服装替换为参考图2中的款式。准确还原服装版型、颜色、材质、纹理和装饰细节,穿着关系自然,光影与原图一致。"; export const CREATIVE_BATCH_PROMPT = "基于参考图创作一张新的商业图片,保留主体身份和关键产品细节,画面构图完整,光影自然。"; export const BATCH_REFERENCE_HANDLE_PREFIX = "batch-reference:"; -/** 两行工具栏(操作栏 44 + 全局提示词 36 + 分隔 1)之后,对准表头中线。 */ -export const BATCH_REFERENCE_HANDLE_TOP = 102; -export const BATCH_REFERENCE_HANDLE_GAP = 40; -export const MIN_BATCH_REFERENCE_COLUMNS = 1; +export const BATCH_REFERENCE_HANDLE_TOP = 112; +export const BATCH_REFERENCE_HANDLE_GAP = 38; export const MAX_BATCH_REFERENCE_COLUMNS = 6; -const LEGACY_BATCH_TABLE_WIDTH = 900; +export const MAX_BATCH_TEXT_COLUMNS = 4; +export const BATCH_TEXT_HANDLE_PREFIX = "batch-text:"; -/** 旧默认 900 宽的批量创作表升级到当前默认尺寸,已经手动改过宽度的节点保持原样。 */ -export function promoteLegacyBatchTableSize(node: CanvasNodeData): CanvasNodeData { - if (node.type !== "batch-table" || node.width !== LEGACY_BATCH_TABLE_WIDTH) return node; - return { ...node, width: 1280, height: Math.max(node.height, 560) }; +export function batchGridTemplateColumns(referenceCount: number, textCount: number) { + return [ + "36px", + "68px", + `repeat(${referenceCount}, 88px)`, + textCount > 0 ? `repeat(${textCount}, minmax(168px, 0.75fr))` : undefined, + "minmax(280px, 1fr)", + "148px", + "36px", + ].filter(Boolean).join(" "); +} + +export function reorderBatchReferenceColumns(table: CanvasBatchTableData, fromColumnId: string, toColumnId: string): CanvasBatchTableData { + const columns = batchReferenceColumns(table); + const fromIndex = columns.findIndex((column) => column.id === fromColumnId); + const toIndex = columns.findIndex((column) => column.id === toColumnId); + if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) return table; + + const nextColumns = [...columns]; + const [moved] = nextColumns.splice(fromIndex, 1); + nextColumns.splice(toIndex, 0, moved); + const nextRows = table.rows.map((row) => ({ + ...row, + inputNodeIds: nextColumns.map((column) => { + const oldIndex = columns.findIndex((item) => item.id === column.id); + return row.inputNodeIds[oldIndex] || ""; + }), + })); + + return { + ...table, + referenceColumns: nextColumns.map((column, index) => ({ ...column, label: `参考图 ${index + 1}` })), + rows: nextRows, + }; +} + +export function moveBatchReferenceCell( + table: CanvasBatchTableData, + sourceRowId: string, + sourceColumnIndex: number, + targetRowId: string, + targetColumnIndex: number, +): CanvasBatchTableData { + if (sourceRowId === targetRowId && sourceColumnIndex === targetColumnIndex) return table; + if (sourceColumnIndex < 0 || targetColumnIndex < 0) return table; + + const referenceCount = Math.max(batchReferenceColumns(table).length, sourceColumnIndex + 1, targetColumnIndex + 1); + const sourceRow = table.rows.find((row) => row.id === sourceRowId); + const targetRow = table.rows.find((row) => row.id === targetRowId); + if (!sourceRow || !targetRow) return table; + + const nextRows = table.rows.map((row) => ({ + ...row, + inputNodeIds: Array.from({ length: referenceCount }, (_, index) => row.inputNodeIds[index] || ""), + })); + const nextSourceRow = nextRows.find((row) => row.id === sourceRowId)!; + const nextTargetRow = nextRows.find((row) => row.id === targetRowId)!; + const sourceNodeId = nextSourceRow.inputNodeIds[sourceColumnIndex]; + if (!sourceNodeId) return table; + + const targetNodeId = nextTargetRow.inputNodeIds[targetColumnIndex]; + nextTargetRow.inputNodeIds[targetColumnIndex] = sourceNodeId; + nextSourceRow.inputNodeIds[sourceColumnIndex] = targetNodeId || ""; + + return { ...table, rows: nextRows }; +} + +// Reserve one slot for adding a reference and a separate gap before text inputs. +export function batchTextHandleTop(referenceCount: number) { + return BATCH_REFERENCE_HANDLE_TOP + (referenceCount + 1) * BATCH_REFERENCE_HANDLE_GAP + 24; } export function defaultBatchReferenceColumns(): CanvasBatchReferenceColumn[] { @@ -29,6 +94,10 @@ export function defaultBatchReferenceColumns(): CanvasBatchReferenceColumn[] { export function batchReferenceColumns(table?: CanvasBatchTableData) { const columns = table?.referenceColumns; if (!columns?.length) return defaultBatchReferenceColumns(); + // Older tables were created before the second default slot existed. They + // never had a remove-column action, so extending this exact legacy shape is + // a safe migration and makes reference slot 2 immediately selectable. + if (columns.length === 1 && columns[0].id === "reference-1") return defaultBatchReferenceColumns(); return columns; } @@ -36,16 +105,47 @@ export function batchReferenceHandleId(columnId: string) { return `${BATCH_REFERENCE_HANDLE_PREFIX}${columnId}`; } -export function batchReferenceMentionToken(index: number) { - return `@参考图${index + 1}`; -} - export function batchReferenceColumnId(handleId?: string) { return handleId?.startsWith(BATCH_REFERENCE_HANDLE_PREFIX) ? handleId.slice(BATCH_REFERENCE_HANDLE_PREFIX.length) : undefined; } +export function batchTextColumns(table?: CanvasBatchTableData) { + return table?.textColumns || []; +} + +export function batchTextHandleId(columnId: string) { + return `${BATCH_TEXT_HANDLE_PREFIX}${columnId}`; +} + +export function batchTextColumnId(handleId?: string) { + return handleId?.startsWith(BATCH_TEXT_HANDLE_PREFIX) ? handleId.slice(BATCH_TEXT_HANDLE_PREFIX.length) : undefined; +} + +export function batchTextHandleY(node: CanvasNodeData, handleId?: string) { + if (node.type !== "batch-table") return undefined; + const columnId = batchTextColumnId(handleId); + const columns = batchTextColumns(node.metadata?.batchTable); + const index = columnId ? columns.findIndex((column) => column.id === columnId) : -1; + if (index < 0) return undefined; + return node.position.y + batchTextHandleTop(batchReferenceColumns(node.metadata?.batchTable).length) + index * BATCH_REFERENCE_HANDLE_GAP; +} + +export function batchTextHandleAtY(node: CanvasNodeData, worldY: number, hitRadius = 18) { + if (node.type !== "batch-table") return undefined; + const columns = batchTextColumns(node.metadata?.batchTable); + const referenceCount = batchReferenceColumns(node.metadata?.batchTable).length; + let nearestIndex = -1; + let nearestDistance = Number.POSITIVE_INFINITY; + columns.forEach((_, index) => { + const distance = Math.abs(worldY - (node.position.y + batchTextHandleTop(referenceCount) + index * BATCH_REFERENCE_HANDLE_GAP)); + if (distance < nearestDistance) { nearestDistance = distance; nearestIndex = index; } + }); + return nearestIndex >= 0 && nearestDistance <= hitRadius ? batchTextHandleId(columns[nearestIndex].id) : undefined; +} export function batchReferenceHandleY(node: CanvasNodeData, handleId?: string) { if (node.type !== "batch-table") return undefined; + const textY = batchTextHandleY(node, handleId); + if (textY !== undefined) return textY; const columnId = batchReferenceColumnId(handleId); const columns = batchReferenceColumns(node.metadata?.batchTable); const index = columnId ? columns.findIndex((column) => column.id === columnId) : 0; @@ -55,71 +155,17 @@ export function batchReferenceHandleY(node: CanvasNodeData, handleId?: string) { export function batchReferenceHandleAtY(node: CanvasNodeData, worldY: number, hitRadius = 18) { if (node.type !== "batch-table") return undefined; - const columns = batchReferenceColumns(node.metadata?.batchTable); - let nearestIndex = -1; - let nearestDistance = Number.POSITIVE_INFINITY; - columns.forEach((_, index) => { - const distance = Math.abs(worldY - (node.position.y + BATCH_REFERENCE_HANDLE_TOP + index * BATCH_REFERENCE_HANDLE_GAP)); - if (distance < nearestDistance) { - nearestDistance = distance; - nearestIndex = index; - } - }); - return nearestIndex >= 0 && nearestDistance <= hitRadius ? batchReferenceHandleId(columns[nearestIndex].id) : undefined; -} - -export function removeLastBatchReferenceColumn(table: CanvasBatchTableData): CanvasBatchTableData | null { - const columns = batchReferenceColumns(table); - if (columns.length <= MIN_BATCH_REFERENCE_COLUMNS) return null; - const nextColumns = columns.slice(0, -1).map((column, index) => ({ ...column, label: `参考图 ${index + 1}` })); - return { - ...table, - referenceColumns: nextColumns, - rows: table.rows.map((row) => ({ ...row, inputNodeIds: nextColumns.map((_, index) => row.inputNodeIds[index] || "") })), - }; -} - -export function reorderBatchReferenceColumns(table: CanvasBatchTableData, fromColumnId: string, toColumnId: string): CanvasBatchTableData { - const columns = batchReferenceColumns(table); - const fromIndex = columns.findIndex((column) => column.id === fromColumnId); - const toIndex = columns.findIndex((column) => column.id === toColumnId); - if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) return table; - const nextColumns = [...columns]; - const [moved] = nextColumns.splice(fromIndex, 1); - nextColumns.splice(toIndex, 0, moved); - return { - ...table, - referenceColumns: nextColumns.map((column, index) => ({ ...column, label: `参考图 ${index + 1}` })), - rows: table.rows.map((row) => ({ - ...row, - inputNodeIds: nextColumns.map((column) => { - const oldIndex = columns.findIndex((item) => item.id === column.id); - return row.inputNodeIds[oldIndex] || ""; - }), - })), - }; -} - -export function moveBatchReferenceCell(table: CanvasBatchTableData, sourceRowId: string, sourceColumnIndex: number, targetRowId: string, targetColumnIndex: number): CanvasBatchTableData { - if (sourceRowId === targetRowId && sourceColumnIndex === targetColumnIndex) return table; - if (sourceColumnIndex < 0 || targetColumnIndex < 0) return table; - const referenceCount = Math.max(batchReferenceColumns(table).length, sourceColumnIndex + 1, targetColumnIndex + 1); - const sourceRow = table.rows.find((row) => row.id === sourceRowId); - const targetRow = table.rows.find((row) => row.id === targetRowId); - if (!sourceRow || !targetRow) return table; - const nextRows = table.rows.map((row) => ({ ...row, inputNodeIds: Array.from({ length: referenceCount }, (_, index) => row.inputNodeIds[index] || "") })); - const nextSource = nextRows.find((row) => row.id === sourceRowId)!; - const nextTarget = nextRows.find((row) => row.id === targetRowId)!; - const sourceNodeId = nextSource.inputNodeIds[sourceColumnIndex]; - if (!sourceNodeId) return table; - const targetNodeId = nextTarget.inputNodeIds[targetColumnIndex]; - nextTarget.inputNodeIds[targetColumnIndex] = sourceNodeId; - nextSource.inputNodeIds[sourceColumnIndex] = targetNodeId || ""; - return { ...table, rows: nextRows }; -} - -export function batchPromptForRow(table: CanvasBatchTableData, row: CanvasBatchRow) { - return table.globalPrompt?.trim() || row.prompt; + const referenceColumns = batchReferenceColumns(node.metadata?.batchTable); + const textColumns = batchTextColumns(node.metadata?.batchTable); + const handles = [ + ...referenceColumns.map((column, index) => ({ id: batchReferenceHandleId(column.id), y: BATCH_REFERENCE_HANDLE_TOP + index * BATCH_REFERENCE_HANDLE_GAP })), + ...textColumns.map((column, index) => ({ id: batchTextHandleId(column.id), y: batchTextHandleTop(referenceColumns.length) + index * BATCH_REFERENCE_HANDLE_GAP })), + ]; + const nearest = handles.reduce<{ id: string; distance: number } | null>((current, handle) => { + const distance = Math.abs(worldY - (node.position.y + handle.y)); + return !current || distance < current.distance ? { id: handle.id, distance } : current; + }, null); + return nearest && nearest.distance <= hitRadius ? nearest.id : undefined; } export function batchInputColumns(node: CanvasNodeData, connections: CanvasConnection[]) { @@ -135,20 +181,34 @@ export function batchInputColumns(node: CanvasNodeData, connections: CanvasConne return result; } +export function batchTextInputColumns(node: CanvasNodeData, connections: CanvasConnection[]) { + const columns = batchTextColumns(node.metadata?.batchTable); + const indexById = new Map(columns.map((column, index) => [column.id, index])); + const result = columns.map(() => [] as string[]); + connections.filter((connection) => connection.toNodeId === node.id && connection.relation !== "batch-output").forEach((connection) => { + const columnId = batchTextColumnId(connection.toHandleId); + const index = columnId ? indexById.get(columnId) : undefined; + if (index === undefined || result[index].includes(connection.fromNodeId)) return; + result[index].push(connection.fromNodeId); + }); + return result; +} + +/** A non-empty global prompt takes precedence without destroying row-specific prompts. */ +export function batchPromptForRow(table: CanvasBatchTableData, row: CanvasBatchRow) { + return table.globalPrompt?.trim() || row.prompt; +} + export function createBatchRow(operation: CanvasBatchOperation, inputNodeIds: string[] = []): CanvasBatchRow { return { id: `batch-row-${nanoid()}`, enabled: true, inputNodeIds, + textNodeIds: [], prompt: operation === "try_on" ? TRY_ON_BATCH_PROMPT : CREATIVE_BATCH_PROMPT, }; } -/** 手工追加任务时沿用上一行的参考图,但重新使用当前任务类型的默认提示词。 */ -export function createInheritedBatchRow(operation: CanvasBatchOperation, rows: CanvasBatchRow[]) { - return createBatchRow(operation, [...(rows.at(-1)?.inputNodeIds || [])]); -} - /** * Connected try-on inputs follow the reference layout: all model/person images first, * followed by one shared garment image. Users can still edit any row afterwards. @@ -160,34 +220,32 @@ export function createBatchRowsFromInputs(operation: CanvasBatchOperation, input return inputNodeIds.slice(0, -1).map((personId) => createBatchRow(operation, [personId, garmentId])); } -/** - * Zip equally sized columns and broadcast singleton columns across every row. - * - * 同步连线是增量操作:匹配到的行更新参考图,未被本次连线覆盖的手工行继续保留。 - * 这样用户在表格内追加或编辑的任务不会因为再次点击“同步连线”而被删除。 - */ +/** Zip equally sized columns and broadcast singleton columns across every row. */ export function createBatchRowsFromColumns(operation: CanvasBatchOperation, columns: string[][], previousRows: CanvasBatchRow[] = []) { const rowCount = Math.max(0, ...columns.map((column) => column.length)); - const unmatchedRows = [...previousRows]; - const synchronizedRows = Array.from({ length: rowCount }, (_, rowIndex) => { + const previousByPrimary = new Map(previousRows.flatMap((row) => row.inputNodeIds[0] ? [[row.inputNodeIds[0], row] as const] : [])); + return Array.from({ length: rowCount }, (_, rowIndex) => { const inputNodeIds = columns.flatMap((column) => { const input = column.length === 1 ? column[0] : column[rowIndex]; return input ? [input] : []; }); - let previousIndex = unmatchedRows.findIndex((row) => sameBatchInputs(row.inputNodeIds, inputNodeIds)); - if (previousIndex < 0 && inputNodeIds[0]) previousIndex = unmatchedRows.findIndex((row) => row.inputNodeIds[0] === inputNodeIds[0]); - const previous = previousIndex >= 0 ? unmatchedRows.splice(previousIndex, 1)[0] : undefined; + const previous = inputNodeIds[0] ? previousByPrimary.get(inputNodeIds[0]) : undefined; const inputsUnchanged = previous && previous.inputNodeIds.length === inputNodeIds.length && previous.inputNodeIds.every((id, index) => id === inputNodeIds[index]); return { ...createBatchRow(operation, inputNodeIds), - ...(previous ? { id: previous.id, enabled: previous.enabled, prompt: previous.prompt } : {}), + ...(previous ? { id: previous.id, enabled: previous.enabled, prompt: previous.prompt, textNodeIds: previous.textNodeIds } : {}), ...(inputsUnchanged && previous?.outputNodeId ? { outputNodeId: previous.outputNodeId } : {}), inputNodeIds, }; }); - return [...synchronizedRows, ...unmatchedRows]; } -function sameBatchInputs(left: string[], right: string[]) { - return left.length === right.length && left.every((id, index) => id === right[index]); +export function assignBatchTextRows(rows: CanvasBatchRow[], columns: string[][]) { + return rows.map((row, rowIndex) => ({ + ...row, + textNodeIds: columns.flatMap((column) => { + const input = column.length === 1 ? column[0] : column[rowIndex]; + return input ? [input] : []; + }), + })); } diff --git a/web/src/lib/canvas/node-registry/definitions/builtin-nodes.tsx b/web/src/lib/canvas/node-registry/definitions/builtin-nodes.tsx index 83f766db8..214a8016c 100644 --- a/web/src/lib/canvas/node-registry/definitions/builtin-nodes.tsx +++ b/web/src/lib/canvas/node-registry/definitions/builtin-nodes.tsx @@ -154,7 +154,14 @@ const BUILTIN_NODE_TRAITS = { showInCreateMenu: true, inputKind: "text", }, - [CanvasNodeType.BatchTable]: { label: "批量创作表", icon: , minSize: { width: 1080, height: 420 }, showInCreateMenu: true, acceptsInputKind: "image", inputKind: "text", resourceKind: () => null }, + [CanvasNodeType.BatchTable]: { + label: "批量创作表", + icon: , + minSize: { width: 720, height: 360 }, + showInCreateMenu: true, + acceptsInputKind: ["image", "text"], + inputKind: "text", + }, [CanvasNodeType.MediaConversion]: { label: "转换", icon: , minSize: { width: 400, height: 360 }, showInCreateMenu: true, resourceKind: (node: CanvasNodeData) => { const c = node.metadata?.mediaConversion; return c?.status === "completed" && c.resultStorageKey ? (c.outputKind === "video" ? "video" : "image") : null; }, acceptsInputKind: ["image", "video"], maxInputCount: 1, inputKind: "image" }, [CanvasNodeType.ColorGrade]: { label: "调色", diff --git a/web/src/lib/user-session.ts b/web/src/lib/user-session.ts index fa7d6cdf2..e9a873930 100644 --- a/web/src/lib/user-session.ts +++ b/web/src/lib/user-session.ts @@ -149,7 +149,7 @@ export function systemChannelModelChannels(channels: PublicChannelCatalog[]): Mo sortOrder: channel.sortOrder, // 系统渠道必须走带渠道 ID 的站内代理;/api 只是业务 API 根路径, // 不能作为模型请求的运行时 Base URL 传给 channelRequest。 - baseUrl: `/api/${channel.id}`, + baseUrl: `/api/ai/system/${channel.id}`, apiKey: "system", apiFormat: "openai", scope: "system" as const, @@ -178,7 +178,7 @@ export function systemChannelModelChannels(channels: PublicChannelCatalog[]): Mo return { model: model.modelKey, displayName: model.displayName, - description: "", + description: model.description || "", icon: model.icon || "", capability: model.capability as ModelCapability, protocol: model.protocol as any, diff --git a/web/src/pages/admin/components/channel-model-editor-form.ts b/web/src/pages/admin/components/channel-model-editor-form.ts index 8c3488cc0..bda6f71d5 100644 --- a/web/src/pages/admin/components/channel-model-editor-form.ts +++ b/web/src/pages/admin/components/channel-model-editor-form.ts @@ -9,6 +9,7 @@ export type ChannelModelFormValues = { providerModelKey?: string; displayName?: string; icon?: string; + description?: string; capability: ModelCapabilityChoice; protocol?: string; priceTiers: PriceTierFormValues[]; @@ -31,6 +32,7 @@ export function initialChannelModelValues(item: ChannelModel | null, protocols: providerModelKey: upstreamModel, displayName: item?.displayName || "", icon: item?.icon || "", + description: item?.description || "", capability, protocol, priceTiers: item ? (item.priceTiers?.length ? item.priceTiers.map(priceTierToForm) : [legacyPriceTierToForm(item)]) : [defaultPriceTier()], diff --git a/web/src/pages/admin/components/channel-model-editor.tsx b/web/src/pages/admin/components/channel-model-editor.tsx index b9ecca305..f0b198570 100644 --- a/web/src/pages/admin/components/channel-model-editor.tsx +++ b/web/src/pages/admin/components/channel-model-editor.tsx @@ -107,6 +107,7 @@ export function ChannelModelEditor({ providerModelKey: upstreamModel, displayName: values.displayName?.trim() || values.modelKey.trim(), icon: values.icon?.trim() || "", + description: values.description?.trim() || "", capability: values.capability, protocol: values.protocol, priceTiers: values.priceTiers.map((tier) => ({ @@ -263,6 +264,9 @@ export function ChannelModelEditor({ + + +
{tiersWithOwnUpstream.length ? ( { if (!projectLoaded) return; - setNodes((current) => { - let changed = false; - const next = current.map((node) => { - const promoted = promoteLegacyBatchTableSize(node); - if (promoted !== node) changed = true; - return promoted; - }); - return changed ? next : current; - }); nodesRef.current.filter((node) => node.type === CanvasNodeType.BatchTable).forEach((node) => { syncRowsFromConnections(node.id, true); }); - }, [connections, projectLoaded, setNodes, syncRowsFromConnections]); + }, [connections, projectLoaded, syncRowsFromConnections]); + + const handleUploadBatchReference = useCallback(async (tableNodeId: string, rowId: string, columnIndex: number, file: File) => { + const tableNode = nodesRef.current.find((item) => item.id === tableNodeId); + const table = tableNode?.metadata?.batchTable; + const row = table?.rows.find((item) => item.id === rowId); + if (!tableNode || !table || !row) return; + const rowIndex = table.rows.findIndex((item) => item.id === rowId); + const uploadedNodeId = await createFileNode(file, { + x: tableNode.position.x - 240, + y: tableNode.position.y + 170 + Math.max(0, rowIndex) * 220, + }); + if (!uploadedNodeId) return; + const inputNodeIds = Array.from({ length: Math.max(batchReferenceColumns(table).length, columnIndex + 1) }, (_, index) => row.inputNodeIds[index] || ""); + inputNodeIds[columnIndex] = uploadedNodeId; + updateBatchRow(tableNodeId, rowId, { inputNodeIds }); + message.success(`已上传并填入参考图 ${columnIndex + 1}`); + }, [createFileNode, message, nodesRef, updateBatchRow]); const { addScriptRow, createAndGenerateScriptVideos, createScriptActionBoards, createScriptImageNodes, createScriptVideoNodes, generateScriptImages, generateScriptRows, generateScriptVideos, removeScriptRow, replaceScriptRows, updateScriptRow } = useCanvasStoryboard({ @@ -2123,26 +2131,11 @@ function InfiniteCanvasPage() { onGenerate={(rowIds) => void generateBatchRows(contentNode.id, rowIds)} onRetryItem={(batchId, itemId) => retryFailedBatchItems(contentNode.id, batchId, itemId)} onAddReferenceColumn={() => addBatchReferenceColumn(contentNode.id)} - onRemoveReferenceColumn={() => removeBatchReferenceColumn(contentNode.id)} - onFocusOutput={(nodeId) => focusCanvasImageNode(nodeId)} + onAddTextColumn={() => addBatchTextColumn(contentNode.id)} onReorderReferenceColumns={(fromColumnId, toColumnId) => reorderBatchReferenceColumns(contentNode.id, fromColumnId, toColumnId)} onMoveReferenceCell={(sourceRowId, sourceColumnIndex, targetRowId, targetColumnIndex) => moveBatchReferenceCell(contentNode.id, sourceRowId, sourceColumnIndex, targetRowId, targetColumnIndex)} - onUploadReference={(rowId, columnIndex, file) => { - const row = contentNode.metadata?.batchTable?.rows.find((item) => item.id === rowId); - const existingId = row?.inputNodeIds[columnIndex]; - if (existingId) { - void replaceNodeMedia(existingId, file); - return; - } - void createFileNode(file, { x: contentNode.position.x - 180, y: contentNode.position.y + columnIndex * 90 }).then((insertedId) => { - if (!insertedId) return; - const currentRow = nodesRef.current.find((item) => item.id === contentNode.id)?.metadata?.batchTable?.rows.find((item) => item.id === rowId); - const ids = [...(currentRow?.inputNodeIds || [])]; - while (ids.length <= columnIndex) ids.push(""); - ids[columnIndex] = insertedId; - updateBatchRow(contentNode.id, rowId, { inputNodeIds: ids }); - }); - }} + onReplaceReference={replaceCanvasNodeMedia} + onUploadReference={(rowId, columnIndex, file) => { void handleUploadBatchReference(contentNode.id, rowId, columnIndex, file); }} onConnectStart={(event, handleId) => handleConnectStart(event, contentNode.id, "target", handleId)} onConnectDrop={(event, handleId) => handleConnectDrop(event, contentNode.id, handleId)} /> @@ -2217,6 +2210,7 @@ function InfiniteCanvasPage() { }, [ addBatchReferenceColumn, + addBatchTextColumn, addBatchRow, addScriptRow, configInputsById, @@ -2227,11 +2221,12 @@ function InfiniteCanvasPage() { createScriptVideoNodes, currentProject?.directorScenes, fillRowsFromConnections, - focusCanvasImageNode, generateBatchRows, generateScriptImages, generateScriptRows, generateScriptVideos, + handleUploadBatchReference, + moveBatchReferenceCell, handleConfigNodeChange, handleConnectDrop, handleConnectStart, @@ -2242,8 +2237,9 @@ function InfiniteCanvasPage() { openDirectorWorkbench, openStoryInput, patchBatchTable, - removeBatchReferenceColumn, removeBatchRow, + reorderBatchReferenceColumns, + replaceCanvasNodeMedia, removeScriptRow, retryFailedBatchItems, runningNodeId, diff --git a/web/src/pages/canvas/shared.tsx b/web/src/pages/canvas/shared.tsx index d88ac716d..07cd4e02c 100644 --- a/web/src/pages/canvas/shared.tsx +++ b/web/src/pages/canvas/shared.tsx @@ -238,7 +238,7 @@ export default function SharedCanvasPage() { })); const renderSharedNode = useCallback((node: CanvasNodeData): ReactNode => { if (node.type === CanvasNodeType.Script) return ; - if (node.type === CanvasNodeType.BatchTable) return {}} onAddRow={() => {}} onRemoveRow={() => {}} onUpdateRow={() => {}} onFillRows={() => {}} onGenerate={() => {}} onRetryItem={() => {}} onAddReferenceColumn={() => {}} onConnectStart={() => {}} />; + if (node.type === CanvasNodeType.BatchTable) return {}} onAddRow={() => {}} onRemoveRow={() => {}} onUpdateRow={() => {}} onFillRows={() => {}} onGenerate={() => {}} onRetryItem={() => {}} onAddReferenceColumn={() => {}} onAddTextColumn={() => {}} onReorderReferenceColumns={() => {}} onMoveReferenceCell={() => {}} onReplaceReference={() => {}} onUploadReference={() => {}} onConnectStart={() => {}} />; return ; }, [connections, nodes, theme, unauthorized]); const toolbarNodeKey = selectedNodeId; diff --git a/web/src/pages/canvas/use-canvas-batch-table.ts b/web/src/pages/canvas/use-canvas-batch-table.ts index 8c9090641..439709759 100644 --- a/web/src/pages/canvas/use-canvas-batch-table.ts +++ b/web/src/pages/canvas/use-canvas-batch-table.ts @@ -3,7 +3,7 @@ import { App } from "antd"; import { nanoid } from "nanoid"; import { NODE_DEFAULT_SIZE } from "@/constant/canvas"; -import { batchInputColumns, batchPromptForRow, batchReferenceColumns, batchReferenceHandleId, createBatchRowsFromColumns, createInheritedBatchRow, moveBatchReferenceCell, removeLastBatchReferenceColumn, reorderBatchReferenceColumns } from "@/lib/canvas/canvas-batch-table"; +import { batchInputColumns, batchPromptForRow, batchReferenceColumns, batchTextInputColumns, createBatchRow, createBatchRowsFromColumns, moveBatchReferenceCell, reorderBatchReferenceColumns } from "@/lib/canvas/canvas-batch-table"; import { createCanvasNode } from "@/lib/canvas/canvas-project-domain"; import { buildGenerationConfig, resetGenerationTaskMetadata } from "@/lib/canvas/canvas-project-generation"; import { navigateToSettings } from "@/lib/settings-navigation"; @@ -25,7 +25,7 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon const isAiConfigReady = useConfigStore((state) => state.isAiConfigReady); const patchTable = useCallback((nodeId: string, patch: Partial) => { - setNodes((current) => current.map((node) => node.id !== nodeId ? node : { ...node, metadata: { ...node.metadata, batchTable: { operation: "try_on", concurrency: 10, rows: [], ...node.metadata?.batchTable, ...patch } } })); + setNodes((current) => current.map((node) => node.id !== nodeId ? node : { ...node, metadata: { ...node.metadata, batchTable: { operation: "try_on", concurrency: 3, rows: [], ...node.metadata?.batchTable, ...patch } } })); }, [setNodes]); const updateRow = useCallback((nodeId: string, rowId: string, patch: Partial) => { @@ -37,7 +37,7 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon const addRow = useCallback((nodeId: string) => { const table = nodesRef.current.find((item) => item.id === nodeId)?.metadata?.batchTable; if (!table) return; - patchTable(nodeId, { rows: [...table.rows, createInheritedBatchRow(table.operation, table.rows)] }); + patchTable(nodeId, { rows: [...table.rows, createBatchRow(table.operation)] }); }, [nodesRef, patchTable]); const removeRow = useCallback((nodeId: string, rowId: string) => { @@ -55,19 +55,13 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon patchTable(nodeId, { referenceColumns: [...columns, { id: `reference-${nanoid()}`, label: `参考图 ${nextIndex}` }] }); }, [message, nodesRef, patchTable]); - const removeReferenceColumn = useCallback((nodeId: string) => { + const addTextColumn = useCallback((nodeId: string) => { const table = nodesRef.current.find((item) => item.id === nodeId)?.metadata?.batchTable; if (!table) return; - const columns = batchReferenceColumns(table); - const nextTable = removeLastBatchReferenceColumn(table); - if (!nextTable) return message.info("至少保留 1 组参考图"); - const removed = columns.at(-1); - patchTable(nodeId, nextTable); - if (removed) { - const handleId = batchReferenceHandleId(removed.id); - setConnections((current) => current.filter((connection) => !(connection.toNodeId === nodeId && connection.toHandleId === handleId))); - } - }, [message, nodesRef, patchTable, setConnections]); + const columns = table.textColumns || []; + if (columns.length >= 4) return message.info("最多支持 4 组文字"); + patchTable(nodeId, { textColumns: [...columns, { id: `text-${nanoid()}`, label: `文字 ${columns.length + 1}` }] }); + }, [message, nodesRef, patchTable]); const syncRowsFromConnections = useCallback((nodeId: string, silent = false) => { const node = nodesRef.current.find((item) => item.id === nodeId); @@ -83,16 +77,25 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon return false; } const rows = createBatchRowsFromColumns(table.operation, columns, table.rows); + const textColumns = batchTextInputColumns(node, connectionsRef.current).map((column) => column.filter((inputNodeId) => { + const input = nodeById.get(inputNodeId); + return input?.type === CanvasNodeType.Text && Boolean(input.metadata?.content || input.metadata?.prompt); + })); + const rowsWithText = rows.map((row, index) => ({ + ...row, + textNodeIds: textColumns.flatMap((column) => { + const input = column.length === 1 ? column[0] : column[index]; + return input ? [input] : []; + }), + })); if (!rows.length) { if (!silent) message.warning("批量换装至少需要一张人物图和一张服装图"); return false; } - if (JSON.stringify(rows) === JSON.stringify(table.rows)) return true; - patchTable(nodeId, { rows }); - if (!silent) { - const added = Math.max(0, rows.length - table.rows.length); - message.success(added ? `已同步连线并新增 ${added} 行,原有任务均已保留` : "已同步最新连线,原有任务均已保留"); - } + const rowsChanged = JSON.stringify(table.rows) !== JSON.stringify(rowsWithText); + if (!rowsChanged) return false; + patchTable(nodeId, { rows: rowsWithText }); + if (!silent) message.success(`已按连线创建 ${rows.length} 行任务`); return true; }, [connectionsRef, message, nodesRef, patchTable]); @@ -126,15 +129,11 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon const activeNodeIds = new Set((sourceNode.metadata?.generationBatches || []).filter((batch) => batch.mode === "batch_image").flatMap((batch) => batch.items.filter((item) => ["waiting", "submitting", "queued", "running"].includes(item.status)).map((item) => item.nodeId))); const requested = requestedRowIds?.length ? new Set(requestedRowIds) : null; const rows = table.rows.filter((row) => { - const inputNodeIds = row.inputNodeIds.filter(Boolean); - if (!row.enabled || (requested && !requested.has(row.id)) || !batchPromptForRow(table, row).trim()) return false; - if (table.operation === "try_on" && inputNodeIds.length < 2) return false; - if (!inputNodeIds.length || inputNodeIds.some((id) => !nodesRef.current.some((node) => node.id === id && node.type === CanvasNodeType.Image && Boolean(node.metadata?.content || node.metadata?.storageKey)))) return false; + if (row.enabled === false || (requested && !requested.has(row.id)) || !batchPromptForRow(table, row).trim()) return false; + if (table.operation === "try_on" && row.inputNodeIds.length < 2) return false; + if (!row.inputNodeIds.length || row.inputNodeIds.some((id) => !nodesRef.current.some((node) => node.id === id && node.type === CanvasNodeType.Image && Boolean(node.metadata?.content || node.metadata?.storageKey)))) return false; const output = row.outputNodeId ? nodesRef.current.find((node) => node.id === row.outputNodeId) : undefined; - if (output && activeNodeIds.has(output.id)) return false; - // 操作列的单行生成允许对已完成结果重新生成;顶部批量按钮仍只提交未完成项。 - if (requested) return true; - return !output?.metadata?.content; + return !output?.metadata?.content && (!output || !activeNodeIds.has(output.id)); }); if (!rows.length) return message.info("没有可提交的未完成任务,请检查参考图和提示词"); const confirmed = await new Promise((resolve) => modal.confirm({ @@ -159,7 +158,10 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon const metadata = { ...(existingIndex >= 0 ? resetGenerationTaskMetadata(nextNodes[existingIndex].metadata) : {}), prompt, - composerContent: prompt, + composerContent: [prompt, ...(row.textNodeIds || []).map((textNodeId) => { + const textNode = nodesRef.current.find((node) => node.id === textNodeId); + return textNode?.metadata?.content || textNode?.metadata?.prompt || ""; + })].filter(Boolean).join("\n\n"), model: buildGenerationConfig(effectiveConfig, undefined, "image").model, generationMode: "image" as const, generationType: "edit" as const, @@ -178,7 +180,7 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon if (existingIndex >= 0) nextNodes[existingIndex] = output; else nextNodes.push(output); nextConnections = nextConnections.filter((connection) => connection.toNodeId !== output.id); - row.inputNodeIds.filter(Boolean).forEach((inputNodeId) => nextConnections.push({ id: nanoid(), fromNodeId: inputNodeId, toNodeId: output.id })); + row.inputNodeIds.forEach((inputNodeId) => nextConnections.push({ id: nanoid(), fromNodeId: inputNodeId, toNodeId: output.id })); nextConnections.push({ id: nanoid(), fromNodeId: sourceNode.id, toNodeId: output.id, relation: "batch-output", storyboardRowId: row.id }); outputByRowId.set(row.id, output.id); targets.push({ rowId: row.id, nodeId: output.id }); @@ -193,5 +195,5 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon if (enqueueGenerationBatch(nodeId, "batch_image", targets, { concurrency: table.concurrency })) message.success(`${targets.length} 个任务已加入并发队列`); }, [connectionsRef, effectiveConfig, enqueueGenerationBatch, isAiConfigReady, message, modal, nodesRef, setConnections, setNodes, setSelectedNodeIds]); - return { addReferenceColumn, addRow, fillRowsFromConnections, generateRows, moveReferenceCell, patchTable, removeReferenceColumn, removeRow, reorderReferenceColumns, syncRowsFromConnections, updateRow }; + return { addReferenceColumn, addTextColumn, addRow, fillRowsFromConnections, generateRows, moveReferenceCell, patchTable, removeRow, reorderReferenceColumns, syncRowsFromConnections, updateRow }; } diff --git a/web/src/pages/canvas/use-canvas-upload.ts b/web/src/pages/canvas/use-canvas-upload.ts index facd22987..2a0202e9b 100644 --- a/web/src/pages/canvas/use-canvas-upload.ts +++ b/web/src/pages/canvas/use-canvas-upload.ts @@ -46,7 +46,6 @@ const NODE_STATUS_SUCCESS = "success" as const; const BATCH_UPLOAD_COLUMNS = 3; const BATCH_UPLOAD_COLUMN_GAP = 380; const BATCH_UPLOAD_ROW_GAP = 300; - const CANVAS_BATCH_TABLE_SELECTOR = "[data-canvas-batch-table]"; function isBatchTableDragEvent(event: DragEvent) { @@ -544,11 +543,13 @@ export function useCanvasUpload({ const handleDrop = useCallback((event: DragEvent) => { if (isBatchTableDragEvent(event)) { event.preventDefault(); + event.stopPropagation(); fileDragDepthRef.current = 0; setFileDropActive(false); return; } event.preventDefault(); + event.stopPropagation(); fileDragDepthRef.current = 0; setFileDropActive(false); const chapterPayload = parseProjectChapterPayload(event.dataTransfer.getData(CANVAS_PROJECT_CHAPTER_DND_TYPE)); @@ -590,12 +591,14 @@ export function useCanvasUpload({ const handleFileDragEnter = useCallback((event: DragEvent) => { if (isBatchTableDragEvent(event)) { event.preventDefault(); + event.stopPropagation(); fileDragDepthRef.current = 0; setFileDropActive(false); return; } if (!hasDraggedFiles(event)) return; event.preventDefault(); + event.stopPropagation(); fileDragDepthRef.current += 1; setFileDropActive(true); }, []); @@ -603,18 +606,21 @@ export function useCanvasUpload({ const handleFileDragOver = useCallback((event: DragEvent) => { if (isBatchTableDragEvent(event)) { event.preventDefault(); + event.stopPropagation(); fileDragDepthRef.current = 0; setFileDropActive(false); return; } if (!hasDraggedFiles(event) && !Array.from(event.dataTransfer.types).includes(CANVAS_PROJECT_CHAPTER_DND_TYPE)) return; event.preventDefault(); + event.stopPropagation(); event.dataTransfer.dropEffect = "copy"; }, []); const handleFileDragLeave = useCallback((event: DragEvent) => { if (isBatchTableDragEvent(event)) { event.preventDefault(); + event.stopPropagation(); fileDragDepthRef.current = 0; setFileDropActive(false); return; @@ -732,6 +738,7 @@ export function useCanvasUpload({ return { assetPickerOpen, closeAssetPicker, + createFileNode, createVideoNodeFromBlob, createAssetPayloadNode, createImageAssetNode, @@ -752,7 +759,6 @@ export function useCanvasUpload({ pasteAssistantImage, pasteSystemClipboard, replaceNodeMedia, - createFileNode, startUploadStatus, uploadModalOpen, uploadStatus, diff --git a/web/src/services/api/logical-models.ts b/web/src/services/api/logical-models.ts index 3ac20568c..eea4cbc20 100644 --- a/web/src/services/api/logical-models.ts +++ b/web/src/services/api/logical-models.ts @@ -150,6 +150,7 @@ export type PublicChannelModel = { displayName: string; sortOrder?: number; icon: string; + description?: string; capability: string; protocol?: string; capabilityConfig?: Record; diff --git a/web/src/services/api/wallet.ts b/web/src/services/api/wallet.ts index 7d51f2aef..253ca473d 100644 --- a/web/src/services/api/wallet.ts +++ b/web/src/services/api/wallet.ts @@ -54,6 +54,7 @@ export type ChannelModel = { displayName: string; sortOrder?: number; icon: string; + description?: string; capability: "text" | "image" | "video" | "audio" | ""; protocol?: import("@/lib/model-protocols").ModelProtocol; billingMode: "fixed_request" | "per_second" | "token"; @@ -97,6 +98,7 @@ export type ChannelModelMutation = { providerModelKey?: string; displayName?: string; icon?: string; + description?: string; capability: ChannelModel["capability"]; protocol?: ChannelModel["protocol"]; enabled?: boolean; diff --git a/web/src/types/canvas.ts b/web/src/types/canvas.ts index 1ad0dc59e..89e3f36e6 100644 --- a/web/src/types/canvas.ts +++ b/web/src/types/canvas.ts @@ -164,9 +164,28 @@ export type CanvasGenerationBatch = { }; export type CanvasBatchOperation = "try_on" | "creative"; -export type CanvasBatchRow = { id: string; enabled: boolean; inputNodeIds: string[]; prompt: string; outputNodeId?: string }; -export type CanvasBatchReferenceColumn = { id: string; label: string }; -export type CanvasBatchTableData = { operation: CanvasBatchOperation; concurrency: number; globalPrompt?: string; referenceColumns?: CanvasBatchReferenceColumn[]; rows: CanvasBatchRow[] }; +export type CanvasBatchRow = { + id: string; + enabled: boolean; + inputNodeIds: string[]; + /** Text nodes selected for this row; their contents are appended to prompt. */ + textNodeIds?: string[]; + prompt: string; + outputNodeId?: string; +}; +export type CanvasBatchReferenceColumn = { + id: string; + label: string; +}; +export type CanvasBatchTableData = { + operation: CanvasBatchOperation; + concurrency: number; + /** Optional prompt override applied to every batch row while non-empty. */ + globalPrompt?: string; + referenceColumns?: CanvasBatchReferenceColumn[]; + textColumns?: CanvasBatchReferenceColumn[]; + rows: CanvasBatchRow[]; +}; export type CanvasSkillSnapshot = { id: string; diff --git a/web/test/canvas-batch-connection.test.ts b/web/test/canvas-batch-connection.test.ts index 6d501cd4d..7cfd8efc8 100644 --- a/web/test/canvas-batch-connection.test.ts +++ b/web/test/canvas-batch-connection.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from "bun:test"; import { buildBatchConnectionCreateRequest, hasBatchConnectionCandidate, planBatchConnections } from "@/lib/canvas/canvas-batch-connection"; -import { canvasConnectionError } from "@/lib/canvas/canvas-connection-policy"; import { canvasConnectionPath } from "@/components/canvas/canvas-connections"; import { defaultConfig } from "@/stores/use-config-store"; import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "@/types/canvas"; @@ -20,11 +19,6 @@ const nodes: CanvasNodeData[] = [ const baseConfig = { ...defaultConfig }; describe("planBatchConnections", () => { - it("only accepts image inputs for the batch creation table", () => { - expect(canvasConnectionError(baseConfig, nodes, [], { fromNodeId: "image-a", toNodeId: "batch-table" })).toBe(""); - expect(canvasConnectionError(baseConfig, nodes, [], { fromNodeId: "text-a", toNodeId: "batch-table" })).toContain("批量创作表节点只接受图片输入"); - }); - it("plans all legal source nodes and preserves the target handle", () => { const result = planBatchConnections({ sourceNodeIds: ["text-a", "text-b"], diff --git a/web/test/canvas-batch-table.test.ts b/web/test/canvas-batch-table.test.ts index 8331eb070..997326d7d 100644 --- a/web/test/canvas-batch-table.test.ts +++ b/web/test/canvas-batch-table.test.ts @@ -1,101 +1,95 @@ -import { describe, expect, test } from "bun:test"; - -import { - batchPromptForRow, - batchReferenceColumns, - batchReferenceHandleAtY, - batchReferenceHandleId, - batchReferenceMentionToken, - createBatchRowsFromColumns, - createBatchRowsFromInputs, - createInheritedBatchRow, - moveBatchReferenceCell, - promoteLegacyBatchTableSize, - removeLastBatchReferenceColumn, - reorderBatchReferenceColumns, - BATCH_REFERENCE_HANDLE_GAP, - BATCH_REFERENCE_HANDLE_TOP, - TRY_ON_BATCH_PROMPT, -} from "@/lib/canvas/canvas-batch-table"; -import { createCanvasNode } from "@/lib/canvas/canvas-project-domain"; -import { CanvasNodeType } from "@/types/canvas"; - -describe("batch creation table", () => { - test("creates one try-on row per person with the final image as the shared garment", () => { - const rows = createBatchRowsFromInputs("try_on", ["person-1", "person-2", "person-3", "garment"]); - - expect(rows.map((row) => row.inputNodeIds)).toEqual([ - ["person-1", "garment"], - ["person-2", "garment"], - ["person-3", "garment"], - ]); - expect(rows.every((row) => row.prompt === TRY_ON_BATCH_PROMPT)).toBe(true); - }); - - test("creates one creative row per connected image", () => { - expect(createBatchRowsFromInputs("creative", ["a", "b"]).map((row) => row.inputNodeIds)).toEqual([["a"], ["b"]]); - }); - - test("broadcasts a singleton reference column across rows", () => { - expect(createBatchRowsFromColumns("try_on", [["person-1", "person-2"], ["garment"]]).map((row) => row.inputNodeIds)).toEqual([ - ["person-1", "garment"], - ["person-2", "garment"], - ]); - }); - - test("inherits the previous row references when adding a manual task", () => { - const row = createInheritedBatchRow("try_on", [{ id: "row-1", enabled: true, inputNodeIds: ["person-1", "garment"], prompt: "自定义提示词" }]); - - expect(row.inputNodeIds).toEqual(["person-1", "garment"]); - expect(row.inputNodeIds).not.toBe(createInheritedBatchRow("try_on", [{ id: "row-1", enabled: true, inputNodeIds: ["person-1", "garment"], prompt: "自定义提示词" }]).inputNodeIds); - expect(row.prompt).toBe(TRY_ON_BATCH_PROMPT); - }); - - test("syncing connections preserves unmatched manual rows", () => { - const manual = { id: "manual-row", enabled: false, inputNodeIds: ["manual-image"], prompt: "保留手工任务" }; - const rows = createBatchRowsFromColumns("try_on", [["person-1"], ["garment"]], [manual]); - - expect(rows).toHaveLength(2); - expect(rows[1]).toEqual(manual); - }); - - test("syncing unchanged references keeps the row identity and linked output", () => { - const existing = { id: "row-1", enabled: true, inputNodeIds: ["person-1", "garment"], prompt: "保留提示词", outputNodeId: "output-1" }; - const [row] = createBatchRowsFromColumns("try_on", [["person-1"], ["garment"]], [existing]); - - expect(row).toEqual(existing); - }); - - test("uses stable prompt mention tokens for positional references", () => { - expect(batchReferenceMentionToken(0)).toBe("@参考图1"); - expect(batchReferenceMentionToken(1)).toBe("@参考图2"); - }); - - test("new canvas node has durable batch defaults", () => { +import { describe, expect, test } from "bun:test"; + +import { batchGridTemplateColumns, batchPromptForRow, batchReferenceHandleAtY, batchReferenceHandleId, batchTextHandleId, batchTextHandleTop, createBatchRowsFromColumns, createBatchRowsFromInputs, CREATIVE_BATCH_PROMPT, moveBatchReferenceCell, reorderBatchReferenceColumns, TRY_ON_BATCH_PROMPT } from "@/lib/canvas/canvas-batch-table"; +import { createCanvasNode } from "@/lib/canvas/canvas-project-domain"; +import { CanvasNodeType } from "@/types/canvas"; + +describe("batch creation table", () => { + test("creates one try-on row per person with the final image as the shared garment", () => { + const rows = createBatchRowsFromInputs("try_on", ["person-1", "person-2", "person-3", "garment"]); + + expect(rows.map((row) => row.inputNodeIds)).toEqual([ + ["person-1", "garment"], + ["person-2", "garment"], + ["person-3", "garment"], + ]); + expect(rows.every((row) => row.prompt === TRY_ON_BATCH_PROMPT)).toBe(true); + }); + + test("creates one creative row per connected image", () => { + expect(createBatchRowsFromInputs("creative", ["a", "b"]).map((row) => row.inputNodeIds)).toEqual([["a"], ["b"]]); + }); + + test("broadcasts a singleton reference column across rows", () => { + expect(createBatchRowsFromColumns("try_on", [["person-1", "person-2"], ["garment"]]).map((row) => row.inputNodeIds)).toEqual([ + ["person-1", "garment"], + ["person-2", "garment"], + ]); + }); + + test("new canvas node has durable batch defaults", () => { + const node = createCanvasNode(CanvasNodeType.BatchTable, { x: 500, y: 300 }); + + expect(node.title).toBe("批量创作表"); + expect(node.metadata?.batchTable).toEqual({ operation: "try_on", concurrency: 10, referenceColumns: [{ id: "reference-1", label: "参考图 1" }, { id: "reference-2", label: "参考图 2" }, { id: "reference-3", label: "参考图 3" }], rows: [] }); + }); + test("keeps the third reference handle addressable", () => { + const node = createCanvasNode(CanvasNodeType.BatchTable, { x: 500, y: 300 }); + + expect(batchReferenceHandleAtY(node, node.position.y + 112 + 2 * 38)).toBe(batchReferenceHandleId("reference-3")); + }); + test("keeps the second reference handle addressable", () => { + const node = createCanvasNode(CanvasNodeType.BatchTable, { x: 500, y: 300 }); + const referenceColumns = [ + { id: "reference-1", label: "参考图 1" }, + { id: "reference-2", label: "参考图 2" }, + ]; + node.metadata = { ...node.metadata, batchTable: { operation: "try_on", concurrency: 10, referenceColumns, rows: [] } }; + + expect(batchReferenceHandleAtY(node, node.position.y + 112 + 38)).toBe(batchReferenceHandleId("reference-2")); + }); + + test("chooses the nearest reference handle when magnetic hit areas overlap", () => { const node = createCanvasNode(CanvasNodeType.BatchTable, { x: 500, y: 300 }); - expect(node.title).toBe("批量创作表"); - expect(node.width).toBe(1280); - expect(node.height).toBe(560); - expect(node.metadata?.batchTable).toEqual({ operation: "try_on", concurrency: 10, referenceColumns: [{ id: "reference-1", label: "参考图 1" }, { id: "reference-2", label: "参考图 2" }, { id: "reference-3", label: "参考图 3" }], rows: [] }); + expect(batchReferenceHandleAtY(node, node.position.y + 112 + 38, 90)).toBe(batchReferenceHandleId("reference-2")); }); - test("promotes the previous 900-wide default without touching resized tables", () => { + test("keeps text handles separated from reference controls", () => { const node = createCanvasNode(CanvasNodeType.BatchTable, { x: 500, y: 300 }); - const legacy = { ...node, width: 900, height: 520 }; - const resized = { ...node, width: 1100, height: 480 }; + node.metadata = { ...node.metadata, batchTable: { ...node.metadata?.batchTable!, textColumns: [{ id: "text-1", label: "文字 1" }] } }; - expect(promoteLegacyBatchTableSize(legacy)).toEqual({ ...legacy, width: 1280, height: 560 }); - expect(promoteLegacyBatchTableSize(resized)).toBe(resized); + expect(batchReferenceHandleAtY(node, node.position.y + batchTextHandleTop(3))).toBe(batchTextHandleId("text-1")); + expect(batchTextHandleTop(3)).toBeGreaterThan(112 + 3 * 38); }); - test("keeps a single explicitly stored reference column", () => { + + test("global prompt overrides a row prompt without changing the row data", () => { const node = createCanvasNode(CanvasNodeType.BatchTable, { x: 500, y: 300 }); - node.metadata = { ...node.metadata, batchTable: { operation: "creative", concurrency: 10, referenceColumns: [{ id: "reference-1", label: "参考图 1" }], rows: [] } }; + const table = node.metadata?.batchTable!; + const row = createBatchRowsFromInputs("creative", ["image-1"])[0]; - expect(batchReferenceColumns(node.metadata.batchTable).map((column) => column.id)).toEqual(["reference-1"]); + expect(batchPromptForRow(table, row)).toBe(row.prompt); + expect(batchPromptForRow({ ...table, globalPrompt: "统一风格:高级商业摄影" }, row)).toBe("统一风格:高级商业摄影"); + expect(row.prompt).toBe(CREATIVE_BATCH_PROMPT); }); - test("removes the last reference column down to one and trims row inputs", () => { + test("legacy rows without enabled stay enabled by default", () => { + const row = createBatchRowsFromInputs("creative", ["image-1"])[0]; + expect(row.enabled).toBe(true); + expect({ ...row, enabled: undefined }.enabled === false).toBe(false); + }); + + test("keeps the table grid valid when no text columns exist", () => { + const withoutText = batchGridTemplateColumns(3, 0); + const withText = batchGridTemplateColumns(3, 1); + + expect(withoutText).not.toContain("repeat(0,"); + expect(withoutText).toContain("68px"); + expect(withoutText).toContain("repeat(3, 88px)"); + expect(withText).toContain("repeat(1, minmax(168px, 0.75fr))"); + }); + + test("reorders reference columns and keeps row materials aligned", () => { const table = { operation: "creative" as const, concurrency: 10, @@ -104,49 +98,16 @@ describe("batch creation table", () => { { id: "reference-2", label: "参考图 2" }, { id: "reference-3", label: "参考图 3" }, ], - rows: [{ id: "row-1", enabled: true, inputNodeIds: ["a", "b", "c"], prompt: "x" }], + rows: [{ id: "row-1", enabled: true, inputNodeIds: ["a", "b", "c"], prompt: "" }], }; + const reordered = reorderBatchReferenceColumns(table, "reference-1", "reference-3"); - const next = removeLastBatchReferenceColumn(table); - expect(next?.referenceColumns?.map((column) => column.id)).toEqual(["reference-1", "reference-2"]); - expect(next?.rows[0].inputNodeIds).toEqual(["a", "b"]); - expect(removeLastBatchReferenceColumn({ ...table, referenceColumns: [{ id: "reference-1", label: "参考图 1" }] })).toBeNull(); + expect(reordered.referenceColumns?.map((column) => column.label)).toEqual(["参考图 1", "参考图 2", "参考图 3"]); + expect(reordered.referenceColumns?.map((column) => column.id)).toEqual(["reference-2", "reference-3", "reference-1"]); + expect(reordered.rows[0].inputNodeIds).toEqual(["b", "c", "a"]); }); - test("keeps the second reference handle addressable", () => { - const node = createCanvasNode(CanvasNodeType.BatchTable, { x: 500, y: 300 }); - const referenceColumns = [ - { id: "reference-1", label: "参考图 1" }, - { id: "reference-2", label: "参考图 2" }, - ]; - node.metadata = { ...node.metadata, batchTable: { operation: "try_on", concurrency: 10, referenceColumns, rows: [] } }; - - expect(batchReferenceHandleAtY(node, node.position.y + BATCH_REFERENCE_HANDLE_TOP + BATCH_REFERENCE_HANDLE_GAP)).toBe(batchReferenceHandleId("reference-2")); - }); - - test("chooses the nearest reference handle when magnetic hit areas overlap", () => { - const node = createCanvasNode(CanvasNodeType.BatchTable, { x: 500, y: 300 }); - - expect(batchReferenceHandleAtY(node, node.position.y + BATCH_REFERENCE_HANDLE_TOP + BATCH_REFERENCE_HANDLE_GAP, 90)).toBe(batchReferenceHandleId("reference-2")); - }); - - test("reorders reference columns and keeps row inputs aligned", () => { - const table = { - operation: "try_on" as const, - concurrency: 10, - referenceColumns: [ - { id: "reference-1", label: "参考图 1" }, - { id: "reference-2", label: "参考图 2" }, - { id: "reference-3", label: "参考图 3" }, - ], - rows: [{ id: "row-1", enabled: true, inputNodeIds: ["a", "b", "c"], prompt: "x" }], - }; - const next = reorderBatchReferenceColumns(table, "reference-1", "reference-3"); - expect(next.referenceColumns?.map((column) => column.id)).toEqual(["reference-2", "reference-3", "reference-1"]); - expect(next.rows[0].inputNodeIds).toEqual(["b", "c", "a"]); - }); - - test("swaps reference cells across rows and columns", () => { + test("moves a reference image into an empty slot across rows", () => { const table = { operation: "creative" as const, concurrency: 10, @@ -155,18 +116,26 @@ describe("batch creation table", () => { { id: "reference-2", label: "参考图 2" }, ], rows: [ - { id: "row-1", enabled: true, inputNodeIds: ["a", "b"], prompt: "one" }, - { id: "row-2", enabled: true, inputNodeIds: ["c", "d"], prompt: "two" }, + { id: "row-1", enabled: true, inputNodeIds: ["a", ""], prompt: "" }, + { id: "row-2", enabled: true, inputNodeIds: ["b", ""], prompt: "" }, ], }; - const next = moveBatchReferenceCell(table, "row-1", 0, "row-2", 1); - expect(next.rows[0].inputNodeIds).toEqual(["d", "b"]); - expect(next.rows[1].inputNodeIds).toEqual(["c", "a"]); + + const moved = moveBatchReferenceCell(table, "row-1", 0, "row-2", 1); + + expect(moved.rows.map((row) => row.inputNodeIds)).toEqual([["", ""], ["b", "a"]]); }); - test("uses a non-empty global prompt instead of the row prompt", () => { - const table = { operation: "creative" as const, concurrency: 10, globalPrompt: " 全局覆盖 ", rows: [{ id: "row-1", enabled: true, inputNodeIds: ["a"], prompt: "行提示词" }] }; - expect(batchPromptForRow(table, table.rows[0])).toBe("全局覆盖"); - expect(batchPromptForRow({ ...table, globalPrompt: " " }, table.rows[0])).toBe("行提示词"); + test("swaps reference images when the target slot is occupied", () => { + const table = { + operation: "creative" as const, + concurrency: 10, + referenceColumns: [{ id: "reference-1", label: "参考图 1" }, { id: "reference-2", label: "参考图 2" }], + rows: [{ id: "row-1", enabled: true, inputNodeIds: ["a", "b"], prompt: "" }, { id: "row-2", enabled: true, inputNodeIds: ["c", "d"], prompt: "" }], + }; + + const swapped = moveBatchReferenceCell(table, "row-1", 0, "row-2", 1); + + expect(swapped.rows.map((row) => row.inputNodeIds)).toEqual([["d", "b"], ["c", "a"]]); }); });