diff --git a/.github/pr-screenshots/km-seedream-size-presets.jpg b/.github/pr-screenshots/km-seedream-size-presets.jpg new file mode 100644 index 000000000..7005b378b Binary files /dev/null and b/.github/pr-screenshots/km-seedream-size-presets.jpg differ diff --git a/backend/internal/app/model_capability_defaults.go b/backend/internal/app/model_capability_defaults.go index b42acedf3..53f516960 100644 --- a/backend/internal/app/model_capability_defaults.go +++ b/backend/internal/app/model_capability_defaults.go @@ -27,6 +27,19 @@ func DefaultImageCapabilityConfig(protocol string, modelName string) *ImageCapab MaxOutputs: 15, } switch model.ChannelInterfaceType(protocol) { + case "km-kemei-seedream": + image.References.MaxImages = 10 + image.References.MaskSupported = false + presets := kemeiSeedreamImagePresets() + values := make([]string, 0, len(presets)) + for _, preset := range presets { + values = append(values, preset.Size) + } + image.Size = ImageSizeConfig{Parameter: "size", Values: values, Default: "2048x2048", AllowCustom: true, Presets: presets} + image.Quality = ImageQualityConfig{Supported: false, Values: []string{}, Default: "auto"} + // 透明背景需要带 alpha 的单张参考图,通过插件命名空间显式配置。 + image.TransparentBackground = VideoBooleanConfig{Supported: false, Default: false} + image.MaxOutputs = 1 case model.ChannelInterfaceGrokImage: image.References.MaxImages = 1 image.References.MaskSupported = false diff --git a/backend/internal/app/model_capability_kemei.go b/backend/internal/app/model_capability_kemei.go new file mode 100644 index 000000000..0b9cfdaba --- /dev/null +++ b/backend/internal/app/model_capability_kemei.go @@ -0,0 +1,29 @@ +package app + +import ( + "strconv" + "strings" +) + +// 供应商与 Seedream 5.0 官方文档的精确尺寸;档位不能由通用像素阈值推断。 +func kemeiSeedreamImagePresets() []ImageSizePreset { + ratios := []string{"1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3", "21:9"} + tiers := []struct { + name string + sizes []string + }{ + {"1k", []string{"1024x1024", "1152x864", "864x1152", "1424x800", "800x1424", "1248x832", "832x1248", "1568x672"}}, + {"1.5k", []string{"1536x1536", "1792x1344", "1344x1792", "2048x1152", "1152x2048", "1872x1248", "1248x1872", "2352x1008"}}, + {"2k", []string{"2048x2048", "2368x1776", "1776x2368", "2816x1584", "1584x2816", "2496x1664", "1664x2496", "3136x1344"}}, + } + result := make([]ImageSizePreset, 0, 24) + for _, tier := range tiers { + for index, size := range tier.sizes { + parts := strings.Split(size, "x") + width, _ := strconv.Atoi(parts[0]) + height, _ := strconv.Atoi(parts[1]) + result = append(result, ImageSizePreset{Tier: tier.name, Ratio: ratios[index], Width: width, Height: height, Size: size}) + } + } + return result +} diff --git a/backend/internal/app/model_capability_kemei_test.go b/backend/internal/app/model_capability_kemei_test.go new file mode 100644 index 000000000..87314eed6 --- /dev/null +++ b/backend/internal/app/model_capability_kemei_test.go @@ -0,0 +1,23 @@ +package app + +import "testing" + +func TestKemeiSeedreamDefaultsAreValidAndUseOfficialTiers(t *testing.T) { + for _, name := range []string{"doubao-seedream-5-0-pro-260628", "doubao-seedream-5-0-flash-260915"} { + profile := DefaultModelCapabilityConfigForModel("km-kemei-seedream", name).Image + if err := validateImageCapabilityConfig(profile); err != nil { + t.Fatal(err) + } + if profile.References.MaxImages != 10 || profile.References.MaskSupported || profile.MaxOutputs != 1 || profile.Size.Default != "2048x2048" || len(profile.Size.Presets) != 24 || profile.Quality.Supported || profile.TransparentBackground.Supported { + t.Fatalf("defaults: %#v", profile) + } + for _, preset := range profile.Size.Presets { + if preset.Tier == "4k" { + t.Fatal("unsupported 4K default") + } + if preset.Size == "2816x1584" && preset.Tier != "2k" { + t.Fatal("official 2K must not become 4K") + } + } + } +} diff --git a/backend/internal/app/model_capability_validate.go b/backend/internal/app/model_capability_validate.go index 36dc40361..d9b1b9faf 100644 --- a/backend/internal/app/model_capability_validate.go +++ b/backend/internal/app/model_capability_validate.go @@ -65,8 +65,8 @@ func validateImageCapabilityConfig(value *ImageCapabilityConfig) error { } seenPresets := make(map[string]bool) for _, preset := range value.Size.Presets { - if preset.Tier != "1k" && preset.Tier != "2k" && preset.Tier != "4k" { - return BadAuthRequest("图片分辨率档位仅支持 1K、2K、4K") + if preset.Tier != "1k" && preset.Tier != "1.5k" && preset.Tier != "2k" && preset.Tier != "4k" { + return BadAuthRequest("图片分辨率档位仅支持 1K、1.5K、2K、4K") } parts := strings.Split(preset.Ratio, ":") if len(parts) != 2 { @@ -416,6 +416,8 @@ func imageResolutionTier(quality string) string { switch strings.ToLower(strings.TrimSpace(quality)) { case "1k", "low": return "1k" + case "1.5k": + return "1.5k" case "2k", "medium": return "2k" case "4k", "high": diff --git a/backend/internal/protocol/km_kemei_seedream_test.go b/backend/internal/protocol/km_kemei_seedream_test.go new file mode 100644 index 000000000..c9d97213f --- /dev/null +++ b/backend/internal/protocol/km_kemei_seedream_test.go @@ -0,0 +1,105 @@ +package protocol + +import ( + "context" + "testing" +) + +func TestKemeiSeedreamRequestDefaultsAndReferences(t *testing.T) { + adapter := officialPackageAdapter(t, "km-kemei-seedream.yingce-plugin", "km-kemei-seedream") + for _, name := range []string{"doubao-seedream-5-0-pro-260628", "doubao-seedream-5-0-flash-260915"} { + request := GenerationRequest{Model: name, Prompt: "暖色书房中的橘猫", ImageCount: 1, Quality: "high"} + spec, err := adapter.BuildCreate(context.Background(), RequestContext{Request: request}) + if err != nil { + t.Fatal(err) + } + body := manifestTestBody(t, spec) + if spec.Path != "/v1/images/generations" || body["model"] != name || body["n"] != float64(1) || body["size"] != "2K" || body["response_format"] != "url" || body["output_format"] != "jpeg" || body["watermark"] != false { + t.Fatalf("defaults = %#v", body) + } + for _, key := range []string{"image", "quality", "resolution", "seed", "stream", "web_search", "sequential_image_generation"} { + if _, exists := body[key]; exists { + t.Fatalf("unexpected %s: %#v", key, body) + } + } + request.Images = []MediaReference{{URL: "https://example.com/b.png", Order: 2}, {DataURL: "data:image/png;base64,aA==", Order: 1}} + request.AspectRatio = "16:9" + spec, err = adapter.BuildCreate(context.Background(), RequestContext{Request: request}) + if err != nil { + t.Fatal(err) + } + body = manifestTestBody(t, spec) + images := body["image"].([]any) + if body["size"] != "2816x1584" || len(images) != 2 || images[0] != request.Images[1].DataURL { + t.Fatalf("references = %#v", body) + } + for _, size := range []string{"1K", "1.5K", "2K", "2352x1008", "2816x1584", "1536x1536"} { + request.AspectRatio = size + if _, err := adapter.BuildCreate(context.Background(), RequestContext{Request: request}); err != nil { + t.Fatalf("size %s: %v", size, err) + } + } + } +} + +func TestKemeiSeedreamRejectsUnsupportedAndOutOfRangeRequests(t *testing.T) { + adapter := officialPackageAdapter(t, "km-kemei-seedream.yingce-plugin", "km-kemei-seedream") + base := GenerationRequest{Model: "doubao-seedream-5-0-pro-260628", Prompt: "橘猫", ImageCount: 1, AspectRatio: "2048x2048"} + for _, tc := range []struct { + name string + change func(*GenerationRequest) + }{ + {"wrong model", func(r *GenerationRequest) { r.Model = "midjourney-1k" }}, + {"multiple outputs", func(r *GenerationRequest) { r.ImageCount = 2 }}, + {"empty prompt", func(r *GenerationRequest) { r.Prompt = " " }}, + {"too small", func(r *GenerationRequest) { r.AspectRatio = "512x512" }}, + {"4k", func(r *GenerationRequest) { r.AspectRatio = "4K" }}, + {"too large", func(r *GenerationRequest) { r.AspectRatio = "2880x2880" }}, + {"bad dimensions", func(r *GenerationRequest) { r.AspectRatio = "2048x2048x1" }}, + {"mask", func(r *GenerationRequest) { + r.Images = []MediaReference{{URL: "https://example.com/a.png", Role: "mask"}} + }}, + {"too many references", func(r *GenerationRequest) { + for i := 0; i < 11; i++ { + r.Images = append(r.Images, MediaReference{URL: "https://example.com/a.png"}) + } + }}, + {"oversize reference", func(r *GenerationRequest) { + r.Images = []MediaReference{{URL: "https://example.com/a.png", Metadata: map[string]any{"bytes": 30*1024*1024 + 1}}} + }}, + {"unsupported format", func(r *GenerationRequest) { + r.ProviderOptions = map[string]map[string]any{"km-kemei-seedream": {"output_format": "webp"}} + }}, + {"group generation", func(r *GenerationRequest) { + r.ProviderOptions = map[string]map[string]any{"km-kemei-seedream": {"sequential_image_generation": "auto"}} + }}, + {"transparent text", func(r *GenerationRequest) { + r.ProviderOptions = map[string]map[string]any{"km-kemei-seedream": {"background": "transparent", "output_format": "png"}} + }}, + } { + t.Run(tc.name, func(t *testing.T) { + request := base + tc.change(&request) + if _, err := adapter.BuildCreate(context.Background(), RequestContext{Request: request}); err == nil { + t.Fatal("invalid request accepted") + } + }) + } +} + +func TestKemeiSeedreamLayerResultsAndErrors(t *testing.T) { + adapter := officialPackageAdapter(t, "km-kemei-seedream.yingce-plugin", "km-kemei-seedream") + request := GenerationRequest{Model: "doubao-seedream-5-0-flash-260915", AspectRatio: "auto", Images: []MediaReference{{URL: "https://example.com/source.png"}}, ProviderOptions: map[string]map[string]any{"km-kemei-seedream": {"layer_decomposition": true, "output_format": "png"}}} + spec, err := adapter.BuildCreate(context.Background(), RequestContext{Request: request}) + if err != nil || manifestTestBody(t, spec)["size"] != "auto" { + t.Fatalf("layer request: %#v %v", spec, err) + } + result, err := adapter.ParseCreate(context.Background(), []byte(`{"data":[{"url":"https://example.com/base.png","z_index":0},{"b64_json":"aA==","z_index":1}],"usage":{"generated_images":2}}`)) + if err != nil || result.Status != StatusSucceeded || result.Result == nil || len(result.Result.Images) != 2 { + t.Fatalf("results: %#v %v", result, err) + } + result, err = adapter.ParseCreate(context.Background(), []byte(`{"error":{"code":"invalid_size","message":"尺寸不合法"}}`)) + if err != nil || result.Status != StatusFailed || result.Message != "尺寸不合法" { + t.Fatalf("error: %#v %v", result, err) + } +} diff --git a/docs/content/docs/backend/code-map.mdx b/docs/content/docs/backend/code-map.mdx index 7adb96a1e..b384a677a 100644 --- a/docs/content/docs/backend/code-map.mdx +++ b/docs/content/docs/backend/code-map.mdx @@ -5,6 +5,13 @@ description: 从产品功能快速定位主要代码入口 # 代码功能地图 +## 可美 KM Seedream + +- 声明式请求、响应与参数合同:`plugin-packages/km-kemei-seedream/manifest.json`、`docs/interface.md`;分发包:`plugin-packages/km-kemei-seedream.yingce-plugin`。 +- 后端默认能力与预设:`backend/internal/app/model_capability_defaults.go`、`model_capability_kemei.go`;档位校验:`model_capability_validate.go`。 +- 前端预设与选择:`web/src/lib/kemei-image-presets.ts`、`image-resolution-tiers.ts`、`image-size-presets.ts`、`model-capabilities.ts`;界面入口:`web/src/components/image-size-picker.tsx`、`image-size-presets-editor.tsx`。 +- 协议回归:`backend/internal/protocol/km_kemei_seedream_test.go`;前后端能力与尺寸测试位于对应测试目录。 + ## 自定义更新公告 - 后台编辑:`web/src/pages/admin/settings/components/update-announcement-editor.tsx`;版本、排序与草稿校验:`web/src/lib/update-announcement.ts`。 diff --git a/docs/content/docs/backend/http-api.mdx b/docs/content/docs/backend/http-api.mdx index ef4ed38ac..14717faec 100644 --- a/docs/content/docs/backend/http-api.mdx +++ b/docs/content/docs/backend/http-api.mdx @@ -20,6 +20,9 @@ Pi 模型桥在保存待执行调用之前,复用单步输出限制校验: 素材绑定使用“素材→分镜”和 `toHandleId: row:`;镜头输出使用“分镜→图片/视频”和 `fromHandleId: row:`。`rowId` 必须是读取工具返回的真实行 ID,不能用镜头编号代替。普通节点连线与镜头行连线按 handle 区分,已有整表连线不代表该行已经绑定。 handle 放错端点、格式错误或行 ID 不存在时,工具错误回执提供具体 `field`(如 `ops[0].fromHandleId`)、`issue`、方向说明及连线参数模板,不再使用无关的新增节点示例。模板不会被自动执行,不自动交换端点或修改原调用。权限、快照和重复连线校验保持不变;工具契约版本为 v4,重启后端后新会话使用更新后的说明。 +## KM 图片尺寸能力 + +图片能力 `size.presets[].tier` 在已有档位之外接受 `1.5k`;前端档位选择、请求归一化与服务端预设校验保持一致。`km-kemei-seedream` 默认 `2048x2048`、单次一张、最多十张参考图,提供二十四个精确尺寸。透明输出使用插件扩展字段,不开启宿主通用透明背景开关。模型能力仍保存在现有 JSON 字段,无数据库迁移或新端点。 ## 注册邮箱等价检查 diff --git a/docs/content/docs/overview/features.mdx b/docs/content/docs/overview/features.mdx index 40dabbccc..37bf8e0ca 100644 --- a/docs/content/docs/overview/features.mdx +++ b/docs/content/docs/overview/features.mdx @@ -5,6 +5,12 @@ description: 当前项目已实现的核心工作台能力与使用入口 # 功能清单 +## 可美 KM Seedream 图片 + +`km-kemei-seedream` 声明式插件接入 Seedream 5.0 Pro / Flash 的文生图与参考图编辑,支持 URL / Base64 响应并由宿主统一保存图片。精确模型 ID 与扩展参数见插件包内 `docs/interface.md`。 + +协议默认 2K 方图、单张生成、最多十张参考图,提供 1K / 1.5K / 2K 共二十四个精确尺寸预设。后台能力编辑、画布选择与服务端校验同步支持 1.5K。蒙版与通用质量开关关闭;透明输出通过插件命名空间配置,并要求单张含 alpha 的参考图。供应商错误和空结果保留失败语义。 + ## 卡藏 Midjourney 画布接收一次任务返回的全部图片,四张结果自动组成收起的图片堆叠,可展开、选择主图和分别使用。每张图保留独立资源及素材绑定,保存、刷新和任务重复同步不会将后续图片覆盖为第一张。已有仅显示首图的成功 Midjourney 节点在重新打开画布时从原任务恢复遗漏结果,无需再次生成。 diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 5ec154f75..9038bf7cd 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -29,6 +29,11 @@ description: 当前项目已实现功能的手工验证清单与未确认边界 - 已实现:所有邮件投递入口统一记录详细后端错误,标记 SMTP 失败步骤,并隐藏错误回显中的账号、密码、邮箱、正文和验证码;不改变公开 API 响应。 - [x] 专项测试通过:本地 SMTP 服务模拟欢迎响应、STARTTLS、认证和收件人拒绝;验证日志上下文、错误链保留、敏感值脱敏、成功时不记录失败日志,并回归密码找回流程。 - [ ] 使用实际 SMTP 配置分别触发连接、TLS、认证和收件人失败,确认 `backend` 日志保留具体错误且无敏感信息;覆盖注册、登录、绑定邮箱和密码找回。 +## 可美 KM Seedream 图片 + +- 已实现:Pro / Flash 声明式协议、参考图与扩展参数、结果保存,以及前后端一致的 1K / 1.5K / 2K 尺寸预设。 +- [ ] 用户验收协议选择后的默认能力、1.5K 档位与宽高比切换、真实图片生成及刷新后的素材显示。 +- [ ] 在供应商环境验证多参考图、Base64 响应、透明输出、全部尺寸和扩展参数;协议测试不替代真实供应商验收。 ## 复制生图节点后节点消失 diff --git a/plugin-packages/km-kemei-seedream.yingce-plugin b/plugin-packages/km-kemei-seedream.yingce-plugin new file mode 100644 index 000000000..3c793699c Binary files /dev/null and b/plugin-packages/km-kemei-seedream.yingce-plugin differ diff --git a/plugin-packages/km-kemei-seedream/README.md b/plugin-packages/km-kemei-seedream/README.md new file mode 100644 index 000000000..0177ada05 --- /dev/null +++ b/plugin-packages/km-kemei-seedream/README.md @@ -0,0 +1,9 @@ +# KM 可美 Seedream 5.0 + +`yingce.plugin/v2` 声明式图片协议包。使用渠道凭证,宿主统一负责 HTTPS、SSRF 校验、超时与媒体持久化;不包含脚本运行时或密钥。 + +适配精确模型 ID:`doubao-seedream-5-0-pro-260628`、`doubao-seedream-5-0-flash-260915`。 + +选择协议时“能力与参数”默认设为:2K / 1:1(2048x2048)、单张生成、最多 10 张参考图、单图 30 MB、关闭蒙版、关闭通用 quality 和透明背景开关。1K / 1.5K / 2K 各有八种官方精确尺寸。响应默认 URL、输出 JPEG、关闭水印;所有返回图片及用量交给宿主消费。 + +详情见 [接口说明](docs/interface.md)。 diff --git a/plugin-packages/km-kemei-seedream/docs/interface.md b/plugin-packages/km-kemei-seedream/docs/interface.md new file mode 100644 index 000000000..8dfd7e6ae --- /dev/null +++ b/plugin-packages/km-kemei-seedream/docs/interface.md @@ -0,0 +1,1525 @@ +# KM 可美 Seedream 5.0 接口 + +## 来源与范围 + +- [供应商图像文档](https://token.xinhankr.com/docs/quickstart/doc0002),公开文档 `volc-image`:OpenAI 兼容端点、Pro 的限制与默认值。 +- [火山引擎 Image generation API](https://docs.volcengine.com/docs/ark/image-generation-api?lang=zh&redirect=1):Pro / Flash 的精确模型 ID、尺寸、参考图、格式和能力限制。 +- 供应商模型市场确认两个精确模型 ID 可用。市场计费档不是参考图或组图数量上限。 + +## 请求 + +`POST https://token.xinhankr.com/v1/images/generations`,JSON,渠道 API Key 作为 Bearer Token。 + +```json +{ + "model": "doubao-seedream-5-0-pro-260628", + "prompt": "暖色书房中的橘猫,写实照片,正方形构图", + "n": 1, + "size": "2048x2048", + "response_format": "url", + "output_format": "jpeg", + "watermark": false, + "background": "opaque" +} +``` + +Flash 使用 `doubao-seedream-5-0-flash-260915`。图生图仍使用同一端点;参考图映射为 `image`,单图是字符串,多图是数组,保持输入顺序,支持 URL / Base64。最多 10 张,每张不超过 30 MB。官方还要求参考图边长大于 14、总像素 196–36000000、比例 1:16–16:1;这些条件需素材本身满足。没有蒙版接口。 + +`size` 支持 `1K` / `1.5K` / `2K`,或像素字符串。空值 / auto 默认 2K;宿主比例值映射为官方 2K 像素。自定义总像素 921600–4624220,比例 1:16–16:1。宿主交互控件还保留全站的 16px 步长、最长边 3840 与 3:1 比例约束。提示词 32000 字符是宿主限制,未声称是供应商的上限。 + +| 比例 | 1K | 1.5K | 2K | +| --- | --- | --- | --- | +| 1:1 | 1024x1024 | 1536x1536 | 2048x2048 | +| 4:3 | 1152x864 | 1792x1344 | 2368x1776 | +| 3:4 | 864x1152 | 1344x1792 | 1776x2368 | +| 16:9 | 1424x800 | 2048x1152 | 2816x1584 | +| 9:16 | 800x1424 | 1152x2048 | 1584x2816 | +| 3:2 | 1248x832 | 1872x1248 | 2496x1664 | +| 2:3 | 832x1248 | 1248x1872 | 1664x2496 | +| 21:9 | 1568x672 | 2352x1008 | 3136x1344 | + +## 命名空间扩展 + +`providerOptions.km-kemei-seedream`: + +- `response_format`:url(默认)/ b64_json。 +- `output_format`:jpeg(默认)/ png。 +- `watermark`:false(默认)/ true。 +- `background`:opaque(默认)/ transparent。透明只用于一张带 alpha 的输入图,且输出 png;插件校验单图与 png 条件,alpha 条件由供应商检查。宿主通用透明开关默认关闭,避免把任意文生图伪装成此能力。 +- `layer_decomposition`:布尔值,仅一张输入图,尺寸使用档位或 auto;此时允许空提示词,保存全部图层,不按 n=1 截断。 + +Pro / Flash 不支持 sequential_image_generation、组图、联网搜索、流式输出。插件拒绝相关扩展字段,不发送通用 quality、resolution、seed。 + +## 响应与失败 + +同步响应中的 `data[]` 全部提取,兼容 url / b64_json,并透传 usage。供应商 URL 有有效期,宿主必须持久化结果。HTTP 错误由宿主处理,JSON 的 error.code / error.message 映射为失败;不以空结果冒充成功。 + +安装包不携带渠道密钥;管理员通过正常插件安装机制启用,再给渠道模型选择此协议。模型价格与启用状态由管理员独立配置。 + + +## Manifest 完整接口定义 + +以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。 + +```json +{ + "apiVersion": "yingce.plugin/v2", + "id": "km-kemei-seedream", + "name": "KM 可美 Seedream 5.0", + "version": "1.0.0", + "author": "KM 可美 / 影绘", + "description": "适配可美 Seedream 5.0 Pro 与 Flash,单张生成、参考图与全部结果提取。", + "enabled": true, + "installable": true, + "permissions": [ + "generation.run", + "media.read" + ], + "configuration": { + "fields": [ + { + "name": "apiKey", + "type": "secret", + "label": "API Key", + "required": true + } + ] + }, + "contributes": { + "providers": [ + { + "id": "km-kemei-seedream", + "label": "KM 可美 Seedream 5.0", + "capabilities": [ + "image" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://token.xinhankr.com", + "requiresPublicMediaUrls": false, + "auth": { + "type": "bearer", + "field": "apiKey" + }, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "精确 ID:doubao-seedream-5-0-pro-260628 / doubao-seedream-5-0-flash-260915。" + }, + { + "name": "prompt", + "type": "string", + "required": true, + "mapping": "prompt", + "description": "图片提示词;宿主限制 32000 字符。" + }, + { + "name": "imageCount", + "type": "integer", + "required": false, + "mapping": "n=1", + "description": "仅单张生成;图层分解返回的所有图层均保存。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "size", + "description": "默认 2048x2048(2K、1:1);支持 1K / 1.5K / 2K、八种比例或合法像素尺寸。" + }, + { + "name": "images", + "type": "media[]", + "required": false, + "mapping": "image", + "description": "最多 10 张;URL / Base64;不支持蒙版。" + }, + { + "name": "providerOptions", + "type": "object", + "required": false, + "mapping": "providerOptions.km-kemei-seedream", + "description": "可选 response_format / output_format / watermark / background / layer_decomposition。" + } + ], + "validations": [ + { + "assert": { + "$in": [ + { + "$ref": "request.model" + }, + [ + "doubao-seedream-5-0-pro-260628", + "doubao-seedream-5-0-flash-260915" + ] + ] + }, + "message": "此协议只适用于可美 Seedream 5.0 Pro / Flash 的已确认模型 ID。" + }, + { + "assert": { + "$lte": [ + { + "$ref": "request.imageCount" + }, + 1 + ] + }, + "message": "Seedream 5.0 Pro / Flash 仅支持单张生成,n 只能为 1。" + }, + { + "assert": { + "$or": [ + { + "$gt": [ + { + "$len": { + "$trim": { + "$ref": "request.prompt" + } + } + }, + 0 + ] + }, + { + "$eq": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + true + ] + } + ] + }, + "message": "提示词不能为空;仅图层分解模式可省略提示词。" + }, + { + "assert": { + "$lte": [ + { + "$len": { + "$ref": "request.prompt" + } + }, + 32000 + ] + }, + "message": "提示词超过宿主配置的 32000 字符上限。" + }, + { + "assert": { + "$lte": [ + { + "$len": { + "$ref": "request.images" + } + }, + 10 + ] + }, + "message": "Seedream 5.0 参考图最多 10 张。" + }, + { + "assert": { + "$eq": [ + { + "$len": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$eq": [ + { + "$ref": "media.role" + }, + "mask" + ] + } + } + } + }, + 0 + ] + }, + "message": "此协议不支持蒙版编辑。" + }, + { + "assert": { + "$eq": [ + { + "$len": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$gt": [ + { + "$coalesce": [ + { + "$ref": "media.metadata.bytes" + }, + 0 + ] + }, + 31457280 + ] + } + } + } + }, + 0 + ] + }, + "message": "单张参考图不得超过 30 MB。" + }, + { + "assert": { + "$or": [ + { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "1k", + "1.5k", + "2k", + "1:1", + "4:3", + "3:4", + "16:9", + "9:16", + "3:2", + "2:3", + "21:9" + ] + ] + }, + { + "$and": [ + { + "$eq": [ + { + "$len": { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + } + }, + 2 + ] + }, + { + "$gt": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + 0 + ] + }, + { + "$gt": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + }, + 0 + ] + }, + { + "$gte": [ + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + } + ] + }, + 921600 + ] + }, + { + "$lte": [ + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + } + ] + }, + 4624220 + ] + }, + { + "$lte": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + }, + 16 + ] + } + ] + }, + { + "$lte": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + }, + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + 16 + ] + } + ] + } + ] + } + ] + }, + "message": "size 必须为 1K / 1.5K / 2K 或合法像素尺寸;像素总数 921600–4624220,比例 1:16–16:1。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.response_format" + }, + "url" + ] + }, + [ + "url", + "b64_json" + ] + ] + }, + "message": "response_format 仅支持 url / b64_json。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.output_format" + }, + "jpeg" + ] + }, + [ + "jpeg", + "png" + ] + ] + }, + "message": "output_format 仅支持 jpeg / png。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.background" + }, + "opaque" + ] + }, + [ + "opaque", + "transparent" + ] + ] + }, + "message": "background 仅支持 opaque / transparent。" + }, + { + "assert": { + "$or": [ + { + "$ne": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.background" + }, + "opaque" + ] + }, + "transparent" + ] + }, + { + "$and": [ + { + "$eq": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + { + "$eq": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.output_format" + }, + "jpeg" + ] + }, + "png" + ] + } + ] + } + ] + }, + "message": "透明背景仅用于一张带透明通道的参考图,输出必须为 png。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + [ + true, + false + ] + ] + }, + "message": "layer_decomposition 必须为布尔值。" + }, + { + "assert": { + "$or": [ + { + "$ne": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + true + ] + }, + { + "$and": [ + { + "$eq": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "1k", + "1.5k", + "2k" + ] + ] + } + ] + } + ] + }, + "message": "图层分解仅支持一张参考图,size 使用 1K / 1.5K / 2K / auto 档位。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.sequential_image_generation" + }, + null + ] + }, + "message": "sequential_image_generation 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.sequential_image_generation_options" + }, + null + ] + }, + "message": "sequential_image_generation_options 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.web_search" + }, + null + ] + }, + "message": "web_search 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.stream" + }, + null + ] + }, + "message": "stream 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.tools" + }, + null + ] + }, + "message": "tools 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + } + ], + "create": { + "method": "POST", + "path": "/v1/images/generations", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "n": 1, + "size": { + "$if": { + "condition": { + "$eq": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + true + ] + }, + "then": { + "$if": { + "condition": { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto" + ] + ] + }, + "then": "auto", + "else": { + "$switch": { + "cases": [ + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "2k" + ] + ] + }, + "then": "2K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1k" + ] + }, + "then": "1K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1.5k" + ] + }, + "then": "1.5K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1:1" + ] + }, + "then": "2048x2048" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "4:3" + ] + }, + "then": "2368x1776" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:4" + ] + }, + "then": "1776x2368" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "16:9" + ] + }, + "then": "2816x1584" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "9:16" + ] + }, + "then": "1584x2816" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:2" + ] + }, + "then": "2496x1664" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "2:3" + ] + }, + "then": "1664x2496" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "21:9" + ] + }, + "then": "3136x1344" + } + ], + "default": { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + } + } + } + } + }, + "else": { + "$switch": { + "cases": [ + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "2k" + ] + ] + }, + "then": "2K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1k" + ] + }, + "then": "1K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1.5k" + ] + }, + "then": "1.5K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1:1" + ] + }, + "then": "2048x2048" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "4:3" + ] + }, + "then": "2368x1776" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:4" + ] + }, + "then": "1776x2368" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "16:9" + ] + }, + "then": "2816x1584" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "9:16" + ] + }, + "then": "1584x2816" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:2" + ] + }, + "then": "2496x1664" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "2:3" + ] + }, + "then": "1664x2496" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "21:9" + ] + }, + "then": "3136x1344" + } + ], + "default": { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + } + } + } + } + }, + "image": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + "then": { + "$first": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "else": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + } + }, + "response_format": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.response_format" + }, + "url" + ] + }, + "output_format": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.output_format" + }, + "jpeg" + ] + }, + "watermark": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.watermark" + }, + false + ] + }, + "background": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.background" + }, + "opaque" + ] + }, + "layer_decomposition": { + "$omitEmpty": { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + } + } + } + }, + "response": { + "status": "succeeded", + "images": { + "$ref": "response.data" + }, + "usage": { + "$ref": "response.usage" + }, + "errorPaths": [ + "error.code", + "error.message" + ], + "messagePaths": [ + "error.message" + ] + } + } + ] + }, + "documentation": "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>" +} +``` + diff --git a/plugin-packages/km-kemei-seedream/manifest.json b/plugin-packages/km-kemei-seedream/manifest.json new file mode 100644 index 000000000..0d033a8eb --- /dev/null +++ b/plugin-packages/km-kemei-seedream/manifest.json @@ -0,0 +1,1459 @@ +{ + "apiVersion": "yingce.plugin/v2", + "id": "km-kemei-seedream", + "name": "KM 可美 Seedream 5.0", + "version": "1.0.0", + "author": "KM 可美 / 影绘", + "description": "适配可美 Seedream 5.0 Pro 与 Flash,单张生成、参考图与全部结果提取。", + "enabled": true, + "installable": true, + "permissions": [ + "generation.run", + "media.read" + ], + "configuration": { + "fields": [ + { + "name": "apiKey", + "type": "secret", + "label": "API Key", + "required": true + } + ] + }, + "contributes": { + "providers": [ + { + "id": "km-kemei-seedream", + "label": "KM 可美 Seedream 5.0", + "capabilities": [ + "image" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://token.xinhankr.com", + "requiresPublicMediaUrls": false, + "auth": { + "type": "bearer", + "field": "apiKey" + }, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "精确 ID:doubao-seedream-5-0-pro-260628 / doubao-seedream-5-0-flash-260915。" + }, + { + "name": "prompt", + "type": "string", + "required": true, + "mapping": "prompt", + "description": "图片提示词;宿主限制 32000 字符。" + }, + { + "name": "imageCount", + "type": "integer", + "required": false, + "mapping": "n=1", + "description": "仅单张生成;图层分解返回的所有图层均保存。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "size", + "description": "默认 2048x2048(2K、1:1);支持 1K / 1.5K / 2K、八种比例或合法像素尺寸。" + }, + { + "name": "images", + "type": "media[]", + "required": false, + "mapping": "image", + "description": "最多 10 张;URL / Base64;不支持蒙版。" + }, + { + "name": "providerOptions", + "type": "object", + "required": false, + "mapping": "providerOptions.km-kemei-seedream", + "description": "可选 response_format / output_format / watermark / background / layer_decomposition。" + } + ], + "validations": [ + { + "assert": { + "$in": [ + { + "$ref": "request.model" + }, + [ + "doubao-seedream-5-0-pro-260628", + "doubao-seedream-5-0-flash-260915" + ] + ] + }, + "message": "此协议只适用于可美 Seedream 5.0 Pro / Flash 的已确认模型 ID。" + }, + { + "assert": { + "$lte": [ + { + "$ref": "request.imageCount" + }, + 1 + ] + }, + "message": "Seedream 5.0 Pro / Flash 仅支持单张生成,n 只能为 1。" + }, + { + "assert": { + "$or": [ + { + "$gt": [ + { + "$len": { + "$trim": { + "$ref": "request.prompt" + } + } + }, + 0 + ] + }, + { + "$eq": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + true + ] + } + ] + }, + "message": "提示词不能为空;仅图层分解模式可省略提示词。" + }, + { + "assert": { + "$lte": [ + { + "$len": { + "$ref": "request.prompt" + } + }, + 32000 + ] + }, + "message": "提示词超过宿主配置的 32000 字符上限。" + }, + { + "assert": { + "$lte": [ + { + "$len": { + "$ref": "request.images" + } + }, + 10 + ] + }, + "message": "Seedream 5.0 参考图最多 10 张。" + }, + { + "assert": { + "$eq": [ + { + "$len": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$eq": [ + { + "$ref": "media.role" + }, + "mask" + ] + } + } + } + }, + 0 + ] + }, + "message": "此协议不支持蒙版编辑。" + }, + { + "assert": { + "$eq": [ + { + "$len": { + "$filter": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "where": { + "$gt": [ + { + "$coalesce": [ + { + "$ref": "media.metadata.bytes" + }, + 0 + ] + }, + 31457280 + ] + } + } + } + }, + 0 + ] + }, + "message": "单张参考图不得超过 30 MB。" + }, + { + "assert": { + "$or": [ + { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "1k", + "1.5k", + "2k", + "1:1", + "4:3", + "3:4", + "16:9", + "9:16", + "3:2", + "2:3", + "21:9" + ] + ] + }, + { + "$and": [ + { + "$eq": [ + { + "$len": { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + } + }, + 2 + ] + }, + { + "$gt": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + 0 + ] + }, + { + "$gt": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + }, + 0 + ] + }, + { + "$gte": [ + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + } + ] + }, + 921600 + ] + }, + { + "$lte": [ + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + } + ] + }, + 4624220 + ] + }, + { + "$lte": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + }, + 16 + ] + } + ] + }, + { + "$lte": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 1 + ] + } + }, + { + "$multiply": [ + { + "$toInt": { + "$at": [ + { + "$split": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "x" + ] + }, + 0 + ] + } + }, + 16 + ] + } + ] + } + ] + } + ] + }, + "message": "size 必须为 1K / 1.5K / 2K 或合法像素尺寸;像素总数 921600–4624220,比例 1:16–16:1。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.response_format" + }, + "url" + ] + }, + [ + "url", + "b64_json" + ] + ] + }, + "message": "response_format 仅支持 url / b64_json。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.output_format" + }, + "jpeg" + ] + }, + [ + "jpeg", + "png" + ] + ] + }, + "message": "output_format 仅支持 jpeg / png。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.background" + }, + "opaque" + ] + }, + [ + "opaque", + "transparent" + ] + ] + }, + "message": "background 仅支持 opaque / transparent。" + }, + { + "assert": { + "$or": [ + { + "$ne": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.background" + }, + "opaque" + ] + }, + "transparent" + ] + }, + { + "$and": [ + { + "$eq": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + { + "$eq": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.output_format" + }, + "jpeg" + ] + }, + "png" + ] + } + ] + } + ] + }, + "message": "透明背景仅用于一张带透明通道的参考图,输出必须为 png。" + }, + { + "assert": { + "$in": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + [ + true, + false + ] + ] + }, + "message": "layer_decomposition 必须为布尔值。" + }, + { + "assert": { + "$or": [ + { + "$ne": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + true + ] + }, + { + "$and": [ + { + "$eq": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "1k", + "1.5k", + "2k" + ] + ] + } + ] + } + ] + }, + "message": "图层分解仅支持一张参考图,size 使用 1K / 1.5K / 2K / auto 档位。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.sequential_image_generation" + }, + null + ] + }, + "message": "sequential_image_generation 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.sequential_image_generation_options" + }, + null + ] + }, + "message": "sequential_image_generation_options 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.web_search" + }, + null + ] + }, + "message": "web_search 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.stream" + }, + null + ] + }, + "message": "stream 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + }, + { + "assert": { + "$eq": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.tools" + }, + null + ] + }, + "message": "tools 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。" + } + ], + "create": { + "method": "POST", + "path": "/v1/images/generations", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "n": 1, + "size": { + "$if": { + "condition": { + "$eq": [ + { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + }, + false + ] + }, + true + ] + }, + "then": { + "$if": { + "condition": { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto" + ] + ] + }, + "then": "auto", + "else": { + "$switch": { + "cases": [ + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "2k" + ] + ] + }, + "then": "2K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1k" + ] + }, + "then": "1K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1.5k" + ] + }, + "then": "1.5K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1:1" + ] + }, + "then": "2048x2048" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "4:3" + ] + }, + "then": "2368x1776" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:4" + ] + }, + "then": "1776x2368" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "16:9" + ] + }, + "then": "2816x1584" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "9:16" + ] + }, + "then": "1584x2816" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:2" + ] + }, + "then": "2496x1664" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "2:3" + ] + }, + "then": "1664x2496" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "21:9" + ] + }, + "then": "3136x1344" + } + ], + "default": { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + } + } + } + } + }, + "else": { + "$switch": { + "cases": [ + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + [ + "", + "auto", + "2k" + ] + ] + }, + "then": "2K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1k" + ] + }, + "then": "1K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1.5k" + ] + }, + "then": "1.5K" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "1:1" + ] + }, + "then": "2048x2048" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "4:3" + ] + }, + "then": "2368x1776" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:4" + ] + }, + "then": "1776x2368" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "16:9" + ] + }, + "then": "2816x1584" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "9:16" + ] + }, + "then": "1584x2816" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "3:2" + ] + }, + "then": "2496x1664" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "2:3" + ] + }, + "then": "1664x2496" + }, + { + "when": { + "$eq": [ + { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + }, + "21:9" + ] + }, + "then": "3136x1344" + } + ], + "default": { + "$lower": { + "$trim": { + "$coalesce": [ + { + "$ref": "request.aspectRatio" + }, + "2k" + ] + } + } + } + } + } + } + }, + "image": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$len": { + "$ref": "request.images" + } + }, + 1 + ] + }, + "then": { + "$first": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + }, + "else": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$ref": "media.value" + } + } + } + } + } + }, + "response_format": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.response_format" + }, + "url" + ] + }, + "output_format": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.output_format" + }, + "jpeg" + ] + }, + "watermark": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.watermark" + }, + false + ] + }, + "background": { + "$coalesce": [ + { + "$ref": "request.providerOptions.km-kemei-seedream.background" + }, + "opaque" + ] + }, + "layer_decomposition": { + "$omitEmpty": { + "$ref": "request.providerOptions.km-kemei-seedream.layer_decomposition" + } + } + } + }, + "response": { + "status": "succeeded", + "images": { + "$ref": "response.data" + }, + "usage": { + "$ref": "response.usage" + }, + "errorPaths": [ + "error.code", + "error.message" + ], + "messagePaths": [ + "error.message" + ] + } + } + ] + }, + "documentation": "# KM 可美 Seedream 5.0\n\n`yingce.plugin/v2` 声明式图片协议包。使用渠道凭证,宿主统一负责 HTTPS、SSRF 校验、超时与媒体持久化;不包含脚本运行时或密钥。\n\n适配精确模型 ID:`doubao-seedream-5-0-pro-260628`、`doubao-seedream-5-0-flash-260915`。\n\n选择协议时“能力与参数”默认设为:2K / 1:1(2048x2048)、单张生成、最多 10 张参考图、单图 30 MB、关闭蒙版、关闭通用 quality 和透明背景开关。1K / 1.5K / 2K 各有八种官方精确尺寸。响应默认 URL、输出 JPEG、关闭水印;所有返回图片及用量交给宿主消费。\n\n详情见 [接口说明](docs/interface.md)。\n\n---\n\n# KM 可美 Seedream 5.0 接口\n\n## 来源与范围\n\n- [供应商图像文档](https://token.xinhankr.com/docs/quickstart/doc0002),公开文档 `volc-image`:OpenAI 兼容端点、Pro 的限制与默认值。\n- [火山引擎 Image generation API](https://docs.volcengine.com/docs/ark/image-generation-api?lang=zh&redirect=1):Pro / Flash 的精确模型 ID、尺寸、参考图、格式和能力限制。\n- 供应商模型市场确认两个精确模型 ID 可用。市场计费档不是参考图或组图数量上限。\n\n## 请求\n\n`POST https://token.xinhankr.com/v1/images/generations`,JSON,渠道 API Key 作为 Bearer Token。\n\n```json\n{\n \"model\": \"doubao-seedream-5-0-pro-260628\",\n \"prompt\": \"暖色书房中的橘猫,写实照片,正方形构图\",\n \"n\": 1,\n \"size\": \"2048x2048\",\n \"response_format\": \"url\",\n \"output_format\": \"jpeg\",\n \"watermark\": false,\n \"background\": \"opaque\"\n}\n```\n\nFlash 使用 `doubao-seedream-5-0-flash-260915`。图生图仍使用同一端点;参考图映射为 `image`,单图是字符串,多图是数组,保持输入顺序,支持 URL / Base64。最多 10 张,每张不超过 30 MB。官方还要求参考图边长大于 14、总像素 196–36000000、比例 1:16–16:1;这些条件需素材本身满足。没有蒙版接口。\n\n`size` 支持 `1K` / `1.5K` / `2K`,或像素字符串。空值 / auto 默认 2K;宿主比例值映射为官方 2K 像素。自定义总像素 921600–4624220,比例 1:16–16:1。宿主交互控件还保留全站的 16px 步长、最长边 3840 与 3:1 比例约束。提示词 32000 字符是宿主限制,未声称是供应商的上限。\n\n| 比例 | 1K | 1.5K | 2K |\n| --- | --- | --- | --- |\n| 1:1 | 1024x1024 | 1536x1536 | 2048x2048 |\n| 4:3 | 1152x864 | 1792x1344 | 2368x1776 |\n| 3:4 | 864x1152 | 1344x1792 | 1776x2368 |\n| 16:9 | 1424x800 | 2048x1152 | 2816x1584 |\n| 9:16 | 800x1424 | 1152x2048 | 1584x2816 |\n| 3:2 | 1248x832 | 1872x1248 | 2496x1664 |\n| 2:3 | 832x1248 | 1248x1872 | 1664x2496 |\n| 21:9 | 1568x672 | 2352x1008 | 3136x1344 |\n\n## 命名空间扩展\n\n`providerOptions.km-kemei-seedream`:\n\n- `response_format`:url(默认)/ b64_json。\n- `output_format`:jpeg(默认)/ png。\n- `watermark`:false(默认)/ true。\n- `background`:opaque(默认)/ transparent。透明只用于一张带 alpha 的输入图,且输出 png;插件校验单图与 png 条件,alpha 条件由供应商检查。宿主通用透明开关默认关闭,避免把任意文生图伪装成此能力。\n- `layer_decomposition`:布尔值,仅一张输入图,尺寸使用档位或 auto;此时允许空提示词,保存全部图层,不按 n=1 截断。\n\nPro / Flash 不支持 sequential_image_generation、组图、联网搜索、流式输出。插件拒绝相关扩展字段,不发送通用 quality、resolution、seed。\n\n## 响应与失败\n\n同步响应中的 `data[]` 全部提取,兼容 url / b64_json,并透传 usage。供应商 URL 有有效期,宿主必须持久化结果。HTTP 错误由宿主处理,JSON 的 error.code / error.message 映射为失败;不以空结果冒充成功。\n\n安装包不携带渠道密钥;管理员通过正常插件安装机制启用,再给渠道模型选择此协议。模型价格与启用状态由管理员独立配置。\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"km-kemei-seedream\",\n \"name\": \"KM 可美 Seedream 5.0\",\n \"version\": \"1.0.0\",\n \"author\": \"KM 可美 / 影绘\",\n \"description\": \"适配可美 Seedream 5.0 Pro 与 Flash,单张生成、参考图与全部结果提取。\",\n \"enabled\": true,\n \"installable\": true,\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\": \"km-kemei-seedream\",\n \"label\": \"KM 可美 Seedream 5.0\",\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://token.xinhankr.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:doubao-seedream-5-0-pro-260628 / doubao-seedream-5-0-flash-260915。\"\n },\n {\n \"name\": \"prompt\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"prompt\",\n \"description\": \"图片提示词;宿主限制 32000 字符。\"\n },\n {\n \"name\": \"imageCount\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"n=1\",\n \"description\": \"仅单张生成;图层分解返回的所有图层均保存。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"size\",\n \"description\": \"默认 2048x2048(2K、1:1);支持 1K / 1.5K / 2K、八种比例或合法像素尺寸。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"image\",\n \"description\": \"最多 10 张;URL / Base64;不支持蒙版。\"\n },\n {\n \"name\": \"providerOptions\",\n \"type\": \"object\",\n \"required\": false,\n \"mapping\": \"providerOptions.km-kemei-seedream\",\n \"description\": \"可选 response_format / output_format / watermark / background / layer_decomposition。\"\n }\n ],\n \"validations\": [\n {\n \"assert\": {\n \"$in\": [\n {\n \"$ref\": \"request.model\"\n },\n [\n \"doubao-seedream-5-0-pro-260628\",\n \"doubao-seedream-5-0-flash-260915\"\n ]\n ]\n },\n \"message\": \"此协议只适用于可美 Seedream 5.0 Pro / Flash 的已确认模型 ID。\"\n },\n {\n \"assert\": {\n \"$lte\": [\n {\n \"$ref\": \"request.imageCount\"\n },\n 1\n ]\n },\n \"message\": \"Seedream 5.0 Pro / Flash 仅支持单张生成,n 只能为 1。\"\n },\n {\n \"assert\": {\n \"$or\": [\n {\n \"$gt\": [\n {\n \"$len\": {\n \"$trim\": {\n \"$ref\": \"request.prompt\"\n }\n }\n },\n 0\n ]\n },\n {\n \"$eq\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.layer_decomposition\"\n },\n false\n ]\n },\n true\n ]\n }\n ]\n },\n \"message\": \"提示词不能为空;仅图层分解模式可省略提示词。\"\n },\n {\n \"assert\": {\n \"$lte\": [\n {\n \"$len\": {\n \"$ref\": \"request.prompt\"\n }\n },\n 32000\n ]\n },\n \"message\": \"提示词超过宿主配置的 32000 字符上限。\"\n },\n {\n \"assert\": {\n \"$lte\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 10\n ]\n },\n \"message\": \"Seedream 5.0 参考图最多 10 张。\"\n },\n {\n \"assert\": {\n \"$eq\": [\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 \"message\": \"此协议不支持蒙版编辑。\"\n },\n {\n \"assert\": {\n \"$eq\": [\n {\n \"$len\": {\n \"$filter\": {\n \"from\": {\n \"$ref\": \"request.images\"\n },\n \"as\": \"media\",\n \"where\": {\n \"$gt\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"media.metadata.bytes\"\n },\n 0\n ]\n },\n 31457280\n ]\n }\n }\n }\n },\n 0\n ]\n },\n \"message\": \"单张参考图不得超过 30 MB。\"\n },\n {\n \"assert\": {\n \"$or\": [\n {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n [\n \"\",\n \"auto\",\n \"1k\",\n \"1.5k\",\n \"2k\",\n \"1:1\",\n \"4:3\",\n \"3:4\",\n \"16:9\",\n \"9:16\",\n \"3:2\",\n \"2:3\",\n \"21:9\"\n ]\n ]\n },\n {\n \"$and\": [\n {\n \"$eq\": [\n {\n \"$len\": {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n }\n },\n 2\n ]\n },\n {\n \"$gt\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 0\n ]\n }\n },\n 0\n ]\n },\n {\n \"$gt\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 1\n ]\n }\n },\n 0\n ]\n },\n {\n \"$gte\": [\n {\n \"$multiply\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 0\n ]\n }\n },\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 1\n ]\n }\n }\n ]\n },\n 921600\n ]\n },\n {\n \"$lte\": [\n {\n \"$multiply\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 0\n ]\n }\n },\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 1\n ]\n }\n }\n ]\n },\n 4624220\n ]\n },\n {\n \"$lte\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 0\n ]\n }\n },\n {\n \"$multiply\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 1\n ]\n }\n },\n 16\n ]\n }\n ]\n },\n {\n \"$lte\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 1\n ]\n }\n },\n {\n \"$multiply\": [\n {\n \"$toInt\": {\n \"$at\": [\n {\n \"$split\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"x\"\n ]\n },\n 0\n ]\n }\n },\n 16\n ]\n }\n ]\n }\n ]\n }\n ]\n },\n \"message\": \"size 必须为 1K / 1.5K / 2K 或合法像素尺寸;像素总数 921600–4624220,比例 1:16–16:1。\"\n },\n {\n \"assert\": {\n \"$in\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.response_format\"\n },\n \"url\"\n ]\n },\n [\n \"url\",\n \"b64_json\"\n ]\n ]\n },\n \"message\": \"response_format 仅支持 url / b64_json。\"\n },\n {\n \"assert\": {\n \"$in\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.output_format\"\n },\n \"jpeg\"\n ]\n },\n [\n \"jpeg\",\n \"png\"\n ]\n ]\n },\n \"message\": \"output_format 仅支持 jpeg / png。\"\n },\n {\n \"assert\": {\n \"$in\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.background\"\n },\n \"opaque\"\n ]\n },\n [\n \"opaque\",\n \"transparent\"\n ]\n ]\n },\n \"message\": \"background 仅支持 opaque / transparent。\"\n },\n {\n \"assert\": {\n \"$or\": [\n {\n \"$ne\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.background\"\n },\n \"opaque\"\n ]\n },\n \"transparent\"\n ]\n },\n {\n \"$and\": [\n {\n \"$eq\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 1\n ]\n },\n {\n \"$eq\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.output_format\"\n },\n \"jpeg\"\n ]\n },\n \"png\"\n ]\n }\n ]\n }\n ]\n },\n \"message\": \"透明背景仅用于一张带透明通道的参考图,输出必须为 png。\"\n },\n {\n \"assert\": {\n \"$in\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.layer_decomposition\"\n },\n false\n ]\n },\n [\n true,\n false\n ]\n ]\n },\n \"message\": \"layer_decomposition 必须为布尔值。\"\n },\n {\n \"assert\": {\n \"$or\": [\n {\n \"$ne\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.layer_decomposition\"\n },\n false\n ]\n },\n true\n ]\n },\n {\n \"$and\": [\n {\n \"$eq\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 1\n ]\n },\n {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n [\n \"\",\n \"auto\",\n \"1k\",\n \"1.5k\",\n \"2k\"\n ]\n ]\n }\n ]\n }\n ]\n },\n \"message\": \"图层分解仅支持一张参考图,size 使用 1K / 1.5K / 2K / auto 档位。\"\n },\n {\n \"assert\": {\n \"$eq\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.sequential_image_generation\"\n },\n null\n ]\n },\n \"message\": \"sequential_image_generation 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。\"\n },\n {\n \"assert\": {\n \"$eq\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.sequential_image_generation_options\"\n },\n null\n ]\n },\n \"message\": \"sequential_image_generation_options 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。\"\n },\n {\n \"assert\": {\n \"$eq\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.web_search\"\n },\n null\n ]\n },\n \"message\": \"web_search 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。\"\n },\n {\n \"assert\": {\n \"$eq\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.stream\"\n },\n null\n ]\n },\n \"message\": \"stream 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。\"\n },\n {\n \"assert\": {\n \"$eq\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.tools\"\n },\n null\n ]\n },\n \"message\": \"tools 不适用于 Seedream 5.0 Pro / Flash,已拒绝请求。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/v1/images/generations\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"n\": 1,\n \"size\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.layer_decomposition\"\n },\n false\n ]\n },\n true\n ]\n },\n \"then\": {\n \"$if\": {\n \"condition\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n [\n \"\",\n \"auto\"\n ]\n ]\n },\n \"then\": \"auto\",\n \"else\": {\n \"$switch\": {\n \"cases\": [\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n [\n \"\",\n \"auto\",\n \"2k\"\n ]\n ]\n },\n \"then\": \"2K\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"1k\"\n ]\n },\n \"then\": \"1K\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"1.5k\"\n ]\n },\n \"then\": \"1.5K\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"1:1\"\n ]\n },\n \"then\": \"2048x2048\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"4:3\"\n ]\n },\n \"then\": \"2368x1776\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"3:4\"\n ]\n },\n \"then\": \"1776x2368\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"16:9\"\n ]\n },\n \"then\": \"2816x1584\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"9:16\"\n ]\n },\n \"then\": \"1584x2816\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"3:2\"\n ]\n },\n \"then\": \"2496x1664\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"2:3\"\n ]\n },\n \"then\": \"1664x2496\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"21:9\"\n ]\n },\n \"then\": \"3136x1344\"\n }\n ],\n \"default\": {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n }\n }\n }\n }\n },\n \"else\": {\n \"$switch\": {\n \"cases\": [\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n [\n \"\",\n \"auto\",\n \"2k\"\n ]\n ]\n },\n \"then\": \"2K\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"1k\"\n ]\n },\n \"then\": \"1K\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"1.5k\"\n ]\n },\n \"then\": \"1.5K\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"1:1\"\n ]\n },\n \"then\": \"2048x2048\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"4:3\"\n ]\n },\n \"then\": \"2368x1776\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"3:4\"\n ]\n },\n \"then\": \"1776x2368\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"16:9\"\n ]\n },\n \"then\": \"2816x1584\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"9:16\"\n ]\n },\n \"then\": \"1584x2816\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"3:2\"\n ]\n },\n \"then\": \"2496x1664\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"2:3\"\n ]\n },\n \"then\": \"1664x2496\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n },\n \"21:9\"\n ]\n },\n \"then\": \"3136x1344\"\n }\n ],\n \"default\": {\n \"$lower\": {\n \"$trim\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2k\"\n ]\n }\n }\n }\n }\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 \"$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 \"else\": {\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 },\n \"response_format\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.response_format\"\n },\n \"url\"\n ]\n },\n \"output_format\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.output_format\"\n },\n \"jpeg\"\n ]\n },\n \"watermark\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.watermark\"\n },\n false\n ]\n },\n \"background\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.background\"\n },\n \"opaque\"\n ]\n },\n \"layer_decomposition\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.km-kemei-seedream.layer_decomposition\"\n }\n }\n }\n },\n \"response\": {\n \"status\": \"succeeded\",\n \"images\": {\n \"$ref\": \"response.data\"\n },\n \"usage\": {\n \"$ref\": \"response.usage\"\n },\n \"errorPaths\": [\n \"error.code\",\n \"error.message\"\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/web/src/components/image-size-picker.tsx b/web/src/components/image-size-picker.tsx index 1b9f7c0e8..94b871c8f 100644 --- a/web/src/components/image-size-picker.tsx +++ b/web/src/components/image-size-picker.tsx @@ -2,12 +2,13 @@ import { useState } from "react"; import "./image-size-picker.css"; import { Input, Button } from "antd"; import type { ImageCapabilityConfig } from "@/lib/model-capabilities"; -import { IMAGE_RESOLUTIONS, imagePresetForRatio, imagePresetValue, imageQualityForSelection, imageQualityForTier, imageResolutionUsesQuality, imageSizePresets, imageTierAvailable } from "@/lib/image-size-presets"; +import { imageResolutionTiers, imagePresetForRatio, imagePresetValue, imageQualityForSelection, imageQualityForTier, imageResolutionUsesQuality, imageSizePresets, imageTierAvailable } from "@/lib/image-size-presets"; import { buildImageResolutionOptions, type ImageResolutionOption, type ImageResolutionTier } from "@/lib/image-resolution-tiers"; import { resolveImageRequestSize, validateImageSize } from "@/services/api/image-validation"; export function ImageSizePicker({ profile, size, quality, onChange }: { profile: ImageCapabilityConfig; size: string; quality?: string; onChange: (size: string, quality?: string) => void }) { + const IMAGE_RESOLUTIONS = imageResolutionTiers(profile); const presets = imageSizePresets(profile).filter((preset) => imageTierAvailable(profile, preset.tier)); const visibleTiers = IMAGE_RESOLUTIONS.filter((value) => presets.some((preset) => preset.tier === value)); const [chosenTier, setChosenTier] = useState("1k"); diff --git a/web/src/components/image-size-presets-editor.tsx b/web/src/components/image-size-presets-editor.tsx index 2e933a069..732efe139 100644 --- a/web/src/components/image-size-presets-editor.tsx +++ b/web/src/components/image-size-presets-editor.tsx @@ -4,11 +4,12 @@ import { Check, Plus } from "lucide-react"; import { Switch } from "@/components/ui/base/switch"; import "./image-size-picker.css"; import type { ImageCapabilityConfig } from "@/lib/model-capabilities"; -import { IMAGE_RATIOS, IMAGE_RESOLUTIONS, imagePresetForRatio, imageResolutionUsesQuality, imageSizeConfigWithPresets, imageSizePresets, imageTierAvailable } from "@/lib/image-size-presets"; +import { IMAGE_RATIOS, imageResolutionTiers, imagePresetForRatio, imageResolutionUsesQuality, imageSizeConfigWithPresets, imageSizePresets, imageTierAvailable } from "@/lib/image-size-presets"; import { validateImageSize } from "@/services/api/image-validation"; import type { ImageResolutionTier } from "@/lib/image-resolution-tiers"; export function ImageSizePresetsEditor({ profile, disabled, onChange }: { profile: ImageCapabilityConfig; disabled?: boolean; onChange: (size: ImageCapabilityConfig["size"]) => void }) { + const IMAGE_RESOLUTIONS = imageResolutionTiers(profile); const presets = imageSizePresets(profile); const id = useId(); const [drafts, setDrafts] = useState>>({}); diff --git a/web/src/lib/image-resolution-tiers.ts b/web/src/lib/image-resolution-tiers.ts index 76a34b291..3bc5bd109 100644 --- a/web/src/lib/image-resolution-tiers.ts +++ b/web/src/lib/image-resolution-tiers.ts @@ -1,4 +1,4 @@ -export type ImageResolutionTier = "1k" | "2k" | "4k"; +export type ImageResolutionTier = "1k" | "1.5k" | "2k" | "4k"; export type ImageResolutionOption = { size: string; @@ -10,7 +10,7 @@ export type ImageResolutionOption = { export type ImageResolutionChoice = "auto" | ImageResolutionTier; -const tierOrder: ImageResolutionTier[] = ["1k", "2k", "4k"]; +const tierOrder: ImageResolutionTier[] = ["1k", "1.5k", "2k", "4k"]; const ratioOrder = ["1:1", "3:2", "2:3", "4:3", "3:4", "4:5", "5:4", "16:9", "9:16", "2:1", "1:2", "21:9"]; export function supportsImageResolutionPresets(size: { parameter: string; values: string[]; allowCustom?: boolean }) { diff --git a/web/src/lib/image-size-presets.ts b/web/src/lib/image-size-presets.ts index 645fb978f..6288e96d2 100644 --- a/web/src/lib/image-size-presets.ts +++ b/web/src/lib/image-size-presets.ts @@ -2,6 +2,10 @@ import { buildImageResolutionOptions, type ImageResolutionOption, type ImageReso import type { ImageCapabilityConfig } from "./model-capabilities"; export const IMAGE_RESOLUTIONS: ImageResolutionTier[] = ["1k", "2k", "4k"]; +// 1.5K 只在模型明确配置此档位时出现,既有模型的档位保持原有顺序。 +export function imageResolutionTiers(profile: ImageCapabilityConfig): ImageResolutionTier[] { + return profile.size.presets?.some((preset) => preset.tier === "1.5k") || profile.quality.values.includes("1.5k") ? ["1k", "1.5k", "2k", "4k"] : IMAGE_RESOLUTIONS; +} export const IMAGE_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9"]; const standardSizes: Record = { "1:1": ["1024x1024", "2048x2048", "2880x2880"], @@ -33,9 +37,9 @@ export function imagePresetForRatio(tier: ImageResolutionTier, input: string): I return width * h === height * w; }) || `${w}:${h}`; const index = IMAGE_RESOLUTIONS.indexOf(tier); - if (index < 0) throw new Error("分辨率仅支持 1K、2K、4K"); - const standard = standardSizes[ratio]?.[index]; - const pixels = [1_048_576, 4_194_304, 8_294_400][index]; + if (index < 0 && tier !== "1.5k") throw new Error("分辨率仅支持 1K、1.5K、2K、4K"); + const standard = tier === "1.5k" ? undefined : standardSizes[ratio]?.[index]; + const pixels = tier === "1.5k" ? 2_359_296 : [1_048_576, 4_194_304, 8_294_400][index]; let width = Math.round(Math.sqrt((pixels * w) / h) / 16) * 16; let height = Math.round((width * h) / w / 16) * 16; while (width * height > pixels || Math.max(width, height) > 3840) { @@ -52,12 +56,12 @@ export function imagePresetForRatio(tier: ImageResolutionTier, input: string): I } export function imageQualityForTier(profile: ImageCapabilityConfig, tier: ImageResolutionTier) { - const aliases = { "1k": ["1k", "low"], "2k": ["2k", "medium"], "4k": ["4k", "high"] }[tier]; + const aliases = { "1k": ["1k", "low"], "1.5k": ["1.5k"], "2k": ["2k", "medium"], "4k": ["4k", "high"] }[tier]; return profile.quality.supported ? profile.quality.values.find((value) => aliases.includes(value.toLowerCase())) : undefined; } export function imageResolutionUsesQuality(profile: ImageCapabilityConfig) { - return profile.size.parameter === "aspect_ratio" && IMAGE_RESOLUTIONS.some((tier) => imageQualityForTier(profile, tier)); + return profile.size.parameter === "aspect_ratio" && imageResolutionTiers(profile).some((tier) => imageQualityForTier(profile, tier)); } export function imageTierAvailable(profile: ImageCapabilityConfig, tier: ImageResolutionTier) { @@ -82,7 +86,7 @@ export function imageSizePresets(profile: ImageCapabilityConfig): ImageResolutio if (profile.size.parameter === "none") return []; if (profile.size.presets) return profile.size.presets; const pixels = buildImageResolutionOptions(profile.size.values); - const tiers = profile.size.parameter === "aspect_ratio" ? IMAGE_RESOLUTIONS.filter((tier) => imageQualityForTier(profile, tier)) : []; + const tiers = profile.size.parameter === "aspect_ratio" ? imageResolutionTiers(profile).filter((tier) => imageQualityForTier(profile, tier)) : []; const ratios = tiers.flatMap((tier) => profile.size.values.flatMap((ratio) => { try { diff --git a/web/src/lib/kemei-image-presets.ts b/web/src/lib/kemei-image-presets.ts new file mode 100644 index 000000000..58c759ba5 --- /dev/null +++ b/web/src/lib/kemei-image-presets.ts @@ -0,0 +1,17 @@ +import type { ImageResolutionOption, ImageResolutionTier } from "./image-resolution-tiers"; + +// Seedream 5.0 官方像素表;不能按通用像素阈值把 2K 横屏误分为 4K。 +export function kemeiSeedreamPresets(): ImageResolutionOption[] { + const ratios = ["1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3", "21:9"]; + const tiers: [ImageResolutionTier, string[]][] = [ + ["1k", ["1024x1024", "1152x864", "864x1152", "1424x800", "800x1424", "1248x832", "832x1248", "1568x672"]], + ["1.5k", ["1536x1536", "1792x1344", "1344x1792", "2048x1152", "1152x2048", "1872x1248", "1248x1872", "2352x1008"]], + ["2k", ["2048x2048", "2368x1776", "1776x2368", "2816x1584", "1584x2816", "2496x1664", "1664x2496", "3136x1344"]], + ]; + return tiers.flatMap(([tier, sizes]) => + sizes.map((size, index) => { + const [width, height] = size.split("x").map(Number); + return { tier, ratio: ratios[index], width, height, size }; + }), + ); +} diff --git a/web/src/lib/model-capabilities.ts b/web/src/lib/model-capabilities.ts index 97f5acdd8..e25ab68fe 100644 --- a/web/src/lib/model-capabilities.ts +++ b/web/src/lib/model-capabilities.ts @@ -1,6 +1,7 @@ import type { ModelProtocol, ModelProtocolWorkflow } from "@/lib/model-protocols"; import type { ImageResolutionOption, ImageResolutionTier } from "@/lib/image-resolution-tiers"; import { imagePresetForRatio } from "./image-size-presets"; +import { kemeiSeedreamPresets } from "./kemei-image-presets"; import { type WorkflowVideoFieldLike, workflowImageCapabilityConfig, workflowVideoCapabilityConfig } from "./model-capabilities-workflow"; import { resolveWorkflowVideoScreenSpec } from "./video-screen-specs"; @@ -220,6 +221,19 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = " outputFormat: { supported: true }, maxOutputs: 15, }; + if (protocol === "km-kemei-seedream") { + const presets = kemeiSeedreamPresets(); + image.references = { ...image.references, maxImages: 10, maskSupported: false }; + image.size = { parameter: "size", values: presets.map((preset) => preset.size), default: "2048x2048", allowCustom: true, presets }; + image.quality = { supported: false, values: [], default: "auto" }; + // 透明输出要求一张含 alpha 的参考图,宿主通用开关不能保证此条件。 + // 此能力通过插件命名空间的 background + output_format 显式配置。 + image.transparentBackground = { supported: false, default: false }; + image.responseFormat = { supported: true }; + image.outputFormat = { supported: true }; + image.maxOutputs = 1; + return image; + } if (protocol === "kacang-midjourney-special" || protocol === "kacang-midjourney-v7" || protocol === "kacang-midjourney") { const stable = protocol === "kacang-midjourney"; const extendedRatios = stable || protocol === "kacang-midjourney-v7"; @@ -493,7 +507,7 @@ export function normalizeImageValue(profile: ImageCapabilityConfig, value: { siz : value.quality && profile.quality.values.includes(value.quality) ? value.quality : profile.quality.default || "auto" - : requestedQuality === "1k" || requestedQuality === "2k" || requestedQuality === "4k" + : requestedQuality === "1k" || requestedQuality === "1.5k" || requestedQuality === "2k" || requestedQuality === "4k" ? requestedQuality : presetTier || profile.quality.default || "auto"; const count = String(Math.max(1, Math.min(profile.maxOutputs, Math.floor(Math.abs(Number(value.count)) || 1)))); diff --git a/web/src/services/api/logical-models.ts b/web/src/services/api/logical-models.ts index 0679b9a92..f49be1438 100644 --- a/web/src/services/api/logical-models.ts +++ b/web/src/services/api/logical-models.ts @@ -1,15 +1,10 @@ import { http } from "@/services/api/request"; import type { ModelTag } from "@/lib/model-tags"; +import type { ImageResolutionOption } from "@/lib/image-resolution-tiers"; export type InputConstraint = { min: number; max: number }; export type OptionConstraint = { values?: unknown[]; min?: number; max?: number; step?: number }; -export type CapabilityImageSizePreset = { - size: string; - tier: "1k" | "2k" | "4k"; - ratio: string; - width: number; - height: number; -}; +export type CapabilityImageSizePreset = ImageResolutionOption; export type CapabilityImageSize = { parameter?: "size" | "aspect_ratio"; allowCustom?: boolean; diff --git a/web/test/channel-model-editor-form.test.ts b/web/test/channel-model-editor-form.test.ts index 62340048b..06ae67b25 100644 --- a/web/test/channel-model-editor-form.test.ts +++ b/web/test/channel-model-editor-form.test.ts @@ -9,6 +9,13 @@ const protocols = [definition("disabled-text", "text", false), definition("text" const protocolsWithAudio = [...protocols, definition("audio", "audio")]; describe("channel model editor drafts", () => { + test("Kemei Seedream selection initializes the image profile and preserves pricing", () => { + const draft = initialChannelModelValues(null, protocols); + draft.priceTiers[0].unitPrice = 19; + const next = changeChannelModelCapability({ ...draft, capability: "image", protocol: "km-kemei-seedream", modelKey: "doubao-seedream-5-0-pro-260628" }, [definition("km-kemei-seedream", "image")]); + expect(next.capabilityConfig?.image).toMatchObject({ references: { maxImages: 10, maskSupported: false }, maxOutputs: 1, size: { default: "2048x2048" } }); + expect(next.priceTiers[0].unitPrice).toBe(19); + }); test("new drafts select an enabled protocol and never share price state", () => { const first = initialChannelModelValues(null, protocols); first.priceTiers[0].unitPrice = 42; diff --git a/web/test/image-size-presets.test.tsx b/web/test/image-size-presets.test.tsx index d3c7d2e71..d56d1edc4 100644 --- a/web/test/image-size-presets.test.tsx +++ b/web/test/image-size-presets.test.tsx @@ -17,6 +17,19 @@ describe("统一图片分辨率与宽高比", () => { expect(imageQualityLabel("MAX")).toBe("最高"); }); + test("Seedream 5.0 shows 1.5K and retains the exact official 2K wide size", () => { + const profile = defaultImageCapabilityConfig("km-kemei-seedream", "doubao-seedream-5-0-pro-260628"); + const intermediate = renderToStaticMarkup( {}} />); + expect(intermediate).toMatch(/aria-pressed="true"[^>]*>1\.5K/); + expect(intermediate).toContain("1536 × 1536 px"); + expect(intermediate).not.toMatch(/]*>4K/); + const wide = renderToStaticMarkup( {}} />); + expect(wide).toMatch(/aria-pressed="true"[^>]*>2K/); + expect(wide).toContain("2816 × 1584 px"); + const reopened = normalizeModelCapabilityConfig({ version: 1, image: profile }); + expect(reopened.image?.size.presets?.filter((p) => p.tier === "1.5k")).toHaveLength(8); + expect(resolveImageRequestSize(profile, "auto", "1536x1536")?.value).toBe("1536x1536"); + }); test("管理员档位开关按已有比例启用,部分比例不显示为关闭", () => { for (const model of ["midjourney-1k", "midjourney-2k"]) { const profile = defaultImageCapabilityConfig("cangyuan-midjourney-v82", model); diff --git a/web/test/model-capabilities.test.ts b/web/test/model-capabilities.test.ts index b4b9e07c1..59e36dc36 100644 --- a/web/test/model-capabilities.test.ts +++ b/web/test/model-capabilities.test.ts @@ -3,7 +3,23 @@ import test from "node:test"; // Bun 直接执行 TypeScript 测试时需要保留扩展名;生产 tsconfig 不包含 test/。 import { DEFAULT_VIDEO_PROMPT_MAX_CHARS, defaultModelCapabilityConfig, normalizeImageValue, normalizeVideoValue } from "../src/lib/model-capabilities.ts"; -import { imageTierAvailable } from "../src/lib/image-size-presets.ts"; +import { imageTierAvailable, imageResolutionTiers } from "../src/lib/image-size-presets.ts"; + +test("Kemei Seedream defaults retain official 1K/1.5K/2K sizes without a 4K preset", () => { + for (const model of ["doubao-seedream-5-0-pro-260628", "doubao-seedream-5-0-flash-260915"]) { + const image = defaultModelCapabilityConfig("km-kemei-seedream", model).image!; + assert.equal(image.references.maxImages, 10); + assert.equal(image.references.maskSupported, false); + assert.equal(image.maxOutputs, 1); + assert.equal(image.size.default, "2048x2048"); + assert.equal(image.quality.supported, false); + assert.equal(image.size.presets!.length, 24); + assert.deepEqual([...new Set(image.size.presets!.map((p) => p.tier))], ["1k", "1.5k", "2k"]); + assert.equal(image.size.presets!.find((p) => p.size === "2816x1584")?.tier, "2k"); + assert.ok(imageResolutionTiers(image).includes("1.5k")); + } + assert.deepEqual(imageResolutionTiers(defaultModelCapabilityConfig("openai-image").image!), ["1k", "2k", "4k"]); +}); test("text multimodal capability is not guessed from a model name", () => { for (const model of ["gpt-4o", "gemini-2.5-pro", "doubao-seed"]) {