diff --git a/.github/pr-screenshots/agnes-image/01-capability-params.png b/.github/pr-screenshots/agnes-image/01-capability-params.png new file mode 100644 index 000000000..054235d8c Binary files /dev/null and b/.github/pr-screenshots/agnes-image/01-capability-params.png differ diff --git a/backend/internal/protocol/agnes_image_test.go b/backend/internal/protocol/agnes_image_test.go new file mode 100644 index 000000000..d17282a4e --- /dev/null +++ b/backend/internal/protocol/agnes_image_test.go @@ -0,0 +1,162 @@ +package protocol + +import ( + "context" + "testing" +) + +// Agnes 图像端点是同步的,文生图与图生图共用 POST /v1/images/generations。 +// 官方参数表只承认 model、prompt、size(必填)、ratio、image、return_base64 与 +// extra_body:顶层 response_format 是官方明确列出的错误写法,output_format 会被 +// 直接拒绝,n 也不在参数表内。因此这里逐字段固定线协议,防止退回 OpenAI 兼容字段。 +func TestAgnesImageSendsOnlyDocumentedFields(t *testing.T) { + adapter := officialPackageAdapter(t, "agnes-image.yingce-plugin", "agnes-image") + if adapter.Metadata().RequiresPublicMediaURLs { + t.Fatal("Agnes 图像支持 Data URI 参考图,不应强制要求公共媒体 URL") + } + create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{ + Model: "agnes-image-2.1-flash", Prompt: "海边写真的亚洲女模特", AspectRatio: "3:4", Quality: "2k", ImageCount: 4, + }}) + if err != nil { + t.Fatal(err) + } + if create.Method != "POST" || create.Path != "/v1/images/generations" { + t.Fatalf("create = %#v", create) + } + // 原生端点在域名根下,originPath 让请求路径替换 Base URL 的路径段, + // 渠道沿用 .../v1 也能命中正确地址。 + if !create.OriginPath { + t.Fatal("create 必须声明 originPath") + } + + body := manifestTestBody(t, create) + if body["model"] != "agnes-image-2.1-flash" || body["prompt"] != "海边写真的亚洲女模特" { + t.Fatalf("body = %#v", body) + } + if body["size"] != "2K" { + t.Fatalf("size = %#v, 期望按质量档位发送 2K", body["size"]) + } + if body["ratio"] != "3:4" { + t.Fatalf("ratio = %#v, 期望独立的 ratio 字段", body["ratio"]) + } + // 官方参数表之外的字段会被上游按“不支持该字段”拒绝,逐个钉死。 + for _, forbidden := range []string{"n", "output_format", "quality", "background", "response_format", "image", "mask", "style", "user"} { + if _, present := body[forbidden]; present { + t.Fatalf("body = %#v, 官方参数表没有 %s 字段,不能发送", body, forbidden) + } + } + + extra, ok := body["extra_body"].(map[string]any) + if !ok { + t.Fatalf("extra_body = %#v", body["extra_body"]) + } + if extra["response_format"] != "url" { + t.Fatalf("extra_body.response_format = %#v, 输出格式只能声明在 extra_body 内", extra["response_format"]) + } + if _, present := extra["image"]; present { + t.Fatalf("extra_body = %#v, 文生图不应发送 image", extra) + } +} + +func TestAgnesImagePutsReferencesInExtraBody(t *testing.T) { + adapter := officialPackageAdapter(t, "agnes-image.yingce-plugin", "agnes-image") + create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{ + Model: "agnes-image-2.1-flash", Prompt: "换装并保留原始构图", AspectRatio: "3:4", Quality: "1k", + Images: []MediaReference{ + {URL: "https://cdn.example/anchor.png", Role: "edit_source", Order: 0}, + {DataURL: "data:image/png;base64,QUJD", Role: "edit_source", Order: 1}, + }, + }}) + if err != nil { + t.Fatal(err) + } + + body := manifestTestBody(t, create) + if _, present := body["image"]; present { + t.Fatalf("body = %#v, 参考图必须放在 extra_body.image,而不是顶层 image", body) + } + extra, _ := body["extra_body"].(map[string]any) + images, ok := extra["image"].([]any) + if !ok || len(images) != 2 { + t.Fatalf("extra_body.image = %#v, 期望两张参考图", extra["image"]) + } + if images[0] != "https://cdn.example/anchor.png" { + t.Fatalf("第一张 = %#v, order 决定顺序", images[0]) + } + // 有 Data URL 时优先 Base64:Agnes 明确支持 Data URI,本地开发无法提供公共图片地址。 + if images[1] != "data:image/png;base64,QUJD" { + t.Fatalf("第二张 = %#v, 有 dataUrl 时必须优先 Base64", images[1]) + } + if body["size"] != "1K" { + t.Fatalf("size = %#v, 图生图同样必须携带必填的 size", body["size"]) + } +} + +func TestAgnesImageSizeFollowsPixelOrTier(t *testing.T) { + adapter := officialPackageAdapter(t, "agnes-image.yingce-plugin", "agnes-image") + tests := []struct { + name string + aspectRatio string + quality string + wantSize string + wantRatio string + }{ + {name: "比例落到分辨率档位", aspectRatio: "16:9", quality: "4k", wantSize: "4K", wantRatio: "16:9"}, + {name: "像素尺寸原样透传", aspectRatio: "1024x768", quality: "2k", wantSize: "1024x768"}, + {name: "auto 也必须落到档位", aspectRatio: "auto", quality: "1k", wantSize: "1K"}, + {name: "空比例回落到默认档位", aspectRatio: "", quality: "", wantSize: "1K"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{ + Model: "agnes-image-2.1-flash", Prompt: "circle", AspectRatio: test.aspectRatio, Quality: test.quality, + }}) + if err != nil { + t.Fatal(err) + } + body := manifestTestBody(t, create) + if body["size"] != test.wantSize { + t.Fatalf("size = %#v, 期望 %q", body["size"], test.wantSize) + } + gotRatio, present := body["ratio"] + if test.wantRatio == "" { + if present { + t.Fatalf("ratio = %#v, 非比例输入不得发送 ratio", gotRatio) + } + return + } + if gotRatio != test.wantRatio { + t.Fatalf("ratio = %#v, 期望 %q", gotRatio, test.wantRatio) + } + }) + } +} + +func TestAgnesImageRejectsUnsupportedRatio(t *testing.T) { + adapter := officialPackageAdapter(t, "agnes-image.yingce-plugin", "agnes-image") + // 9:21 / 4:5 等比例不在官方比例表内,静默回落到 1:1 会让用户拿到与选择不符的画幅。 + if _, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{ + Model: "agnes-image-2.1-flash", Prompt: "x", AspectRatio: "9:21", + }}); err == nil { + t.Fatal("9:21 不在官方比例表内,必须在校验阶段拒绝") + } +} + +func TestAgnesImageReadsUrlAndBase64Output(t *testing.T) { + adapter := officialPackageAdapter(t, "agnes-image.yingce-plugin", "agnes-image") + result, err := adapter.ParseCreate(context.Background(), []byte(`{"created":1780000000,"data":[{"url":"https://storage.googleapis.com/agnes-aigc/x.png","b64_json":null}]}`)) + if err != nil { + t.Fatal(err) + } + if result.Result == nil || len(result.Result.Images) != 1 || result.Result.Images[0].URL != "https://storage.googleapis.com/agnes-aigc/x.png" { + t.Fatalf("images = %#v", result.Result) + } + + inline, err := adapter.ParseCreate(context.Background(), []byte(`{"data":[{"url":null,"b64_json":"aW1hZ2U="}]}`)) + if err != nil { + t.Fatal(err) + } + if inline.Result == nil || len(inline.Result.Images) != 1 || inline.Result.Images[0].DataURL != "data:image/png;base64,aW1hZ2U=" { + t.Fatalf("images = %#v", inline.Result) + } +} diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 2d0fc3fdb..7fb8faa7a 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -138,6 +138,17 @@ description: 当前项目已实现功能的手工验证清单与未确认边界 - 已知边界:协议用 `originPath` 声明请求路径,请求路径会替换渠道 Base URL 的路径段,因此渠道继续沿用 `.../compatible-mode/v1` 也能命中原生端点(`.../api/v1/services/...`);只有域名需要正确。协议把 `size` 映射为 1024 级竖版/方形档位,未登记的档位会被省略并交由模型自动推荐(官方要求像素面积 512*512~2048*2048)。 - 尚未处理:AI 模特页仍按“带参考图即走 `/images/edits`”的通用规则分支,未按协议能力判断;换成该插件后两阶段派生能否直接复用需实测确认。 +## Agnes 图像协议插件(agnes-image) + +- 新增官方协议包 `plugin-packages/agnes-image.yingce-plugin`,解决 Agnes 图片模型无法调用的问题:内置 OpenAI 图片路径会固定发送 `n`,并按渠道能力开关发送 `output_format` 与顶层 `response_format`,而 Agnes 的参数表没有这些字段,`POST /v1/images/generations` 直接返回 HTTP 400 `invalid_request`,上游原话为“output_format 不是文生图队列支持的字段”。`n` 无法通过任何开关摘除,因此必须独立协议。 +- 协议形态:同步 `POST /v1/images/generations`(`originPath`),文生图与图生图共用同一入口。`size` 必填,由统一层质量归一为 `1K`/`2K`/`3K`/`4K` 档位,`WxH` 像素尺寸原样透传;画面比例走独立的 `ratio` 字段;参考图放 `extra_body.image` 并优先 Base64 data URL,多图合成传多张,不使用 `/v1/images/edits` multipart;输出格式只能声明在 `extra_body.response_format`(默认 `url`,可用 `providerOptions.agnes-image.response_format` 覆盖为 `b64_json`)。 +- 前端能力默认值同步新增 `agnes-image` 分支:尺寸按“比例 + 1k/2k/4k 档位”选择,关闭上游不支持的 `output_format`、顶层 `response_format`、透明背景和蒙版编辑,`maxOutputs` 固定为 1(该端点不接受 `n`,单次请求固定返回一张)。官方另有 `3K` 档位,统一层只提供 1k/2k/4k 三档,故界面不露出。 +- 已由单元测试覆盖:逐项断言 `n`、`output_format`、`quality`、`background`、顶层 `response_format` 与顶层 `image` 均不出现在请求体中,参考图进 `extra_body.image` 且按 order 排序并优先 Base64,比例落档位 / 像素原样透传 / `auto` 与空值回落档位,不在官方比例表内的取值在校验阶段拒绝,响应同时读取 `data[0].url` 与 `data[0].b64_json`。`go test ./internal/protocol/`、`go build ./...`、`bun run typecheck` 与相关前端测试均通过。 +- 已完成真机验收(Agnes 中国站 Key,`agnes-image-2.1-flash` 与 `agnes-image-2.5-flash` 各一轮,AI 模特页):母版与派生任务全部 `succeeded`,`portrait_role` 分别为 `anchor` 与 `derive` 并拿到 Agnes 的 `task_*` 号;调用日志里母版对应 `/images/t2i/...`、派生对应 `/images/i2i/...`,且两张派生图都被宿主成功下载。派生阶段能产出 i2i 结果,证明参考图确实经 `extra_body.image` 送达——内置 OpenAI 路径只会把参考图送去 Agnes 没有的 `/images/edits`,因此这条链路只可能由本插件完成。 +- 待验收:多图合成(单次传多张参考图)、`providerOptions.agnes-image.response_format=b64_json` 覆盖、以及 `size`/`ratio` 的实际取值。任务行只持久化 `mode` 与 `metadata`,调用日志也不记录请求体,因此档位与比例发出去的具体值无法从库中读回,需要抓包或在插件侧临时打印才能确认,目前只能由上游行为反推。 +- 渠道配置注意:模型协议改为本插件后必须重启后端,官方插件目录只在启动时装载;已保存过的 `capabilityConfig` 不会被新协议默认值覆盖,需要手动把尺寸参数改为 `aspect_ratio`、最大参考图设回大于 0,否则 AI 模特会退化为不带参考图的单阶段候选图。 +- 已知边界:默认输出 URL,由宿主立即下载并持久化;若上游图片地址无法公开回源,改用 `providerOptions.agnes-image.response_format=b64_json`。官方比例表只有 8 种,其他比例(如 `4:5`、`9:21`)在请求前即被拒绝,不会静默回落为 1:1。 + ## 云端 Agent 行为、能力和偏好分层(待真实环境验收) - [x] 系统/媒体行为资源由后端发布物加载,运行快照记录策略和能力版本/哈希;用户 → 项目 → 画布偏好经归属校验和独立 revision CAS,合并 revision 固定到新轮次,旧轮次保留原快照。system prompt 只注入偏好清单,正文由 `agent_profile_read` 从固定快照按层读取。工具 schema、节点类型、可写字段和画布读取来自服务端节点能力 registry;收费生成还需实际媒体任务适配器。写入、审批与预算仍以服务端强校验为准。 diff --git a/plugin-packages/agnes-image.yingce-plugin b/plugin-packages/agnes-image.yingce-plugin new file mode 100644 index 000000000..c4b28a97c Binary files /dev/null and b/plugin-packages/agnes-image.yingce-plugin differ diff --git a/plugin-packages/agnes-image/README.md b/plugin-packages/agnes-image/README.md new file mode 100644 index 000000000..1772eb71d --- /dev/null +++ b/plugin-packages/agnes-image/README.md @@ -0,0 +1,5 @@ +# Agnes Image + +该目录是独立官方协议插件源码。后端从生成的 `agnes-image.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。 + +完整接口见 [docs/interface.md](docs/interface.md)。 diff --git a/plugin-packages/agnes-image/docs/interface.md b/plugin-packages/agnes-image/docs/interface.md new file mode 100644 index 000000000..744c04547 --- /dev/null +++ b/plugin-packages/agnes-image/docs/interface.md @@ -0,0 +1,462 @@ +# Agnes Image 接口字段 + +## 协议身份 + +- 插件 ID:`agnes-image`。 +- Provider ID:`agnes-image`。 +- 能力:`image`。 +- 默认 Base URL:`https://api.agnes-ai.cn`。 +- 鉴权驱动:`bearer`。 +- 创建:`POST /v1/images/generations`。 +- 生命周期:同步响应。 + +## 配置字段 + +| 字段 | 类型 | 必填 | 含义 | +| --- | --- | --- | --- | +| `apiKey` | secret | 是 | API Key | + +## 统一字段映射 + +| 统一字段 | 类型 | 必填 | 上游映射 | 说明 | +| --- | --- | --- | --- | --- | +| `model` | string | 是 | `model` | 图片模型 ID。 | +| `prompt` | string | 是 | `prompt` | 图片提示词。 | +| `images` | media[] | 否 | `provider image/reference fields` | 参考图或编辑源图,role 由业务层确定。 | +| `imageCount` | integer | 否 | `n/sample_count` | 输出数量。 | +| `aspectRatio` | string | 否 | `size/aspect_ratio` | 比例或尺寸,语义按协议说明。 | +| `resolution` | string | 否 | `resolution/imageSize` | 分辨率档位。 | +| `quality` | string | 否 | `quality` | 质量档位。 | +| `providerOptions` | object | 否 | `provider-specific fields` | 插件命名空间内的厂商扩展字段。 | + +## 上游请求模板逐字段清单 + +下表由插件请求模板生成,覆盖 body、query、headers 和 multipart 文件声明中的每个字段。 + +| 上游位置 | 值或转换表达式 | +| --- | --- | +| `create.method` | `"POST"` | +| `create.path` | `"/v1/images/generations"` | +| `create.contentType` | `"application/json"` | +| `create.body.model` | `{"$ref":"request.model"}` | +| `create.body.prompt` | `{"$ref":"request.prompt"}` | +| `create.body.size` | `{"$omitEmpty":{"$if":{"condition":{"$eq":[{"$len":{"$split":[{"$trim":{"$ref":"request.aspectRatio"}},"x"]}},2]},"then":{"$trim":{"$ref":"request.aspectRatio"}},"else":{"$switch":{"cases":[{"when":{"$in":[{"$lower":{"$trim":{"$ref":"request.quality"}}},["1k","low","standard"]]},"then":"1K"},{"when":{"$in":[{"$lower":{"$trim":{"$ref":"request.quality"}}},["2k","medium","hd","high"]]},"then":"2K"},{"when":{"$in":[{"$lower":{"$trim":{"$ref":"request.quality"}}},["3k"]]},"then":"3K"},{"when":{"$in":[{"$lower":{"$trim":{"$ref":"request.quality"}}},["4k"]]},"then":"4K"}],"default":"1K"}}}}}` | +| `create.body.ratio` | `{"$omitEmpty":{"$if":{"condition":{"$in":[{"$trim":{"$ref":"request.aspectRatio"}},["1:1","3:4","4:3","16:9","9:16","2:3","3:2","21:9"]]},"then":{"$trim":{"$ref":"request.aspectRatio"}},"else":null}}}` | +| `create.body.extra_body` | `{"$omitEmpty":{"image":{"$omitEmpty":{"$map":{"from":{"$sortByOrder":{"$ref":"request.images"}},"as":"media","in":{"$coalesce":[{"$ref":"media.dataUrl"},{"$ref":"media.url"}]}}}},"response_format":{"$coalesce":[{"$ref":"request.providerOptions.agnes-image.response_format"},"url"]}}}` | + +## Provider 扩展键 + +- `providerOptions.agnes-image.response_format` + +动态模型或工作流允许使用文档声明的完整 `parameters/input/extra_body` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。 + +## 响应映射逐字段清单 + +| 映射位置 | 上游路径或转换表达式 | +| --- | --- | +| `response.status` | `"succeeded"` | +| `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}}}}}` | +| `response.errorPaths[0]` | `"error.code"` | +| `response.errorPaths[1]` | `"code"` | +| `response.messagePaths[0]` | `"error.message"` | +| `response.messagePaths[1]` | `"message"` | +| `response.messagePaths[2]` | `"msg"` | + +## 响应与错误 + +插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。 + +## 兼容边界 + +Agnes 官方图像端点,同步返回。文生图与图生图共用 /v1/images/generations:不传 image 为文生图,传 image 为图生图或多图合成。size 必填,取 1K/2K/3K/4K 档位或 WxH 精确尺寸;画面比例走独立的 ratio 字段。参考图放在 extra_body.image,支持公共 HTTPS URL 或 Data URI Base64。顶层 response_format 是官方明确列出的错误写法,输出格式只能声明在 extra_body.response_format。该端点不接受 n,单次请求固定返回一张图片,需要多张时由上层拆分为多个任务。 + + +## Manifest 完整接口定义 + +以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。 + +```json +{ + "apiVersion": "yingce.plugin/v2", + "id": "agnes-image", + "name": "Agnes Image", + "version": "2.0.0", + "author": "Agnes AI / 影策", + "description": "Agnes Image 独立请求协议插件。", + "documentation": "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>", + "permissions": [ + "generation.run", + "media.read" + ], + "configuration": { + "fields": [ + { + "name": "apiKey", + "type": "secret", + "label": "API Key", + "required": true + } + ] + }, + "contributes": { + "providers": [ + { + "id": "agnes-image", + "label": "Agnes Image", + "capabilities": [ + "image" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://api.agnes-ai.cn", + "requiresPublicMediaUrls": false, + "auth": { + "type": "bearer", + "field": "apiKey" + }, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "图片模型 ID。" + }, + { + "name": "prompt", + "type": "string", + "required": true, + "mapping": "prompt", + "description": "图片提示词。" + }, + { + "name": "images", + "type": "media[]", + "required": false, + "mapping": "provider image/reference fields", + "description": "参考图或编辑源图,role 由业务层确定。" + }, + { + "name": "imageCount", + "type": "integer", + "required": false, + "mapping": "n/sample_count", + "description": "输出数量。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "size/aspect_ratio", + "description": "比例或尺寸,语义按协议说明。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/imageSize", + "description": "分辨率档位。" + }, + { + "name": "quality", + "type": "string", + "required": false, + "mapping": "quality", + "description": "质量档位。" + }, + { + "name": "providerOptions", + "type": "object", + "required": false, + "mapping": "provider-specific fields", + "description": "插件命名空间内的厂商扩展字段。" + } + ], + "validations": [ + { + "assert": { + "$or": [ + { + "$in": [ + { + "$trim": { + "$ref": "request.aspectRatio" + } + }, + [ + "", + "auto", + "1:1", + "3:4", + "4:3", + "16:9", + "9:16", + "2:3", + "3:2", + "21:9" + ] + ] + }, + { + "$eq": [ + { + "$len": { + "$split": [ + { + "$trim": { + "$ref": "request.aspectRatio" + } + }, + "x" + ] + } + }, + 2 + ] + } + ] + }, + "message": "Agnes 图像只支持 1:1、3:4、4:3、16:9、9:16、2:3、3:2、21:9 比例或 WxH 像素尺寸" + } + ], + "create": { + "method": "POST", + "path": "/v1/images/generations", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "size": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$len": { + "$split": [ + { + "$trim": { + "$ref": "request.aspectRatio" + } + }, + "x" + ] + } + }, + 2 + ] + }, + "then": { + "$trim": { + "$ref": "request.aspectRatio" + } + }, + "else": { + "$switch": { + "cases": [ + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$ref": "request.quality" + } + } + }, + [ + "1k", + "low", + "standard" + ] + ] + }, + "then": "1K" + }, + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$ref": "request.quality" + } + } + }, + [ + "2k", + "medium", + "hd", + "high" + ] + ] + }, + "then": "2K" + }, + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$ref": "request.quality" + } + } + }, + [ + "3k" + ] + ] + }, + "then": "3K" + }, + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$ref": "request.quality" + } + } + }, + [ + "4k" + ] + ] + }, + "then": "4K" + } + ], + "default": "1K" + } + } + } + } + }, + "ratio": { + "$omitEmpty": { + "$if": { + "condition": { + "$in": [ + { + "$trim": { + "$ref": "request.aspectRatio" + } + }, + [ + "1:1", + "3:4", + "4:3", + "16:9", + "9:16", + "2:3", + "3:2", + "21:9" + ] + ] + }, + "then": { + "$trim": { + "$ref": "request.aspectRatio" + } + }, + "else": null + } + } + }, + "extra_body": { + "$omitEmpty": { + "image": { + "$omitEmpty": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$coalesce": [ + { + "$ref": "media.dataUrl" + }, + { + "$ref": "media.url" + } + ] + } + } + } + }, + "response_format": { + "$coalesce": [ + { + "$ref": "request.providerOptions.agnes-image.response_format" + }, + "url" + ] + } + } + } + }, + "originPath": true + }, + "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 + } + } + } + } + }, + "errorPaths": [ + "error.code", + "code" + ], + "messagePaths": [ + "error.message", + "message", + "msg" + ] + } + } + ] + } +} +``` + diff --git a/plugin-packages/agnes-image/manifest.json b/plugin-packages/agnes-image/manifest.json new file mode 100644 index 000000000..d31273093 --- /dev/null +++ b/plugin-packages/agnes-image/manifest.json @@ -0,0 +1,382 @@ +{ + "apiVersion": "yingce.plugin/v2", + "id": "agnes-image", + "name": "Agnes Image", + "version": "2.0.0", + "author": "Agnes AI / 影策", + "description": "Agnes Image 独立请求协议插件。", + "documentation": "# Agnes Image\n\n该目录是独立官方协议插件源码。后端从生成的 `agnes-image.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。\n\n完整接口见 [docs/interface.md](docs/interface.md)。\n\n---\n\n# Agnes Image 接口字段\n\n## 协议身份\n\n- 插件 ID:`agnes-image`。\n- Provider ID:`agnes-image`。\n- 能力:`image`。\n- 默认 Base URL:`https://api.agnes-ai.cn`。\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.size` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$eq\":[{\"$len\":{\"$split\":[{\"$trim\":{\"$ref\":\"request.aspectRatio\"}},\"x\"]}},2]},\"then\":{\"$trim\":{\"$ref\":\"request.aspectRatio\"}},\"else\":{\"$switch\":{\"cases\":[{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"1k\",\"low\",\"standard\"]]},\"then\":\"1K\"},{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"2k\",\"medium\",\"hd\",\"high\"]]},\"then\":\"2K\"},{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"3k\"]]},\"then\":\"3K\"},{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"4k\"]]},\"then\":\"4K\"}],\"default\":\"1K\"}}}}}` |\n| `create.body.ratio` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$in\":[{\"$trim\":{\"$ref\":\"request.aspectRatio\"}},[\"1:1\",\"3:4\",\"4:3\",\"16:9\",\"9:16\",\"2:3\",\"3:2\",\"21:9\"]]},\"then\":{\"$trim\":{\"$ref\":\"request.aspectRatio\"}},\"else\":null}}}` |\n| `create.body.extra_body` | `{\"$omitEmpty\":{\"image\":{\"$omitEmpty\":{\"$map\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.images\"}},\"as\":\"media\",\"in\":{\"$coalesce\":[{\"$ref\":\"media.dataUrl\"},{\"$ref\":\"media.url\"}]}}}},\"response_format\":{\"$coalesce\":[{\"$ref\":\"request.providerOptions.agnes-image.response_format\"},\"url\"]}}}` |\n\n## Provider 扩展键\n\n- `providerOptions.agnes-image.response_format`\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.errorPaths[0]` | `\"error.code\"` |\n| `response.errorPaths[1]` | `\"code\"` |\n| `response.messagePaths[0]` | `\"error.message\"` |\n| `response.messagePaths[1]` | `\"message\"` |\n| `response.messagePaths[2]` | `\"msg\"` |\n\n## 响应与错误\n\n插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。\n\n## 兼容边界\n\nAgnes 官方图像端点,同步返回。文生图与图生图共用 /v1/images/generations:不传 image 为文生图,传 image 为图生图或多图合成。size 必填,取 1K/2K/3K/4K 档位或 WxH 精确尺寸;画面比例走独立的 ratio 字段。参考图放在 extra_body.image,支持公共 HTTPS URL 或 Data URI Base64。顶层 response_format 是官方明确列出的错误写法,输出格式只能声明在 extra_body.response_format。该端点不接受 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\": \"agnes-image\",\n \"name\": \"Agnes Image\",\n \"version\": \"2.0.0\",\n \"author\": \"Agnes AI / 影策\",\n \"description\": \"Agnes Image 独立请求协议插件。\",\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\": \"agnes-image\",\n \"label\": \"Agnes 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.agnes-ai.cn\",\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 \"validations\": [\n {\n \"assert\": {\n \"$or\": [\n {\n \"$in\": [\n {\n \"$trim\": {\n \"$ref\": \"request.aspectRatio\"\n }\n },\n [\n \"\",\n \"auto\",\n \"1:1\",\n \"3:4\",\n \"4:3\",\n \"16:9\",\n \"9:16\",\n \"2:3\",\n \"3:2\",\n \"21:9\"\n ]\n ]\n },\n {\n \"$eq\": [\n {\n \"$len\": {\n \"$split\": [\n {\n \"$trim\": {\n \"$ref\": \"request.aspectRatio\"\n }\n },\n \"x\"\n ]\n }\n },\n 2\n ]\n }\n ]\n },\n \"message\": \"Agnes 图像只支持 1:1、3:4、4:3、16:9、9:16、2:3、3:2、21:9 比例或 WxH 像素尺寸\"\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 \"size\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$len\": {\n \"$split\": [\n {\n \"$trim\": {\n \"$ref\": \"request.aspectRatio\"\n }\n },\n \"x\"\n ]\n }\n },\n 2\n ]\n },\n \"then\": {\n \"$trim\": {\n \"$ref\": \"request.aspectRatio\"\n }\n },\n \"else\": {\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 \"low\",\n \"standard\"\n ]\n ]\n },\n \"then\": \"1K\"\n },\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"2k\",\n \"medium\",\n \"hd\",\n \"high\"\n ]\n ]\n },\n \"then\": \"2K\"\n },\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"3k\"\n ]\n ]\n },\n \"then\": \"3K\"\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\": \"4K\"\n }\n ],\n \"default\": \"1K\"\n }\n }\n }\n }\n },\n \"ratio\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$in\": [\n {\n \"$trim\": {\n \"$ref\": \"request.aspectRatio\"\n }\n },\n [\n \"1:1\",\n \"3:4\",\n \"4:3\",\n \"16:9\",\n \"9:16\",\n \"2:3\",\n \"3:2\",\n \"21:9\"\n ]\n ]\n },\n \"then\": {\n \"$trim\": {\n \"$ref\": \"request.aspectRatio\"\n }\n },\n \"else\": null\n }\n }\n },\n \"extra_body\": {\n \"$omitEmpty\": {\n \"image\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$coalesce\": [\n {\n \"$ref\": \"media.dataUrl\"\n },\n {\n \"$ref\": \"media.url\"\n }\n ]\n }\n }\n }\n },\n \"response_format\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.agnes-image.response_format\"\n },\n \"url\"\n ]\n }\n }\n }\n },\n \"originPath\": true\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 \"errorPaths\": [\n \"error.code\",\n \"code\"\n ],\n \"messagePaths\": [\n \"error.message\",\n \"message\",\n \"msg\"\n ]\n }\n }\n ]\n }\n}\n```\n\n", + "permissions": [ + "generation.run", + "media.read" + ], + "configuration": { + "fields": [ + { + "name": "apiKey", + "type": "secret", + "label": "API Key", + "required": true + } + ] + }, + "contributes": { + "providers": [ + { + "id": "agnes-image", + "label": "Agnes Image", + "capabilities": [ + "image" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://api.agnes-ai.cn", + "requiresPublicMediaUrls": false, + "auth": { + "type": "bearer", + "field": "apiKey" + }, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "图片模型 ID。" + }, + { + "name": "prompt", + "type": "string", + "required": true, + "mapping": "prompt", + "description": "图片提示词。" + }, + { + "name": "images", + "type": "media[]", + "required": false, + "mapping": "provider image/reference fields", + "description": "参考图或编辑源图,role 由业务层确定。" + }, + { + "name": "imageCount", + "type": "integer", + "required": false, + "mapping": "n/sample_count", + "description": "输出数量。" + }, + { + "name": "aspectRatio", + "type": "string", + "required": false, + "mapping": "size/aspect_ratio", + "description": "比例或尺寸,语义按协议说明。" + }, + { + "name": "resolution", + "type": "string", + "required": false, + "mapping": "resolution/imageSize", + "description": "分辨率档位。" + }, + { + "name": "quality", + "type": "string", + "required": false, + "mapping": "quality", + "description": "质量档位。" + }, + { + "name": "providerOptions", + "type": "object", + "required": false, + "mapping": "provider-specific fields", + "description": "插件命名空间内的厂商扩展字段。" + } + ], + "validations": [ + { + "assert": { + "$or": [ + { + "$in": [ + { + "$trim": { + "$ref": "request.aspectRatio" + } + }, + [ + "", + "auto", + "1:1", + "3:4", + "4:3", + "16:9", + "9:16", + "2:3", + "3:2", + "21:9" + ] + ] + }, + { + "$eq": [ + { + "$len": { + "$split": [ + { + "$trim": { + "$ref": "request.aspectRatio" + } + }, + "x" + ] + } + }, + 2 + ] + } + ] + }, + "message": "Agnes 图像只支持 1:1、3:4、4:3、16:9、9:16、2:3、3:2、21:9 比例或 WxH 像素尺寸" + } + ], + "create": { + "method": "POST", + "path": "/v1/images/generations", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "prompt": { + "$ref": "request.prompt" + }, + "size": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$len": { + "$split": [ + { + "$trim": { + "$ref": "request.aspectRatio" + } + }, + "x" + ] + } + }, + 2 + ] + }, + "then": { + "$trim": { + "$ref": "request.aspectRatio" + } + }, + "else": { + "$switch": { + "cases": [ + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$ref": "request.quality" + } + } + }, + [ + "1k", + "low", + "standard" + ] + ] + }, + "then": "1K" + }, + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$ref": "request.quality" + } + } + }, + [ + "2k", + "medium", + "hd", + "high" + ] + ] + }, + "then": "2K" + }, + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$ref": "request.quality" + } + } + }, + [ + "3k" + ] + ] + }, + "then": "3K" + }, + { + "when": { + "$in": [ + { + "$lower": { + "$trim": { + "$ref": "request.quality" + } + } + }, + [ + "4k" + ] + ] + }, + "then": "4K" + } + ], + "default": "1K" + } + } + } + } + }, + "ratio": { + "$omitEmpty": { + "$if": { + "condition": { + "$in": [ + { + "$trim": { + "$ref": "request.aspectRatio" + } + }, + [ + "1:1", + "3:4", + "4:3", + "16:9", + "9:16", + "2:3", + "3:2", + "21:9" + ] + ] + }, + "then": { + "$trim": { + "$ref": "request.aspectRatio" + } + }, + "else": null + } + } + }, + "extra_body": { + "$omitEmpty": { + "image": { + "$omitEmpty": { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "$coalesce": [ + { + "$ref": "media.dataUrl" + }, + { + "$ref": "media.url" + } + ] + } + } + } + }, + "response_format": { + "$coalesce": [ + { + "$ref": "request.providerOptions.agnes-image.response_format" + }, + "url" + ] + } + } + } + }, + "originPath": true + }, + "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 + } + } + } + } + }, + "errorPaths": [ + "error.code", + "code" + ], + "messagePaths": [ + "error.message", + "message", + "msg" + ] + } + } + ] + } +} diff --git a/plugin-packages/generate-catalog.mjs b/plugin-packages/generate-catalog.mjs index af16aae4e..3c02b3c4d 100644 --- a/plugin-packages/generate-catalog.mjs +++ b/plugin-packages/generate-catalog.mjs @@ -686,6 +686,57 @@ add({ }) }); +// Agnes 图像是同步端点,文生图与图生图共用 POST /v1/images/generations: +// 不传 image 为文生图,传 image 为图生图或多图合成。官方参数表只承认 +// model、prompt、size(必填,档位 1K/2K/3K/4K 或 WxH 精确尺寸)、ratio、image、 +// return_base64 和 extra_body,因此这里不发送 n、output_format、quality、 +// background 等 OpenAI 兼容字段。顶层 response_format 是官方明确列出的错误写法, +// 输出格式只能声明在 extra_body 内。参考图放 extra_body.image,支持公共 HTTPS URL +// 和 Data URI Base64,所以不强制公共媒体 URL,本地开发也能直接联调。 +const agnesImageRatios = ["1:1", "3:4", "4:3", "16:9", "9:16", "2:3", "3:2", "21:9"]; +const agnesImageAspect = trim(ref("request.aspectRatio")); +const agnesImageIsRatio = { $in: [agnesImageAspect, agnesImageRatios] }; +const agnesImageIsPixel = eq(len(split(agnesImageAspect, "x")), 2); +// size 是官方必填项:像素尺寸原样透传,其余情况(比例、auto、空值)一律落到分辨率档位,不能省略。 +const agnesImageSize = conditional(agnesImageIsPixel, agnesImageAspect, { + $switch: { + cases: [ + { when: { $in: [lower(trim(ref("request.quality"))), ["1k", "low", "standard"]] }, then: "1K" }, + { when: { $in: [lower(trim(ref("request.quality"))), ["2k", "medium", "hd", "high"]] }, then: "2K" }, + { when: { $in: [lower(trim(ref("request.quality"))), ["3k"]] }, then: "3K" }, + { when: { $in: [lower(trim(ref("request.quality"))), ["4k"]] }, then: "4K" } + ], + default: "1K" + } +}); + +add({ + id: "agnes-image", providerId: "agnes-image", name: "Agnes Image", vendor: "Agnes AI", capability: "image", + baseUrl: "https://api.agnes-ai.cn", auth: bearer, params: imageParams, requiresPublicMediaUrls: false, + notes: "Agnes 官方图像端点,同步返回。文生图与图生图共用 /v1/images/generations:不传 image 为文生图,传 image 为图生图或多图合成。size 必填,取 1K/2K/3K/4K 档位或 WxH 精确尺寸;画面比例走独立的 ratio 字段。参考图放在 extra_body.image,支持公共 HTTPS URL 或 Data URI Base64。顶层 response_format 是官方明确列出的错误写法,输出格式只能声明在 extra_body.response_format。该端点不接受 n,单次请求固定返回一张图片,需要多张时由上层拆分为多个任务。", + validations: [ + { assert: { $or: [{ $in: [agnesImageAspect, ["", "auto", ...agnesImageRatios]] }, eq(len(split(agnesImageAspect, "x")), 2)] }, message: "Agnes 图像只支持 1:1、3:4、4:3、16:9、9:16、2:3、3:2、21:9 比例或 WxH 像素尺寸" } + ], + create: jsonCreate("/v1/images/generations", { + model: ref("request.model"), + prompt: ref("request.prompt"), + size: omit(agnesImageSize), + ratio: omit(conditional(agnesImageIsRatio, agnesImageAspect)), + extra_body: omit({ + image: omit(map(sorted(ref("request.images")), "media", coalesce(ref("media.dataUrl"), ref("media.url")))), + response_format: coalesce(ref("request.providerOptions.agnes-image.response_format"), "url") + }) + }, { originPath: true }), + response: { + status: "succeeded", + images: map(ref("response.data"), "item", { + url: omit(ref("item.url")), + dataUrl: conditional(ref("item.b64_json"), { $concat: ["data:image/png;base64,", ref("item.b64_json")] }) + }), + errorPaths: ["error.code", "code"], messagePaths: ["error.message", "message", "msg"] + } +}); + add({ id: "agnes-video-v20", providerId: "agnes-video-v20", name: "Agnes Video V2.0", vendor: "Agnes AI", capability: "video", baseUrl: "https://apihub.agnes-ai.com/v1", auth: bearer, params: videoParams, requiresPublicMediaUrls: true, diff --git a/web/src/lib/model-capabilities.ts b/web/src/lib/model-capabilities.ts index 3fa86df56..5cf64edef 100644 --- a/web/src/lib/model-capabilities.ts +++ b/web/src/lib/model-capabilities.ts @@ -245,6 +245,26 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = " image.outputFormat = { supported: false }; image.maxOutputs = 4; } + if (protocol === "agnes-image") { + // Agnes 图像:size 必填,取 1K/2K/3K/4K 档位或 WxH 精确尺寸,画面比例走独立的 ratio 字段; + // 参考图放 extra_body.image,支持多图合成,但没有蒙版端点。 + image.references.maxImages = 9; + image.references.maskSupported = false; + image.size = { + parameter: "aspect_ratio", + values: ["1:1", "3:4", "4:3", "16:9", "9:16", "2:3", "3:2", "21:9"], + default: "1:1", + allowCustom: false, + }; + // 官方档位是 1K/2K/3K/4K,统一层只提供 1k/2k/4k 三档;3K 保留在协议映射里但不在界面露出。 + image.quality = { supported: true, values: ["1k", "2k", "4k"], default: "2k" }; + image.transparentBackground = { supported: false, default: false }; + // 顶层 response_format 是官方明确的错误写法,输出格式只能在 extra_body 内声明。 + image.responseFormat = { supported: false }; + image.outputFormat = { supported: false }; + // 该端点不接受 n,单次请求固定返回一张图片。 + image.maxOutputs = 1; + } if (protocol !== "grok-image" && model.trim().toLowerCase().startsWith("grok-imagine-image")) { image.references.maxImages = 0; image.references.maskSupported = false; diff --git a/web/test/image-resolution-tiers.test.ts b/web/test/image-resolution-tiers.test.ts index 30f8a23b8..5f1e1a3da 100644 --- a/web/test/image-resolution-tiers.test.ts +++ b/web/test/image-resolution-tiers.test.ts @@ -8,7 +8,7 @@ import { imageSizeForResolution, supportsImageResolutionPresets, } from "../src/lib/image-resolution-tiers"; -import { defaultImageCapabilityConfig } from "../src/lib/model-capabilities"; +import { defaultImageCapabilityConfig, normalizeImageValue } from "../src/lib/model-capabilities"; import { resolveImageRequestSize } from "../src/services/api/image-validation"; const sizes = [ @@ -65,6 +65,23 @@ describe("image resolution tiers", () => { expect(buildImageResolutionOptions(profile.size.values)).toHaveLength(30); }); + // Agnes 图像的 size 是官方必填项,但取分辨率档位,画面比例走独立的 ratio 字段, + // 所以界面必须同时保留比例和档位两个选择器,不能退化成 OpenAI 的像素尺寸。 + test("Agnes 图像按比例加档位选择尺寸,并关闭上游不支持的输出开关", () => { + const profile = defaultImageCapabilityConfig("agnes-image", "agnes-image-2.1-flash"); + + expect(profile.size.parameter).toBe("aspect_ratio"); + expect(profile.size.values).toEqual(["1:1", "3:4", "4:3", "16:9", "9:16", "2:3", "3:2", "21:9"]); + expect(profile.quality.values).toEqual(["1k", "2k", "4k"]); + expect(normalizeImageValue(profile, { size: "3:4", quality: "4k", count: "4" })).toMatchObject({ size: "3:4", quality: "4k", count: "1" }); + + // 顶层 response_format 是官方明确列出的错误写法,output_format 会被上游直接拒绝。 + expect(profile.responseFormat.supported).toBe(false); + expect(profile.outputFormat.supported).toBe(false); + expect(profile.transparentBackground.supported).toBe(false); + expect(profile.references.maskSupported).toBe(false); + }); + test("识别适合全景图的 2:1 自定义尺寸", () => { const options = buildImageResolutionOptions(["1440x720", "2048x1024", "3840x1920"]);