From a35868ddef6f5468072d96d90afa4a1c17548cf6 Mon Sep 17 00:00:00 2001 From: zhuchi Date: Wed, 16 Sep 2026 20:03:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(provider):=20=E6=A8=A1=E5=9E=8B=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=20-=20=E6=96=B0=E5=A2=9E=20Agnes=20=E5=9B=BE=E5=83=8F?= =?UTF-8?q?=E5=A3=B0=E6=98=8E=E5=BC=8F=E5=8D=8F=E8=AE=AE=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/protocol/agnes_image_test.go | 162 ++++++ docs/content/docs/progress/pending-test.mdx | 11 + plugin-packages/agnes-image.yingce-plugin | Bin 0 -> 9676 bytes plugin-packages/agnes-image/README.md | 5 + plugin-packages/agnes-image/docs/interface.md | 462 ++++++++++++++++++ plugin-packages/agnes-image/manifest.json | 382 +++++++++++++++ plugin-packages/generate-catalog.mjs | 51 ++ 7 files changed, 1073 insertions(+) create mode 100644 backend/internal/protocol/agnes_image_test.go create mode 100644 plugin-packages/agnes-image.yingce-plugin create mode 100644 plugin-packages/agnes-image/README.md create mode 100644 plugin-packages/agnes-image/docs/interface.md create mode 100644 plugin-packages/agnes-image/manifest.json 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 838c91832..9fc1d60c6 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -64,6 +64,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 0000000000000000000000000000000000000000..c4b28a97c60be2064665be0a44bf20d02fee330d GIT binary patch literal 9676 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 S+kbCE{ +## 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,