feat(provider): 模型协议 - 新增 Agnes 图像声明式协议插件
This commit is contained in:
162
backend/internal/protocol/agnes_image_test.go
Normal file
162
backend/internal/protocol/agnes_image_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;收费生成还需实际媒体任务适配器。写入、审批与预算仍以服务端强校验为准。
|
||||
|
||||
BIN
plugin-packages/agnes-image.yingce-plugin
Normal file
BIN
plugin-packages/agnes-image.yingce-plugin
Normal file
Binary file not shown.
5
plugin-packages/agnes-image/README.md
Normal file
5
plugin-packages/agnes-image/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Agnes Image
|
||||
|
||||
该目录是独立官方协议插件源码。后端从生成的 `agnes-image.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。
|
||||
|
||||
完整接口见 [docs/interface.md](docs/interface.md)。
|
||||
462
plugin-packages/agnes-image/docs/interface.md
Normal file
462
plugin-packages/agnes-image/docs/interface.md
Normal file
@@ -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,单次请求固定返回一张图片,需要多张时由上层拆分为多个任务。
|
||||
|
||||
<!-- YINGCE_MANIFEST_CONTRACT_START -->
|
||||
## 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
<!-- YINGCE_MANIFEST_CONTRACT_END -->
|
||||
382
plugin-packages/agnes-image/manifest.json
Normal file
382
plugin-packages/agnes-image/manifest.json
Normal file
File diff suppressed because one or more lines are too long
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user