diff --git a/backend/internal/protocol/qianwenai_maas_image_test.go b/backend/internal/protocol/qianwenai_maas_image_test.go new file mode 100644 index 000000000..c46050c2d --- /dev/null +++ b/backend/internal/protocol/qianwenai_maas_image_test.go @@ -0,0 +1,174 @@ +package protocol + +import ( + "context" + "testing" +) + +// 千问 AI 平台(QianWenAI MAAS)的网关域名是 maas.qianwenaiapi.com,与阿里云百炼 +// 不是同一个入口,但多模态图像报文形状相同:参考图放在 input.messages[].content[].image, +// 任务结果在 output.choices[0].message.content[].image。 +func TestQianwenaiMaasImageUsesMaasMultimodalEndpoint(t *testing.T) { + adapter := officialPackageAdapter(t, "qianwenai-maas-image.yingce-plugin", "qianwenai-maas-image") + create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{ + Model: "qwen-image-3.0-pro", Prompt: "灰色圆点", AspectRatio: "3:4", ImageCount: 1, + }}) + if err != nil { + t.Fatal(err) + } + // 平台自己的多模态端点,与百炼的 image-generation 路径不同,不能互相替换。 + if create.Method != "POST" || create.Path != "/api/v1/services/aigc/multimodal-generation/generation" { + t.Fatalf("create = %#v", create) + } + if create.Headers["X-DashScope-Async"] != "enable" { + t.Fatalf("create headers = %#v", create.Headers) + } + // originPath 让请求路径替换 Base URL 的路径段,避免渠道沿用兼容模式路径时打错地址。 + if !create.OriginPath { + t.Fatal("create 必须声明 originPath") + } + + body := manifestTestBody(t, create) + if body["model"] != "qwen-image-3.0-pro" { + t.Fatalf("model = %#v", body["model"]) + } + content := qianwenaiMaasImageContent(t, body) + if len(content) != 1 { + t.Fatalf("文生图 content = %#v, 期望只有 text 一项", content) + } + entry, _ := content[0].(map[string]any) + if entry["text"] != "灰色圆点" { + t.Fatalf("text 项 = %#v", entry) + } + + parameters, _ := body["parameters"].(map[string]any) + if parameters["size"] != "960*1280" { + t.Fatalf("size = %#v, 期望 960*1280(DashScope 用星号而非字母 x)", parameters["size"]) + } + if parameters["enable_thinking"] != false { + t.Fatalf("enable_thinking = %#v, 官方要求非流式调用必须为 false", parameters["enable_thinking"]) + } + if parameters["n"] != float64(1) { + t.Fatalf("n = %#v, 期望 1", parameters["n"]) + } +} + +func TestQianwenaiMaasImagePutsReferencesInMessageContent(t *testing.T) { + adapter := officialPackageAdapter(t, "qianwenai-maas-image.yingce-plugin", "qianwenai-maas-image") + create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{ + Model: "qwen-image-3.0-pro", Prompt: "换装", AspectRatio: "1:1", ImageCount: 2, + Images: []MediaReference{ + {DataURL: "data:image/png;base64,QUJD", Role: "reference_image", Order: 2}, + {URL: "https://cdn.example/anchor.png", Role: "edit_source", Order: 1}, + }, + }}) + if err != nil { + t.Fatal(err) + } + + content := qianwenaiMaasImageContent(t, manifestTestBody(t, create)) + if len(content) != 3 { + t.Fatalf("图生图 content = %#v, 期望 2 张图 + 1 条 text", content) + } + // order 决定顺序:先 order=1 的参考图,再 order=2;text 永远排在最后。 + first, _ := content[0].(map[string]any) + if first["image"] != "https://cdn.example/anchor.png" { + t.Fatalf("第一项 = %#v", first) + } + second, _ := content[1].(map[string]any) + if second["image"] != "data:image/png;base64,QUJD" { + t.Fatalf("第二项 = %#v, 有 dataUrl 时必须优先 Base64(大陆地域无法回源站外图片)", second) + } + third, _ := content[2].(map[string]any) + if third["text"] != "换装" { + t.Fatalf("末项 = %#v", third) + } + + parameters, _ := manifestTestBody(t, create)["parameters"].(map[string]any) + if parameters["n"] != float64(2) { + t.Fatalf("n = %#v, 期望 2", parameters["n"]) + } + if parameters["size"] != "1024*1024" { + t.Fatalf("size = %#v", parameters["size"]) + } +} + +func TestQianwenaiMaasImageOmitsUnregisteredSize(t *testing.T) { + adapter := officialPackageAdapter(t, "qianwenai-maas-image.yingce-plugin", "qianwenai-maas-image") + create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{ + Model: "qwen-image-3.0-pro", Prompt: "circle", AspectRatio: "2880x2880", + }}) + if err != nil { + t.Fatal(err) + } + parameters, _ := manifestTestBody(t, create)["parameters"].(map[string]any) + if _, present := parameters["size"]; present { + t.Fatalf("parameters = %#v, 未登记的档位必须省略 size 交由模型自动推荐", parameters) + } +} + +func TestQianwenaiMaasImageLifecycleReadsMultimodalChoices(t *testing.T) { + adapter := officialPackageAdapter(t, "qianwenai-maas-image.yingce-plugin", "qianwenai-maas-image") + created, err := adapter.ParseCreate(context.Background(), []byte(`{"output":{"task_id":"task-qw-1","task_status":"PENDING"},"request_id":"req-1"}`)) + if err != nil { + t.Fatal(err) + } + if created.TaskID != "task-qw-1" { + t.Fatalf("taskId = %q", created.TaskID) + } + + poll, err := adapter.BuildPoll(context.Background(), PollContext{TaskID: created.TaskID}) + if err != nil { + t.Fatal(err) + } + if poll.Method != "GET" || poll.Path != "/api/v1/tasks/task-qw-1" { + t.Fatalf("poll = %#v", poll) + } + if !poll.OriginPath { + t.Fatal("poll 必须声明 originPath") + } + + // 图片藏在 output.choices[0].message.content[].image,这是多模态端点与 wanx 的差异所在。 + completed, err := adapter.ParsePoll(context.Background(), PollContext{TaskID: created.TaskID}, []byte(`{"output":{"task_id":"task-qw-1","task_status":"SUCCEEDED","choices":[{"finish_reason":"stop","message":{"role":"assistant","content":[{"image":"https://maas-result.example/a.png","type":"image"}]}}]},"usage":{"output_image_count":1},"request_id":"req-2"}`)) + if err != nil { + t.Fatal(err) + } + if completed.Status != StatusSucceeded { + t.Fatalf("status = %q, want %q", completed.Status, StatusSucceeded) + } + if completed.Result == nil || len(completed.Result.Images) != 1 { + t.Fatalf("result = %#v", completed.Result) + } + if completed.Result.Images[0].URL != "https://maas-result.example/a.png" { + t.Fatalf("images = %#v", completed.Result.Images) + } +} + +func TestQianwenaiMaasImageSurfacesFailureMessage(t *testing.T) { + adapter := officialPackageAdapter(t, "qianwenai-maas-image.yingce-plugin", "qianwenai-maas-image") + failed, err := adapter.ParsePoll(context.Background(), PollContext{TaskID: "task-qw-1"}, []byte(`{"output":{"task_id":"task-qw-1","task_status":"FAILED","code":"InvalidParameter","message":"url error, please check url!"},"request_id":"req-3"}`)) + if err != nil { + t.Fatal(err) + } + if failed.Status != StatusFailed { + t.Fatalf("status = %q, want %q", failed.Status, StatusFailed) + } + if failed.Message == "" { + t.Fatal("上游失败必须保留错误信息,不能被包装成成功") + } +} + +func qianwenaiMaasImageContent(t *testing.T, body map[string]any) []any { + t.Helper() + input, _ := body["input"].(map[string]any) + messages, _ := input["messages"].([]any) + if len(messages) != 1 { + t.Fatalf("messages = %#v", messages) + } + message, _ := messages[0].(map[string]any) + if message["role"] != "user" { + t.Fatalf("role = %#v", message["role"]) + } + content, _ := message["content"].([]any) + return content +} diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 39bf51e1f..4162bceff 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -14,6 +14,15 @@ description: 当前项目已实现功能的手工验证清单与未确认边界 - [ ] 用户验收:真实登录态访问 `/toolbox/image` 和 `/toolbox/video`,确认快捷目标卡使用真实工具、图片/视频切换、工具弹层、素材上传和生成提交均正常。 - [ ] 用户验收:明暗主题、窄屏滚动、不同工具数量、工具封面缺失、生成中/成功/失败结果状态下,确认布局和色彩层级符合预期。 +## 千问 AI 平台(QianWenAI MAAS)图像协议插件 + +- 已实现:新增独立协议插件 `qianwenai-maas-image`,Base URL `https://maas.qianwenaiapi.com`,创建端点 `POST /api/v1/services/aigc/multimodal-generation/generation`,查询 `GET /api/v1/tasks/{{taskId}}`。与百炼的 `dashscope-qwen-image` 报文形状一致,但网关域名不同,不能互相替代。 +- 已验证:manifest 关键字段与百炼多模态模板逐字段对照一致(仅 id/name/baseUrl/endpoint/providerOptions 命名空间按预期不同);JSON 合法;插件包 `qianwenai-maas-image.yingce-plugin` 已生成。Go 测试已补齐,真实 API 请求未发出。 +- 实现位置:`plugin-packages/qianwenai-maas-image/`、`plugin-packages/generate-catalog.mjs`、`backend/internal/protocol/qianwenai_maas_image_test.go`。 +- [ ] 用户验收:用千问 AI 平台真实 API Key 配置渠道,文生图与图生图各跑一次,确认返回图片能正常落盘;确认渠道 Base URL 填 `https://maas.qianwenaiapi.com`。 +- [ ] 未验证:真实请求未发出,模型 ID(`qwen-image-3.0-pro` 等)是否在平台侧全部开通需按实际账号确认。 + + ## 灵感库中文提示词 - 已实现:内置目录与首页画廊的提示词统一为中文;复用来源中文正文,补齐外文翻译;导入时校验原文摘要,缺译或旧译文阻止覆盖。 diff --git a/plugin-packages/generate-catalog.mjs b/plugin-packages/generate-catalog.mjs index 362c3a247..6e1393f80 100644 --- a/plugin-packages/generate-catalog.mjs +++ b/plugin-packages/generate-catalog.mjs @@ -1117,6 +1117,55 @@ add({ }) }); +// 千问 AI 平台(QianWenAI MAAS)是阿里独立的 MaaS 站点,网关域名是 +// maas.qianwenaiapi.com,与百炼的 dashscope.aliyuncs.com 不是同一入口,因此单列一个插件。 +// 它同样走多模态图像端点 multimodal-generation/generation,报文形状与百炼一致 +// (input.messages[].content[].image + text),但不能复用百炼插件:改 Base URL 后仍指向 +// dashscope.aliyuncs.com,请求会打到没有开通该模型的站点。 +const qianwenaiMaasImageSizes = [ + ["1:1", "1024*1024"], ["3:4", "960*1280"], ["4:3", "1280*960"], ["2:3", "1024*1536"], ["3:2", "1536*1024"], + ["9:16", "864*1536"], ["16:9", "1536*864"], + ["1024x1024", "1024*1024"], ["1024x1536", "1024*1536"], ["1536x1024", "1536*1024"], + ["1024x1280", "1024*1280"], ["1280x1024", "1280*1024"], ["960x1280", "960*1280"], ["1280x960", "1280*960"] +]; + +add({ + id: "qianwenai-maas-image", providerId: "qianwenai-maas-image", name: "QianWenAI MAAS", vendor: "Alibaba QianWenAI", capability: "image", + baseUrl: "https://maas.qianwenaiapi.com", auth: bearer, params: imageParams, requiresPublicMediaUrls: false, + notes: "千问 AI 平台(QianWenAI MAAS)多模态图像端点(multimodal-generation 异步任务)。文生图与图生图共用同一入口:不传 image 为文生图,传参考图为图生图。参考图通过 input.messages[].content[].image 传输(优先 Base64 data URL)。size 采用“宽*高”,未登记的档位省略并由模型自动推荐;enable_thinking 固定为 false,因为官方要求非流式调用关闭思考模式。", + create: jsonCreate("/api/v1/services/aigc/multimodal-generation/generation", { + model: ref("request.model"), + input: { + messages: [{ + role: "user", + content: { + $concatArrays: [ + map(sorted(ref("request.images")), "media", { image: coalesce(ref("media.dataUrl"), ref("media.url")) }), + [{ text: ref("request.prompt") }] + ] + } + }] + }, + parameters: { + size: omit({ $switch: { cases: qianwenaiMaasImageSizes.map(([ratio, size]) => ({ when: eq(ref("request.aspectRatio"), ratio), then: size })), default: null } }), + n: conditional(gt({ $toInt: ref("request.imageCount") }, 0), { $toInt: ref("request.imageCount") }, 1), + prompt_extend: true, + enable_thinking: false, + negative_prompt: omit(ref("request.providerOptions.qianwenai-maas-image.negative_prompt")), + seed: omit(ref("request.providerOptions.qianwenai-maas-image.seed")), + watermark: omit(ref("request.watermark")) + } + }, { headers: { "X-DashScope-Async": "enable" }, originPath: true }), + poll: { method: "GET", path: "/api/v1/tasks/{{taskId}}", originPath: true }, + response: asyncResponse("image", { + taskId: coalesce(ref("response.output.task_id"), ref("response.task_id"), ref("taskId")), + status: coalesce(ref("response.output.task_status"), ref("response.status"), "pending"), + message: coalesce(ref("response.output.message"), ref("response.message")), + images: map(ref("response.output.choices.0.message.content"), "item", { url: omit(ref("item.image")) }), + errorPaths: ["code", "output.code"], messagePaths: ["message", "output.message"] + }) +}); + for (const [id, name, vendor, capability, baseUrl, createPath, pollPath, requestShape] of [ ["dashscope-qwen-native", "DashScope Qwen Native", "Alibaba Cloud", "text", "https://dashscope.aliyuncs.com", "/api/v1/services/aigc/text-generation/generation", null, "input+parameters"], ["minimax-text-native", "MiniMax Text Native", "MiniMax", "text", "https://api.minimax.chat", "/v1/text/chatcompletion_v2", null, "messages+tokens_to_generate"], diff --git a/plugin-packages/qianwenai-maas-image.yingce-plugin b/plugin-packages/qianwenai-maas-image.yingce-plugin new file mode 100644 index 000000000..6342dc5b7 Binary files /dev/null and b/plugin-packages/qianwenai-maas-image.yingce-plugin differ diff --git a/plugin-packages/qianwenai-maas-image/README.md b/plugin-packages/qianwenai-maas-image/README.md new file mode 100644 index 000000000..c32452d9b --- /dev/null +++ b/plugin-packages/qianwenai-maas-image/README.md @@ -0,0 +1,5 @@ +# QianWenAI MAAS + +该目录是独立官方协议插件源码。后端从生成的 `qianwenai-maas-image.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。 + +完整接口见 [docs/interface.md](docs/interface.md)。 diff --git a/plugin-packages/qianwenai-maas-image/docs/interface.md b/plugin-packages/qianwenai-maas-image/docs/interface.md new file mode 100644 index 000000000..0e02d82ff --- /dev/null +++ b/plugin-packages/qianwenai-maas-image/docs/interface.md @@ -0,0 +1,518 @@ +# QianWenAI MAAS 接口字段 + +## 协议身份 + +- 插件 ID:`qianwenai-maas-image`。 +- Provider ID:`qianwenai-maas-image`。 +- 能力:`image`。 +- 默认 Base URL:`https://maas.qianwenaiapi.com`。 +- 鉴权驱动:`bearer`。 +- 创建:`POST /api/v1/services/aigc/multimodal-generation/generation`。 +- 查询:`GET /api/v1/tasks/{{taskId}}`。 + +## 配置字段 + +| 字段 | 类型 | 必填 | 含义 | +| --- | --- | --- | --- | +| `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` | `"/api/v1/services/aigc/multimodal-generation/generation"` | +| `create.contentType` | `"application/json"` | +| `create.body.model` | `{"$ref":"request.model"}` | +| `create.body.input.messages[0].role` | `"user"` | +| `create.body.input.messages[0].content` | `{"$concatArrays":[{"$map":{"from":{"$sortByOrder":{"$ref":"request.images"}},"as":"media","in":{"image":{"$coalesce":[{"$ref":"media.dataUrl"},{"$ref":"media.url"}]}}}},[{"text":{"$ref":"request.prompt"}}]]}` | +| `create.body.parameters.size` | `{"$omitEmpty":{"$switch":{"cases":[{"when":{"$eq":[{"$ref":"request.aspectRatio"},"1:1"]},"then":"1024*1024"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"3:4"]},"then":"960*1280"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"4:3"]},"then":"1280*960"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"2:3"]},"then":"1024*1536"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"3:2"]},"then":"1536*1024"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"9:16"]},"then":"864*1536"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"16:9"]},"then":"1536*864"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"1024x1024"]},"then":"1024*1024"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"1024x1536"]},"then":"1024*1536"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"1536x1024"]},"then":"1536*1024"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"1024x1280"]},"then":"1024*1280"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"1280x1024"]},"then":"1280*1024"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"960x1280"]},"then":"960*1280"},{"when":{"$eq":[{"$ref":"request.aspectRatio"},"1280x960"]},"then":"1280*960"}],"default":null}}}` | +| `create.body.parameters.n` | `{"$if":{"condition":{"$gt":[{"$toInt":{"$ref":"request.imageCount"}},0]},"then":{"$toInt":{"$ref":"request.imageCount"}},"else":1}}` | +| `create.body.parameters.prompt_extend` | `true` | +| `create.body.parameters.enable_thinking` | `false` | +| `create.body.parameters.negative_prompt` | `{"$omitEmpty":{"$ref":"request.providerOptions.qianwenai-maas-image.negative_prompt"}}` | +| `create.body.parameters.seed` | `{"$omitEmpty":{"$ref":"request.providerOptions.qianwenai-maas-image.seed"}}` | +| `create.body.parameters.watermark` | `{"$omitEmpty":{"$ref":"request.watermark"}}` | +| `create.headers.X-DashScope-Async` | `"enable"` | +| `poll.method` | `"GET"` | +| `poll.path` | `"/api/v1/tasks/{{taskId}}"` | +| `poll.contentType` | `"application/json"` | + +## Provider 扩展键 + +- `providerOptions.qianwenai-maas-image.negative_prompt` +- `providerOptions.qianwenai-maas-image.seed` + +动态模型或工作流允许使用文档声明的完整 `parameters/input/extra_body` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。 + +## 响应映射逐字段清单 + +| 映射位置 | 上游路径或转换表达式 | +| --- | --- | +| `response.taskId` | `{"$coalesce":[{"$ref":"response.output.task_id"},{"$ref":"response.task_id"},{"$ref":"taskId"}]}` | +| `response.status` | `{"$coalesce":[{"$ref":"response.output.task_status"},{"$ref":"response.status"},"pending"]}` | +| `response.message` | `{"$coalesce":[{"$ref":"response.output.message"},{"$ref":"response.message"}]}` | +| `response.images` | `{"$map":{"from":{"$ref":"response.output.choices.0.message.content"},"as":"item","in":{"url":{"$omitEmpty":{"$ref":"item.image"}}}}}` | +| `response.errorPaths[0]` | `"code"` | +| `response.errorPaths[1]` | `"output.code"` | +| `response.resultEphemeral` | `true` | +| `response.messagePaths[0]` | `"message"` | +| `response.messagePaths[1]` | `"output.message"` | + +## 响应与错误 + +插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。 + +## 兼容边界 + +千问 AI 平台(QianWenAI MAAS)多模态图像端点(multimodal-generation 异步任务)。文生图与图生图共用同一入口:不传 image 为文生图,传参考图为图生图。参考图通过 input.messages[].content[].image 传输(优先 Base64 data URL)。size 采用“宽*高”,未登记的档位省略并由模型自动推荐;enable_thinking 固定为 false,因为官方要求非流式调用关闭思考模式。 + + +## Manifest 完整接口定义 + +以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。 + +```json +{ + "apiVersion": "yingce.plugin/v2", + "id": "qianwenai-maas-image", + "name": "QianWenAI MAAS", + "version": "2.0.0", + "author": "Alibaba QianWenAI / 影策", + "description": "QianWenAI MAAS 独立请求协议插件。", + "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": "qianwenai-maas-image", + "label": "QianWenAI MAAS", + "capabilities": [ + "image" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://maas.qianwenaiapi.com", + "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": "插件命名空间内的厂商扩展字段。" + } + ], + "create": { + "method": "POST", + "path": "/api/v1/services/aigc/multimodal-generation/generation", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "input": { + "messages": [ + { + "role": "user", + "content": { + "$concatArrays": [ + { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "image": { + "$coalesce": [ + { + "$ref": "media.dataUrl" + }, + { + "$ref": "media.url" + } + ] + } + } + } + }, + [ + { + "text": { + "$ref": "request.prompt" + } + } + ] + ] + } + } + ] + }, + "parameters": { + "size": { + "$omitEmpty": { + "$switch": { + "cases": [ + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "1:1" + ] + }, + "then": "1024*1024" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "3:4" + ] + }, + "then": "960*1280" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "4:3" + ] + }, + "then": "1280*960" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "2:3" + ] + }, + "then": "1024*1536" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "3:2" + ] + }, + "then": "1536*1024" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "9:16" + ] + }, + "then": "864*1536" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "then": "1536*864" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "1024x1024" + ] + }, + "then": "1024*1024" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "1024x1536" + ] + }, + "then": "1024*1536" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "1536x1024" + ] + }, + "then": "1536*1024" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "1024x1280" + ] + }, + "then": "1024*1280" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "1280x1024" + ] + }, + "then": "1280*1024" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "960x1280" + ] + }, + "then": "960*1280" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "1280x960" + ] + }, + "then": "1280*960" + } + ], + "default": null + } + } + }, + "n": { + "$if": { + "condition": { + "$gt": [ + { + "$toInt": { + "$ref": "request.imageCount" + } + }, + 0 + ] + }, + "then": { + "$toInt": { + "$ref": "request.imageCount" + } + }, + "else": 1 + } + }, + "prompt_extend": true, + "enable_thinking": false, + "negative_prompt": { + "$omitEmpty": { + "$ref": "request.providerOptions.qianwenai-maas-image.negative_prompt" + } + }, + "seed": { + "$omitEmpty": { + "$ref": "request.providerOptions.qianwenai-maas-image.seed" + } + }, + "watermark": { + "$omitEmpty": { + "$ref": "request.watermark" + } + } + } + }, + "headers": { + "X-DashScope-Async": "enable" + }, + "originPath": true + }, + "poll": { + "method": "GET", + "path": "/api/v1/tasks/{{taskId}}", + "originPath": true + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.output.task_id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "taskId" + } + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.output.task_status" + }, + { + "$ref": "response.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.output.message" + }, + { + "$ref": "response.message" + } + ] + }, + "images": { + "$map": { + "from": { + "$ref": "response.output.choices.0.message.content" + }, + "as": "item", + "in": { + "url": { + "$omitEmpty": { + "$ref": "item.image" + } + } + } + } + }, + "errorPaths": [ + "code", + "output.code" + ], + "resultEphemeral": true, + "messagePaths": [ + "message", + "output.message" + ] + } + } + ] + } +} +``` + diff --git a/plugin-packages/qianwenai-maas-image/manifest.json b/plugin-packages/qianwenai-maas-image/manifest.json new file mode 100644 index 000000000..68fa05586 --- /dev/null +++ b/plugin-packages/qianwenai-maas-image/manifest.json @@ -0,0 +1,426 @@ +{ + "apiVersion": "yingce.plugin/v2", + "id": "qianwenai-maas-image", + "name": "QianWenAI MAAS", + "version": "2.0.0", + "author": "Alibaba QianWenAI / 影策", + "description": "QianWenAI MAAS 独立请求协议插件。", + "documentation": "# QianWenAI MAAS\n\n该目录是独立官方协议插件源码。后端从生成的 `qianwenai-maas-image.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。\n\n完整接口见 [docs/interface.md](docs/interface.md)。\n\n---\n\n# QianWenAI MAAS 接口字段\n\n## 协议身份\n\n- 插件 ID:`qianwenai-maas-image`。\n- Provider ID:`qianwenai-maas-image`。\n- 能力:`image`。\n- 默认 Base URL:`https://maas.qianwenaiapi.com`。\n- 鉴权驱动:`bearer`。\n- 创建:`POST /api/v1/services/aigc/multimodal-generation/generation`。\n- 查询:`GET /api/v1/tasks/{{taskId}}`。\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` | `\"/api/v1/services/aigc/multimodal-generation/generation\"` |\n| `create.contentType` | `\"application/json\"` |\n| `create.body.model` | `{\"$ref\":\"request.model\"}` |\n| `create.body.input.messages[0].role` | `\"user\"` |\n| `create.body.input.messages[0].content` | `{\"$concatArrays\":[{\"$map\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.images\"}},\"as\":\"media\",\"in\":{\"image\":{\"$coalesce\":[{\"$ref\":\"media.dataUrl\"},{\"$ref\":\"media.url\"}]}}}},[{\"text\":{\"$ref\":\"request.prompt\"}}]]}` |\n| `create.body.parameters.size` | `{\"$omitEmpty\":{\"$switch\":{\"cases\":[{\"when\":{\"$eq\":[{\"$ref\":\"request.aspectRatio\"},\"1:1\"]},\"then\":\"1024*1024\"},{\"when\":{\"$eq\":[{\"$ref\":\"request.aspectRatio\"},\"3:4\"]},\"then\":\"960*1280\"},{\"when\":{\"$eq\":[{\"$ref\":\"request.aspectRatio\"},\"4:3\"]},\"then\":\"1280*960\"},{\"when\":{\"$eq\":[{\"$ref\":\"request.aspectRatio\"},\"2:3\"]},\"then\":\"1024*1536\"},{\"when\":{\"$eq\":[{\"$ref\":\"request.aspectRatio\"},\"3:2\"]},\"then\":\"1536*1024\"},{\"when\":{\"$eq\":[{\"$ref\":\"request.aspectRatio\"},\"9:16\"]},\"then\":\"864*1536\"},{\"when\":{\"$eq\":[{\"$ref\":\"request.aspectRatio\"},\"16:9\"]},\"then\":\"1536*864\"},{\"when\":{\"$eq\":[{\"$ref\":\"request.aspectRatio\"},\"1024x1024\"]},\"then\":\"1024*1024\"},{\"when\":{\"$eq\":[{\"$ref\":\"request.aspectRatio\"},\"1024x1536\"]},\"then\":\"1024*1536\"},{\"when\":{\"$eq\":[{\"$ref\":\"request.aspectRatio\"},\"1536x1024\"]},\"then\":\"1536*1024\"},{\"when\":{\"$eq\":[{\"$ref\":\"request.aspectRatio\"},\"1024x1280\"]},\"then\":\"1024*1280\"},{\"when\":{\"$eq\":[{\"$ref\":\"request.aspectRatio\"},\"1280x1024\"]},\"then\":\"1280*1024\"},{\"when\":{\"$eq\":[{\"$ref\":\"request.aspectRatio\"},\"960x1280\"]},\"then\":\"960*1280\"},{\"when\":{\"$eq\":[{\"$ref\":\"request.aspectRatio\"},\"1280x960\"]},\"then\":\"1280*960\"}],\"default\":null}}}` |\n| `create.body.parameters.n` | `{\"$if\":{\"condition\":{\"$gt\":[{\"$toInt\":{\"$ref\":\"request.imageCount\"}},0]},\"then\":{\"$toInt\":{\"$ref\":\"request.imageCount\"}},\"else\":1}}` |\n| `create.body.parameters.prompt_extend` | `true` |\n| `create.body.parameters.enable_thinking` | `false` |\n| `create.body.parameters.negative_prompt` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.qianwenai-maas-image.negative_prompt\"}}` |\n| `create.body.parameters.seed` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.qianwenai-maas-image.seed\"}}` |\n| `create.body.parameters.watermark` | `{\"$omitEmpty\":{\"$ref\":\"request.watermark\"}}` |\n| `create.headers.X-DashScope-Async` | `\"enable\"` |\n| `poll.method` | `\"GET\"` |\n| `poll.path` | `\"/api/v1/tasks/{{taskId}}\"` |\n| `poll.contentType` | `\"application/json\"` |\n\n## Provider 扩展键\n\n- `providerOptions.qianwenai-maas-image.negative_prompt`\n- `providerOptions.qianwenai-maas-image.seed`\n\n动态模型或工作流允许使用文档声明的完整 `parameters/input/extra_body` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。\n\n## 响应映射逐字段清单\n\n| 映射位置 | 上游路径或转换表达式 |\n| --- | --- |\n| `response.taskId` | `{\"$coalesce\":[{\"$ref\":\"response.output.task_id\"},{\"$ref\":\"response.task_id\"},{\"$ref\":\"taskId\"}]}` |\n| `response.status` | `{\"$coalesce\":[{\"$ref\":\"response.output.task_status\"},{\"$ref\":\"response.status\"},\"pending\"]}` |\n| `response.message` | `{\"$coalesce\":[{\"$ref\":\"response.output.message\"},{\"$ref\":\"response.message\"}]}` |\n| `response.images` | `{\"$map\":{\"from\":{\"$ref\":\"response.output.choices.0.message.content\"},\"as\":\"item\",\"in\":{\"url\":{\"$omitEmpty\":{\"$ref\":\"item.image\"}}}}}` |\n| `response.errorPaths[0]` | `\"code\"` |\n| `response.errorPaths[1]` | `\"output.code\"` |\n| `response.resultEphemeral` | `true` |\n| `response.messagePaths[0]` | `\"message\"` |\n| `response.messagePaths[1]` | `\"output.message\"` |\n\n## 响应与错误\n\n插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。\n\n## 兼容边界\n\n千问 AI 平台(QianWenAI MAAS)多模态图像端点(multimodal-generation 异步任务)。文生图与图生图共用同一入口:不传 image 为文生图,传参考图为图生图。参考图通过 input.messages[].content[].image 传输(优先 Base64 data URL)。size 采用“宽*高”,未登记的档位省略并由模型自动推荐;enable_thinking 固定为 false,因为官方要求非流式调用关闭思考模式。\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"qianwenai-maas-image\",\n \"name\": \"QianWenAI MAAS\",\n \"version\": \"2.0.0\",\n \"author\": \"Alibaba QianWenAI / 影策\",\n \"description\": \"QianWenAI MAAS 独立请求协议插件。\",\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\": \"qianwenai-maas-image\",\n \"label\": \"QianWenAI MAAS\",\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://maas.qianwenaiapi.com\",\n \"requiresPublicMediaUrls\": false,\n \"auth\": {\n \"type\": \"bearer\",\n \"field\": \"apiKey\"\n },\n \"parameters\": [\n {\n \"name\": \"model\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"model\",\n \"description\": \"图片模型 ID。\"\n },\n {\n \"name\": \"prompt\",\n \"type\": \"string\",\n \"required\": true,\n \"mapping\": \"prompt\",\n \"description\": \"图片提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"provider image/reference fields\",\n \"description\": \"参考图或编辑源图,role 由业务层确定。\"\n },\n {\n \"name\": \"imageCount\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"n/sample_count\",\n \"description\": \"输出数量。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"size/aspect_ratio\",\n \"description\": \"比例或尺寸,语义按协议说明。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution/imageSize\",\n \"description\": \"分辨率档位。\"\n },\n {\n \"name\": \"quality\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"quality\",\n \"description\": \"质量档位。\"\n },\n {\n \"name\": \"providerOptions\",\n \"type\": \"object\",\n \"required\": false,\n \"mapping\": \"provider-specific fields\",\n \"description\": \"插件命名空间内的厂商扩展字段。\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/api/v1/services/aigc/multimodal-generation/generation\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"input\": {\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": {\n \"$concatArrays\": [\n {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"image\": {\n \"$coalesce\": [\n {\n \"$ref\": \"media.dataUrl\"\n },\n {\n \"$ref\": \"media.url\"\n }\n ]\n }\n }\n }\n },\n [\n {\n \"text\": {\n \"$ref\": \"request.prompt\"\n }\n }\n ]\n ]\n }\n }\n ]\n },\n \"parameters\": {\n \"size\": {\n \"$omitEmpty\": {\n \"$switch\": {\n \"cases\": [\n {\n \"when\": {\n \"$eq\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"1:1\"\n ]\n },\n \"then\": \"1024*1024\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"3:4\"\n ]\n },\n \"then\": \"960*1280\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"4:3\"\n ]\n },\n \"then\": \"1280*960\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"2:3\"\n ]\n },\n \"then\": \"1024*1536\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"3:2\"\n ]\n },\n \"then\": \"1536*1024\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"9:16\"\n ]\n },\n \"then\": \"864*1536\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"16:9\"\n ]\n },\n \"then\": \"1536*864\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"1024x1024\"\n ]\n },\n \"then\": \"1024*1024\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"1024x1536\"\n ]\n },\n \"then\": \"1024*1536\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"1536x1024\"\n ]\n },\n \"then\": \"1536*1024\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"1024x1280\"\n ]\n },\n \"then\": \"1024*1280\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"1280x1024\"\n ]\n },\n \"then\": \"1280*1024\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"960x1280\"\n ]\n },\n \"then\": \"960*1280\"\n },\n {\n \"when\": {\n \"$eq\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"1280x960\"\n ]\n },\n \"then\": \"1280*960\"\n }\n ],\n \"default\": null\n }\n }\n },\n \"n\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$toInt\": {\n \"$ref\": \"request.imageCount\"\n }\n },\n 0\n ]\n },\n \"then\": {\n \"$toInt\": {\n \"$ref\": \"request.imageCount\"\n }\n },\n \"else\": 1\n }\n },\n \"prompt_extend\": true,\n \"enable_thinking\": false,\n \"negative_prompt\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.qianwenai-maas-image.negative_prompt\"\n }\n },\n \"seed\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.qianwenai-maas-image.seed\"\n }\n },\n \"watermark\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.watermark\"\n }\n }\n }\n },\n \"headers\": {\n \"X-DashScope-Async\": \"enable\"\n },\n \"originPath\": true\n },\n \"poll\": {\n \"method\": \"GET\",\n \"path\": \"/api/v1/tasks/{{taskId}}\",\n \"originPath\": true\n },\n \"response\": {\n \"taskId\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.output.task_id\"\n },\n {\n \"$ref\": \"response.task_id\"\n },\n {\n \"$ref\": \"taskId\"\n }\n ]\n },\n \"status\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.output.task_status\"\n },\n {\n \"$ref\": \"response.status\"\n },\n \"pending\"\n ]\n },\n \"message\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.output.message\"\n },\n {\n \"$ref\": \"response.message\"\n }\n ]\n },\n \"images\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"response.output.choices.0.message.content\"\n },\n \"as\": \"item\",\n \"in\": {\n \"url\": {\n \"$omitEmpty\": {\n \"$ref\": \"item.image\"\n }\n }\n }\n }\n },\n \"errorPaths\": [\n \"code\",\n \"output.code\"\n ],\n \"resultEphemeral\": true,\n \"messagePaths\": [\n \"message\",\n \"output.message\"\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": "qianwenai-maas-image", + "label": "QianWenAI MAAS", + "capabilities": [ + "image" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://maas.qianwenaiapi.com", + "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": "插件命名空间内的厂商扩展字段。" + } + ], + "create": { + "method": "POST", + "path": "/api/v1/services/aigc/multimodal-generation/generation", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "input": { + "messages": [ + { + "role": "user", + "content": { + "$concatArrays": [ + { + "$map": { + "from": { + "$sortByOrder": { + "$ref": "request.images" + } + }, + "as": "media", + "in": { + "image": { + "$coalesce": [ + { + "$ref": "media.dataUrl" + }, + { + "$ref": "media.url" + } + ] + } + } + } + }, + [ + { + "text": { + "$ref": "request.prompt" + } + } + ] + ] + } + } + ] + }, + "parameters": { + "size": { + "$omitEmpty": { + "$switch": { + "cases": [ + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "1:1" + ] + }, + "then": "1024*1024" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "3:4" + ] + }, + "then": "960*1280" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "4:3" + ] + }, + "then": "1280*960" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "2:3" + ] + }, + "then": "1024*1536" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "3:2" + ] + }, + "then": "1536*1024" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "9:16" + ] + }, + "then": "864*1536" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "16:9" + ] + }, + "then": "1536*864" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "1024x1024" + ] + }, + "then": "1024*1024" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "1024x1536" + ] + }, + "then": "1024*1536" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "1536x1024" + ] + }, + "then": "1536*1024" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "1024x1280" + ] + }, + "then": "1024*1280" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "1280x1024" + ] + }, + "then": "1280*1024" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "960x1280" + ] + }, + "then": "960*1280" + }, + { + "when": { + "$eq": [ + { + "$ref": "request.aspectRatio" + }, + "1280x960" + ] + }, + "then": "1280*960" + } + ], + "default": null + } + } + }, + "n": { + "$if": { + "condition": { + "$gt": [ + { + "$toInt": { + "$ref": "request.imageCount" + } + }, + 0 + ] + }, + "then": { + "$toInt": { + "$ref": "request.imageCount" + } + }, + "else": 1 + } + }, + "prompt_extend": true, + "enable_thinking": false, + "negative_prompt": { + "$omitEmpty": { + "$ref": "request.providerOptions.qianwenai-maas-image.negative_prompt" + } + }, + "seed": { + "$omitEmpty": { + "$ref": "request.providerOptions.qianwenai-maas-image.seed" + } + }, + "watermark": { + "$omitEmpty": { + "$ref": "request.watermark" + } + } + } + }, + "headers": { + "X-DashScope-Async": "enable" + }, + "originPath": true + }, + "poll": { + "method": "GET", + "path": "/api/v1/tasks/{{taskId}}", + "originPath": true + }, + "response": { + "taskId": { + "$coalesce": [ + { + "$ref": "response.output.task_id" + }, + { + "$ref": "response.task_id" + }, + { + "$ref": "taskId" + } + ] + }, + "status": { + "$coalesce": [ + { + "$ref": "response.output.task_status" + }, + { + "$ref": "response.status" + }, + "pending" + ] + }, + "message": { + "$coalesce": [ + { + "$ref": "response.output.message" + }, + { + "$ref": "response.message" + } + ] + }, + "images": { + "$map": { + "from": { + "$ref": "response.output.choices.0.message.content" + }, + "as": "item", + "in": { + "url": { + "$omitEmpty": { + "$ref": "item.image" + } + } + } + } + }, + "errorPaths": [ + "code", + "output.code" + ], + "resultEphemeral": true, + "messagePaths": [ + "message", + "output.message" + ] + } + } + ] + } +}