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 000000000..cd70adef5 Binary files /dev/null and b/plugin-packages/a6api.yingce-plugin differ 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 000000000..f5ad9ab1f Binary files /dev/null and b/plugin-packages/antigravity-proxy.yingce-plugin differ 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 000000000..3bcb96aab Binary files /dev/null and b/plugin-packages/lxmone-video-suite.yingce-plugin differ diff --git a/plugin-packages/lxmone-video-suite/README.md b/plugin-packages/lxmone-video-suite/README.md new file mode 100644 index 000000000..5aa6281a6 --- /dev/null +++ b/plugin-packages/lxmone-video-suite/README.md @@ -0,0 +1,27 @@ +# 万有引力视频套件(lxmone.xyz) + +该目录是万有引力中转站(`https://lxmone.xyz/v1`)视频协议的官方声明式插件源码。 +后端从生成的 `lxmone-video-suite.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。 + +协议按中转站公开接口文档 `https://lxmone.xyz/docs/` 的 `/v1/videos` 与 `/v1/videos/generations` +两套请求体形状编写,拆成 8 个 provider,覆盖该站点当前开放的全部视频模型: + +| Provider ID | 覆盖模型 | 创建接口 | +| --- | --- | --- | +| `lxmone-wan-videos` | `wan3.0-video`、`wan3.0-video-prime` | `POST /v1/videos` | +| `lxmone-wan-channel-s` | `wan3.0-video-s`、`wan3.0-video-prime-s` | `POST /v1/videos` | +| `lxmone-seedance-videos` | `seedance-2-pro/fast/mini`、`seedance-2.5-pro`、`seedance2.0-a`~`seedance2.0-f`、`seedance2.5-a` | `POST /v1/videos` | +| `lxmone-h3-max-videos` | `minimax-h3-max` | `POST /v1/videos` | +| `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` | +| `lxmone-sd-mini-videos` | `sd-mini` | `POST /v1/videos` | +| `lxmone-grok-videos` | `grok-imagine-video`、`grok-imagine-video-1.5`、`grok-imagine-video-1.5S` | `POST /v1/videos/generations` | +| `lxmone-h3-workflow` | `minimax-h3-a`~`minimax-h3-e` | `POST /v1/videos/generations` | + +完整字段、请求模板与响应映射见 [docs/interface.md](docs/interface.md)。 + +## 构建 + +```sh +node plugin-packages/embed-documentation.mjs lxmone-video-suite +sh plugin-packages/build-packages.sh +``` diff --git a/plugin-packages/lxmone-video-suite/docs/interface.md b/plugin-packages/lxmone-video-suite/docs/interface.md new file mode 100644 index 000000000..840b17305 --- /dev/null +++ b/plugin-packages/lxmone-video-suite/docs/interface.md @@ -0,0 +1,2391 @@ +# 万有引力视频套件接口字段 + +## 协议身份 + +- 插件 ID:`lxmone-video-suite`。 +- 能力:`video`。 +- 默认 Base URL:`https://lxmone.xyz/v1`。 +- 鉴权驱动:`bearer`。 +- 创建接口:`POST /v1/videos` 或 `POST /v1/videos/generations`(按模型所属 provider 决定)。 +- 查询接口:`GET /v1/videos/{{taskId}}` 或 `GET /v1/videos/generations/{{taskId}}`。 +- 参考媒体要求公开可访问的 HTTPS URL(`requiresPublicMediaUrls`)。 + +以上形状来自中转站公开文档 `https://lxmone.xyz/docs/`:同一批模型的 `model`、`prompt`、时长、 +分辨率与画幅字段名在不同渠道并不一致,因此按上游请求体拆成 8 个 provider,而不是一个通用协议。 + +## 配置字段 + +| 字段 | 类型 | 必填 | 含义 | +| --- | --- | --- | --- | +| `apiKey` | secret | 是 | 万有引力中转站的 API Key,随请求发送 `Authorization: Bearer `。 | + +## 统一字段映射 + +宿主向插件提交统一的视频生成请求,插件按 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 000000000..6b4ece702 Binary files /dev/null and b/plugin-packages/metaso-h3.yingce-plugin differ 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 806624b25..0305559ca 100644 Binary files a/plugin-packages/openai-images.yingce-plugin and b/plugin-packages/openai-images.yingce-plugin differ 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"]]); }); });