From 4290af0c294e38a9f64b52a38bfa4bdce3453d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B1=B3=E7=BD=97?= Date: Tue, 13 Oct 2026 20:56:49 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(provider):=20=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE=20-=20=E6=96=B0=E5=A2=9E=E5=8D=83=E9=97=AE?= =?UTF-8?q?=20AI=20=E5=B9=B3=E5=8F=B0=EF=BC=88QianWenAI=20MAAS=EF=BC=89?= =?UTF-8?q?=E5=9B=BE=E5=83=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 千问 AI 平台的网关域名 maas.qianwenaiapi.com 与阿里云百炼的 dashscope.aliyuncs.com 不是同一个入口,现有模板无法直接复用:把渠道 Base URL 改过去后,create 端点仍是百炼的 image-generation 路径。 单独提供 qianwenai-maas-image 插件,走平台自己的多模态端点 POST /api/v1/services/aigc/multimodal-generation/generation。报文形状与 百炼多模态一致(参考图放 input.messages[].content[].image,任务结果在 output.choices[0].message.content[].image),size 用“宽*高”, enable_thinking 固定 false。 --- .../protocol/qianwenai_maas_image_test.go | 174 ++++++ docs/content/docs/progress/pending-test.mdx | 8 + plugin-packages/generate-catalog.mjs | 49 ++ .../qianwenai-maas-image.yingce-plugin | Bin 0 -> 9893 bytes .../qianwenai-maas-image/README.md | 5 + .../qianwenai-maas-image/docs/interface.md | 518 ++++++++++++++++++ .../qianwenai-maas-image/manifest.json | 426 ++++++++++++++ 7 files changed, 1180 insertions(+) create mode 100644 backend/internal/protocol/qianwenai_maas_image_test.go create mode 100644 plugin-packages/qianwenai-maas-image.yingce-plugin create mode 100644 plugin-packages/qianwenai-maas-image/README.md create mode 100644 plugin-packages/qianwenai-maas-image/docs/interface.md create mode 100644 plugin-packages/qianwenai-maas-image/manifest.json 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..0e82a0015 --- /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 +} \ No newline at end of file diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 0989dfffa..29003cb37 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,6 +5,14 @@ description: 当前项目已实现功能的手工验证清单与未确认边界 # 待测试 +## 千问 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 测试已补齐但**本机未安装 Go 工具链,尚未实际执行**,需 CI 或具备 Go 1.25 环境复跑。 +- 实现位置:`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 0000000000000000000000000000000000000000..6342dc5b74ceb40df32c5ce51433dc6aae64ef1d GIT binary patch literal 9893 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 P4f^lY|I<6<|FQlDOiunw literal 0 HcmV?d00001 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" + ] + } + } + ] + } +} From cc53d4534f50485fb0ddc7435c6034f645eb8077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B1=B3=E7=BD=97?= Date: Tue, 13 Oct 2026 21:33:17 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test(provider):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E5=8D=83=E9=97=AE=20AI=20=E5=B9=B3=E5=8F=B0=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=9A=84=E6=96=87=E4=BB=B6=E6=9C=AB=E5=B0=BE?= =?UTF-8?q?=E6=8D=A2=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gofmt 要求文件以换行结尾,缺失会导致 CI 的格式检查失败。 --- backend/internal/protocol/qianwenai_maas_image_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/internal/protocol/qianwenai_maas_image_test.go b/backend/internal/protocol/qianwenai_maas_image_test.go index 0e82a0015..c46050c2d 100644 --- a/backend/internal/protocol/qianwenai_maas_image_test.go +++ b/backend/internal/protocol/qianwenai_maas_image_test.go @@ -171,4 +171,4 @@ func qianwenaiMaasImageContent(t *testing.T, body map[string]any) []any { } content, _ := message["content"].([]any) return content -} \ No newline at end of file +}