From 12125e18f631066fd1a93d1649937bf4e5d308a4 Mon Sep 17 00:00:00 2001 From: ddcat666 <26564566+ddcat666@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:43:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E8=A7=92=E8=89=B2=E4=B8=8E?= =?UTF-8?q?=E9=9F=B3=E9=A2=91=E5=B7=A5=E4=BD=9C=E5=8F=B0=20-=20=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E7=B4=A0=E6=9D=90=E9=80=89=E6=8B=A9=E3=80=81=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=8F=82=E6=95=B0=E4=B8=8E=E5=90=8C=E6=AD=A5=E4=BA=A4?= =?UTF-8?q?=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/content/docs/backend/http-api.mdx | 5 +- plugin-packages/autodl-comfyui.yingce-plugin | Bin 15218 -> 95181 bytes .../dashscope-wan3-video/docs/interface.md | 8 +- .../dashscope-wan3-video/manifest.json | 8 +- .../doubao-streaming-tts.yingce-plugin | Bin 0 -> 9679 bytes .../doubao-streaming-tts/README.md | 5 + .../doubao-streaming-tts/docs/interface.md | 455 ++++++++++++++++++ .../doubao-streaming-tts/manifest.json | 381 +++++++++++++++ plugin-packages/generate-catalog.mjs | 54 +++ .../docs/interface.md | 29 +- .../newapi-video-generations-v1/manifest.json | 29 +- plugin-packages/openai-images/README.md | 2 - .../openai-images/docs/interface.md | 4 +- plugin-packages/openai-images/manifest.json | 6 +- .../assets/asset-library-picker-modal.tsx | 125 ++++- .../components/assets/asset-rich-cover.tsx | 137 ++++++ web/src/components/audio-settings-panel.tsx | 120 ++--- .../components/canvas/asset-picker-modal.tsx | 26 +- .../canvas/canvas-audio-settings-popover.tsx | 6 +- .../canvas/canvas-character-library-modal.tsx | 126 +++++ .../canvas-character-reference-modal.tsx | 402 +++++++++++----- .../canvas-character-reference-node.tsx | 103 +++- .../canvas/canvas-cloud-agent-chat-ui.tsx | 2 + .../canvas/canvas-cloud-agent-panel-parts.tsx | 8 +- .../canvas/canvas-config-node-panel.tsx | 8 + .../canvas/canvas-node-generation.ts | 11 +- .../canvas/canvas-node-prompt-config.ts | 2 + web/src/components/canvas/canvas-node.tsx | 8 +- .../canvas/character-media-library.tsx | 177 +++++++ web/src/lib/asset-record.ts | 8 +- web/src/lib/audio-generation.ts | 113 ++++- .../lib/canvas/agent-approval-presentation.ts | 4 +- web/src/lib/canvas/agent-canvas-actions.ts | 4 +- web/src/lib/canvas/agent-canvas-patch.ts | 9 +- web/src/lib/canvas/agent-tool-presentation.ts | 4 +- .../lib/canvas/canvas-connection-policy.ts | 24 +- .../canvas/canvas-generation-submission.ts | 4 + web/src/lib/canvas/canvas-project-domain.ts | 2 + .../lib/canvas/canvas-project-generation.ts | 4 + .../canvas/generation-contract.generated.ts | 20 + .../definitions/builtin-nodes.tsx | 2 +- .../definitions/add-node-menu-tools.tsx | 2 +- web/src/lib/doubao-audio-voices.ts | 118 +++++ web/src/lib/model-selection.ts | 56 ++- .../components/api-log-detail-drawer.tsx | 324 ++++++++----- .../components/channel-model-editor-form.ts | 2 +- .../channel-model-price-tier-fields.tsx | 4 +- web/src/pages/admin/logs/logs-page.css | 272 +++++++++++ web/src/pages/admin/logs/logs-page.tsx | 43 +- web/src/pages/assets/asset-library-cards.tsx | 8 +- web/src/pages/assets/asset-library-format.ts | 15 +- web/src/pages/assets/asset-library-panels.tsx | 120 ++++- web/src/pages/assets/index.tsx | 9 +- .../canvas-media-generation-executors.ts | 16 +- web/src/pages/canvas/canvas-sync-status.tsx | 42 +- web/src/pages/canvas/project.tsx | 51 +- .../use-canvas-connection-controller.ts | 20 +- web/src/pages/canvas/use-canvas-upload.ts | 5 +- web/src/services/api/agent.ts | 2 +- web/src/services/api/auth.ts | 14 +- web/src/services/api/generation-task.ts | 35 +- web/src/services/api/projects.ts | 45 +- web/src/services/user-data-sync.ts | 246 ++++++---- web/src/stores/config-workflow-fields.ts | 4 + web/src/stores/use-asset-store.ts | 13 +- web/src/stores/use-config-store.ts | 6 + web/src/stores/use-sync-progress-store.ts | 2 +- web/src/types/canvas.ts | 2 + web/test/admin-ui-regressions.test.ts | 8 +- web/test/agent-canvas-refresh.test.ts | 121 +++-- web/test/agent-chat-node-links.test.tsx | 7 + web/test/audio-generation.test.ts | 41 ++ web/test/canvas-character-menu.test.ts | 16 + web/test/canvas-model-policy.test.ts | 102 +++- .../canvas-node-generation-mentions.test.ts | 40 +- web/test/canvas-remote-revision.test.ts | 96 ++-- web/test/channel-model-editor-form.test.ts | 10 + 77 files changed, 3641 insertions(+), 721 deletions(-) create mode 100644 plugin-packages/doubao-streaming-tts.yingce-plugin create mode 100644 plugin-packages/doubao-streaming-tts/README.md create mode 100644 plugin-packages/doubao-streaming-tts/docs/interface.md create mode 100644 plugin-packages/doubao-streaming-tts/manifest.json create mode 100644 web/src/components/assets/asset-rich-cover.tsx create mode 100644 web/src/components/canvas/canvas-character-library-modal.tsx create mode 100644 web/src/components/canvas/character-media-library.tsx create mode 100644 web/src/lib/doubao-audio-voices.ts create mode 100644 web/test/audio-generation.test.ts create mode 100644 web/test/canvas-character-menu.test.ts diff --git a/docs/content/docs/backend/http-api.mdx b/docs/content/docs/backend/http-api.mdx index fb82b814d..d8c4cca97 100644 --- a/docs/content/docs/backend/http-api.mdx +++ b/docs/content/docs/backend/http-api.mdx @@ -17,9 +17,10 @@ description: 信封、错误码与 OpenAPI 资源展示、播放、下载、复制地址和模型输入统一消费资源访问合同,不在客户端通过 `direct=1`、`proxy=1` 或存储厂商规则选择出口。 -- `POST /api/resources/access` 要求登录,请求体为 1–100 项 `{ resourceId, purpose?, variant? }`。`purpose` 支持 `display`、`copy`、`download`、`browser-process`、`provider-input`;`variant` 支持 `original`、`playback`。每项独立返回 `access` 或结构化 `error`。 +- `POST /api/resources/access` 要求登录,请求体为 1–100 项 `{ resourceId, purpose?, variant? }`。`purpose` 支持 `display`、`copy`、`download`、`browser-process`、`provider-input`;`variant` 支持 `original`、`playback`、`thumbnail`(缩略图未生成前回落原件,`fallbackReason` 为 `thumbnail_not_ready`)。每项独立返回 `access` 或结构化 `error`。 - `access.delivery` 为 `cdn`、`origin`、`platform-local` 或 `platform-proxy`。配置了可用 CDN 时返回 CDN 地址;OSS 源站签名是源站直连,不等于 CDN。`requireCDN` 开启且 CDN 鉴权不完整时失败,不静默回源。浏览器的展示、复制、下载和本地处理不允许使用 `platform-proxy`,云资源正文始终由 OSS/CDN 直接发送给浏览器;`allowPrivateProxy` 只允许服务端在 `provider-input` 场景向第三方模型提交参考素材时读取私有源站。 -- `expiresAt` / `refreshAt` 只描述当前授权有效期。`provider-input` 默认 4 小时,其余签名访问默认 5 分钟;公开 CDN 可没有 `expiresAt`。签名地址不写入画布或服务端持久化内容。 +- `expiresAt` / `refreshAt` 只描述当前授权有效期。有效期来自后台「资源与请求策略」(`provider-input` 与其余签名访问分别配置,默认均为 4 小时);公开 CDN 可没有 `expiresAt`。签名地址不写入画布或服务端持久化内容。 +- `display`、`copy`、`browser-process` 的源站签名把签名起始时间对齐到有效期 1/4 的时间窗(按 Unix 纪元对齐),过期时间为窗口起点加有效期:同一窗口内重复签发得到逐字节相同的 URL,刷新画布或多实例签发都能命中浏览器 HTTP 缓存;签名同时携带 `response-cache-control=private, max-age=<有效期>, immutable`,由对象存储回写缓存头。`download` 与 `provider-input` 仍按当前时间签发,不带缓存覆盖。 - `GET /api/resources/:id/file` 仍是稳定平台入口,负责本地/平台代理字节流,云资源只返回 307 到 CDN 或对象存储;`?access=1` 只返回访问描述,不打开正文。`Range` 只影响字节流路径。 - 公共资源、画布分享、外观、公告、管理员资源和日志媒体复用同一个 HTTP 执行器。公共签名链接受自身到期时间和变体绑定限制,但已签发的外部 URL 不承诺立即撤销。 diff --git a/plugin-packages/autodl-comfyui.yingce-plugin b/plugin-packages/autodl-comfyui.yingce-plugin index a163beb682287c49ac33f15d848aa6d0f0c144eb..b21f866c29a49a3a7f4f91c0f2d1e39705a83983 100644 GIT binary patch literal 95181 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 diff --git a/plugin-packages/dashscope-wan3-video/docs/interface.md b/plugin-packages/dashscope-wan3-video/docs/interface.md index 62b889f26..bdcf6368c 100644 --- a/plugin-packages/dashscope-wan3-video/docs/interface.md +++ b/plugin-packages/dashscope-wan3-video/docs/interface.md @@ -44,7 +44,7 @@ | `create.body.model` | `{"$ref":"request.model"}` | | `create.body.input.prompt` | `{"$omitEmpty":{"$ref":"request.prompt"}}` | | `create.body.input.media` | `{"$omitEmpty":{"$coalesce":[{"$ref":"request.providerOptions.dashscope-wan3-video.media"},{"$map":{"from":{"$sortByOrder":{"$ref":"request.inputs"}},"as":"media","in":{"type":{"$coalesce":[{"$ref":"media.role"},{"$if":{"condition":{"$eq":[{"$ref":"media.kind"},"image"]},"then":"reference_image","else":{"$if":{"condition":{"$eq":[{"$ref":"media.kind"},"video"]},"then":"reference_video","else":{"$if":{"condition":{"$eq":[{"$ref":"media.kind"},"audio"]},"then":"reference_audio","else":{"$ref":"media.kind"}}}}}}}]},"url":{"$ref":"media.value"}}}}]}}` | -| `create.body.parameters.resolution` | `{"$omitEmpty":{"$upper":{"$ref":"request.resolution"}}}` | +| `create.body.parameters.resolution` | `{"$omitEmpty":{"$ref":"request.resolution"}}` | | `create.body.parameters.ratio` | `{"$omitEmpty":{"$ref":"request.aspectRatio"}}` | | `create.body.parameters.duration` | `{"$omitEmpty":{"$ref":"request.duration"}}` | | `create.body.parameters.audio` | `{"$coalesce":[{"$ref":"request.providerOptions.dashscope-wan3-video.audio"},{"$ref":"request.generateAudio"}]}` | @@ -98,7 +98,7 @@ "apiVersion": "yingce.plugin/v2", "id": "dashscope-wan3-video", "name": "DashScope Wan 3.0 Video", - "version": "2.0.1", + "version": "2.0.0", "author": "Alibaba Cloud / 影策", "description": "DashScope Wan 3.0 Video 独立请求协议插件。", "documentation": "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>", @@ -385,9 +385,7 @@ "parameters": { "resolution": { "$omitEmpty": { - "$upper": { - "$ref": "request.resolution" - } + "$ref": "request.resolution" } }, "ratio": { diff --git a/plugin-packages/dashscope-wan3-video/manifest.json b/plugin-packages/dashscope-wan3-video/manifest.json index bac9e33bd..c5df9c114 100644 --- a/plugin-packages/dashscope-wan3-video/manifest.json +++ b/plugin-packages/dashscope-wan3-video/manifest.json @@ -2,10 +2,10 @@ "apiVersion": "yingce.plugin/v2", "id": "dashscope-wan3-video", "name": "DashScope Wan 3.0 Video", - "version": "2.0.1", + "version": "2.0.0", "author": "Alibaba Cloud / 影策", "description": "DashScope Wan 3.0 Video 独立请求协议插件。", - "documentation": "# DashScope Wan 3.0 Video\n\n该目录是独立官方协议插件源码。后端从生成的 `dashscope-wan3-video.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。\n\n完整接口见 [docs/interface.md](docs/interface.md)。\n\n---\n\n# DashScope Wan 3.0 Video 接口字段\n\n## 协议身份\n\n- 插件 ID:`dashscope-wan3-video`。\n- Provider ID:`dashscope-wan3-video`。\n- 能力:`video`。\n- 默认 Base URL:`https://dashscope.aliyuncs.com`。\n- 鉴权驱动:`bearer`。\n- 创建:`POST /api/v1/services/aigc/video-generation/video-synthesis`。\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/content/input` | 视频提示词。 |\n| `images` | media[] | 否 | `first/last/reference image` | 显式 role 图片输入。 |\n| `videos` | media[] | 否 | `reference video` | 参考视频。 |\n| `audios` | media[] | 否 | `reference audio/voice` | 参考音频或音色。 |\n| `duration` | integer | 否 | `duration/seconds` | 时长秒数。 |\n| `aspectRatio` | string | 否 | `ratio/aspect_ratio/size` | 画幅比例或尺寸。 |\n| `resolution` | string | 否 | `resolution` | 分辨率档位。 |\n| `generateAudio` | boolean | 否 | `generate_audio` | 是否生成音频。 |\n| `watermark` | boolean | 否 | `watermark` | 水印开关。 |\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/video-generation/video-synthesis\"` |\n| `create.contentType` | `\"application/json\"` |\n| `create.body.model` | `{\"$ref\":\"request.model\"}` |\n| `create.body.input.prompt` | `{\"$omitEmpty\":{\"$ref\":\"request.prompt\"}}` |\n| `create.body.input.media` | `{\"$omitEmpty\":{\"$coalesce\":[{\"$ref\":\"request.providerOptions.dashscope-wan3-video.media\"},{\"$map\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.inputs\"}},\"as\":\"media\",\"in\":{\"type\":{\"$coalesce\":[{\"$ref\":\"media.role\"},{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"media.kind\"},\"image\"]},\"then\":\"reference_image\",\"else\":{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"media.kind\"},\"video\"]},\"then\":\"reference_video\",\"else\":{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"media.kind\"},\"audio\"]},\"then\":\"reference_audio\",\"else\":{\"$ref\":\"media.kind\"}}}}}}}]},\"url\":{\"$ref\":\"media.value\"}}}}]}}` |\n| `create.body.parameters.resolution` | `{\"$omitEmpty\":{\"$upper\":{\"$ref\":\"request.resolution\"}}}` |\n| `create.body.parameters.ratio` | `{\"$omitEmpty\":{\"$ref\":\"request.aspectRatio\"}}` |\n| `create.body.parameters.duration` | `{\"$omitEmpty\":{\"$ref\":\"request.duration\"}}` |\n| `create.body.parameters.audio` | `{\"$coalesce\":[{\"$ref\":\"request.providerOptions.dashscope-wan3-video.audio\"},{\"$ref\":\"request.generateAudio\"}]}` |\n| `create.body.parameters.seed` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.dashscope-wan3-video.seed\"}}` |\n| `create.body.parameters.prompt_extend` | `{\"$coalesce\":[{\"$ref\":\"request.providerOptions.dashscope-wan3-video.prompt_extend\"},true]}` |\n| `create.body.parameters.watermark` | `{\"$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.dashscope-wan3-video.audio`\n- `providerOptions.dashscope-wan3-video.media`\n- `providerOptions.dashscope-wan3-video.prompt_extend`\n- `providerOptions.dashscope-wan3-video.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` | `{\"$if\":{\"condition\":{\"$eq\":[{\"$lower\":{\"$ref\":\"response.output.task_status\"}},\"unknown\"]},\"then\":\"failed\",\"else\":{\"$coalesce\":[{\"$ref\":\"response.output.task_status\"},{\"$ref\":\"response.status\"},\"pending\"]}}}` |\n| `response.message` | `{\"$coalesce\":[{\"$ref\":\"response.error.message\"},{\"$ref\":\"response.message\"},{\"$ref\":\"response.fail_reason\"}]}` |\n| `response.videos` | `{\"$ref\":\"response.output.video_url\"}` |\n| `response.errorPaths[0]` | `\"code\"` |\n| `response.errorPaths[1]` | `\"output.code\"` |\n| `response.resultEphemeral` | `true` |\n| `response.usage` | `{\"$ref\":\"response.usage\"}` |\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该包只代表上述线协议 profile;同一品牌的其他 endpoint、云区域或网关包装必须使用独立插件,不能根据模型名猜测。\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"dashscope-wan3-video\",\n \"name\": \"DashScope Wan 3.0 Video\",\n \"version\": \"2.0.1\",\n \"author\": \"Alibaba Cloud / 影策\",\n \"description\": \"DashScope Wan 3.0 Video 独立请求协议插件。\",\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\": \"dashscope-wan3-video\",\n \"label\": \"DashScope Wan 3.0 Video\",\n \"capabilities\": [\n \"video\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://dashscope.aliyuncs.com\",\n \"requiresPublicMediaUrls\": true,\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/content/input\",\n \"description\": \"视频提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"first/last/reference image\",\n \"description\": \"显式 role 图片输入。\"\n },\n {\n \"name\": \"videos\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference video\",\n \"description\": \"参考视频。\"\n },\n {\n \"name\": \"audios\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference audio/voice\",\n \"description\": \"参考音频或音色。\"\n },\n {\n \"name\": \"duration\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"duration/seconds\",\n \"description\": \"时长秒数。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"ratio/aspect_ratio/size\",\n \"description\": \"画幅比例或尺寸。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution\",\n \"description\": \"分辨率档位。\"\n },\n {\n \"name\": \"generateAudio\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"generate_audio\",\n \"description\": \"是否生成音频。\"\n },\n {\n \"name\": \"watermark\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"watermark\",\n \"description\": \"水印开关。\"\n },\n {\n \"name\": \"providerOptions\",\n \"type\": \"object\",\n \"required\": false,\n \"mapping\": \"provider-specific fields\",\n \"description\": \"插件命名空间内的厂商扩展字段。\"\n }\n ],\n \"validations\": [\n {\n \"assert\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$ref\": \"request.model\"\n }\n },\n [\n \"wan3.0-video-prime\",\n \"wan3.0-video\"\n ]\n ]\n },\n \"message\": \"Wan 3.0 Video 仅支持 wan3.0-video-prime 或 wan3.0-video\"\n },\n {\n \"assert\": {\n \"$lte\": [\n {\n \"$len\": {\n \"$filter\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"where\": {\n \"$in\": [\n {\n \"$ref\": \"media.role\"\n },\n [\n \"first_frame\"\n ]\n ]\n }\n }\n }\n },\n 1\n ]\n },\n \"message\": \"Wan 3.0 Video 最多只能有一个 first_frame\"\n },\n {\n \"assert\": {\n \"$lte\": [\n {\n \"$len\": {\n \"$filter\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"where\": {\n \"$in\": [\n {\n \"$ref\": \"media.role\"\n },\n [\n \"last_frame\"\n ]\n ]\n }\n }\n }\n },\n 1\n ]\n },\n \"message\": \"Wan 3.0 Video 最多只能有一个 last_frame\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/api/v1/services/aigc/video-generation/video-synthesis\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"input\": {\n \"prompt\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.prompt\"\n }\n },\n \"media\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.dashscope-wan3-video.media\"\n },\n {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.inputs\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"type\": {\n \"$coalesce\": [\n {\n \"$ref\": \"media.role\"\n },\n {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"media.kind\"\n },\n \"image\"\n ]\n },\n \"then\": \"reference_image\",\n \"else\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"media.kind\"\n },\n \"video\"\n ]\n },\n \"then\": \"reference_video\",\n \"else\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"media.kind\"\n },\n \"audio\"\n ]\n },\n \"then\": \"reference_audio\",\n \"else\": {\n \"$ref\": \"media.kind\"\n }\n }\n }\n }\n }\n }\n }\n ]\n },\n \"url\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n ]\n }\n }\n },\n \"parameters\": {\n \"resolution\": {\n \"$omitEmpty\": {\n \"$upper\": {\n \"$ref\": \"request.resolution\"\n }\n }\n },\n \"ratio\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.aspectRatio\"\n }\n },\n \"duration\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.duration\"\n }\n },\n \"audio\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.dashscope-wan3-video.audio\"\n },\n {\n \"$ref\": \"request.generateAudio\"\n }\n ]\n },\n \"seed\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.dashscope-wan3-video.seed\"\n }\n },\n \"prompt_extend\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.dashscope-wan3-video.prompt_extend\"\n },\n true\n ]\n },\n \"watermark\": {\n \"$ref\": \"request.watermark\"\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 \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$ref\": \"response.output.task_status\"\n }\n },\n \"unknown\"\n ]\n },\n \"then\": \"failed\",\n \"else\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.output.task_status\"\n },\n {\n \"$ref\": \"response.status\"\n },\n \"pending\"\n ]\n }\n }\n },\n \"message\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.error.message\"\n },\n {\n \"$ref\": \"response.message\"\n },\n {\n \"$ref\": \"response.fail_reason\"\n }\n ]\n },\n \"videos\": {\n \"$ref\": \"response.output.video_url\"\n },\n \"errorPaths\": [\n \"code\",\n \"output.code\"\n ],\n \"resultEphemeral\": true,\n \"usage\": {\n \"$ref\": \"response.usage\"\n },\n \"messagePaths\": [\n \"message\",\n \"output.message\"\n ]\n }\n }\n ]\n }\n}\n```\n\n", + "documentation": "# DashScope Wan 3.0 Video\n\n该目录是独立官方协议插件源码。后端从生成的 `dashscope-wan3-video.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。\n\n完整接口见 [docs/interface.md](docs/interface.md)。\n\n---\n\n# DashScope Wan 3.0 Video 接口字段\n\n## 协议身份\n\n- 插件 ID:`dashscope-wan3-video`。\n- Provider ID:`dashscope-wan3-video`。\n- 能力:`video`。\n- 默认 Base URL:`https://dashscope.aliyuncs.com`。\n- 鉴权驱动:`bearer`。\n- 创建:`POST /api/v1/services/aigc/video-generation/video-synthesis`。\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/content/input` | 视频提示词。 |\n| `images` | media[] | 否 | `first/last/reference image` | 显式 role 图片输入。 |\n| `videos` | media[] | 否 | `reference video` | 参考视频。 |\n| `audios` | media[] | 否 | `reference audio/voice` | 参考音频或音色。 |\n| `duration` | integer | 否 | `duration/seconds` | 时长秒数。 |\n| `aspectRatio` | string | 否 | `ratio/aspect_ratio/size` | 画幅比例或尺寸。 |\n| `resolution` | string | 否 | `resolution` | 分辨率档位。 |\n| `generateAudio` | boolean | 否 | `generate_audio` | 是否生成音频。 |\n| `watermark` | boolean | 否 | `watermark` | 水印开关。 |\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/video-generation/video-synthesis\"` |\n| `create.contentType` | `\"application/json\"` |\n| `create.body.model` | `{\"$ref\":\"request.model\"}` |\n| `create.body.input.prompt` | `{\"$omitEmpty\":{\"$ref\":\"request.prompt\"}}` |\n| `create.body.input.media` | `{\"$omitEmpty\":{\"$coalesce\":[{\"$ref\":\"request.providerOptions.dashscope-wan3-video.media\"},{\"$map\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.inputs\"}},\"as\":\"media\",\"in\":{\"type\":{\"$coalesce\":[{\"$ref\":\"media.role\"},{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"media.kind\"},\"image\"]},\"then\":\"reference_image\",\"else\":{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"media.kind\"},\"video\"]},\"then\":\"reference_video\",\"else\":{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"media.kind\"},\"audio\"]},\"then\":\"reference_audio\",\"else\":{\"$ref\":\"media.kind\"}}}}}}}]},\"url\":{\"$ref\":\"media.value\"}}}}]}}` |\n| `create.body.parameters.resolution` | `{\"$omitEmpty\":{\"$ref\":\"request.resolution\"}}` |\n| `create.body.parameters.ratio` | `{\"$omitEmpty\":{\"$ref\":\"request.aspectRatio\"}}` |\n| `create.body.parameters.duration` | `{\"$omitEmpty\":{\"$ref\":\"request.duration\"}}` |\n| `create.body.parameters.audio` | `{\"$coalesce\":[{\"$ref\":\"request.providerOptions.dashscope-wan3-video.audio\"},{\"$ref\":\"request.generateAudio\"}]}` |\n| `create.body.parameters.seed` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.dashscope-wan3-video.seed\"}}` |\n| `create.body.parameters.prompt_extend` | `{\"$coalesce\":[{\"$ref\":\"request.providerOptions.dashscope-wan3-video.prompt_extend\"},true]}` |\n| `create.body.parameters.watermark` | `{\"$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.dashscope-wan3-video.audio`\n- `providerOptions.dashscope-wan3-video.media`\n- `providerOptions.dashscope-wan3-video.prompt_extend`\n- `providerOptions.dashscope-wan3-video.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` | `{\"$if\":{\"condition\":{\"$eq\":[{\"$lower\":{\"$ref\":\"response.output.task_status\"}},\"unknown\"]},\"then\":\"failed\",\"else\":{\"$coalesce\":[{\"$ref\":\"response.output.task_status\"},{\"$ref\":\"response.status\"},\"pending\"]}}}` |\n| `response.message` | `{\"$coalesce\":[{\"$ref\":\"response.error.message\"},{\"$ref\":\"response.message\"},{\"$ref\":\"response.fail_reason\"}]}` |\n| `response.videos` | `{\"$ref\":\"response.output.video_url\"}` |\n| `response.errorPaths[0]` | `\"code\"` |\n| `response.errorPaths[1]` | `\"output.code\"` |\n| `response.resultEphemeral` | `true` |\n| `response.usage` | `{\"$ref\":\"response.usage\"}` |\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该包只代表上述线协议 profile;同一品牌的其他 endpoint、云区域或网关包装必须使用独立插件,不能根据模型名猜测。\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"dashscope-wan3-video\",\n \"name\": \"DashScope Wan 3.0 Video\",\n \"version\": \"2.0.0\",\n \"author\": \"Alibaba Cloud / 影策\",\n \"description\": \"DashScope Wan 3.0 Video 独立请求协议插件。\",\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\": \"dashscope-wan3-video\",\n \"label\": \"DashScope Wan 3.0 Video\",\n \"capabilities\": [\n \"video\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://dashscope.aliyuncs.com\",\n \"requiresPublicMediaUrls\": true,\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/content/input\",\n \"description\": \"视频提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"first/last/reference image\",\n \"description\": \"显式 role 图片输入。\"\n },\n {\n \"name\": \"videos\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference video\",\n \"description\": \"参考视频。\"\n },\n {\n \"name\": \"audios\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference audio/voice\",\n \"description\": \"参考音频或音色。\"\n },\n {\n \"name\": \"duration\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"duration/seconds\",\n \"description\": \"时长秒数。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"ratio/aspect_ratio/size\",\n \"description\": \"画幅比例或尺寸。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution\",\n \"description\": \"分辨率档位。\"\n },\n {\n \"name\": \"generateAudio\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"generate_audio\",\n \"description\": \"是否生成音频。\"\n },\n {\n \"name\": \"watermark\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"watermark\",\n \"description\": \"水印开关。\"\n },\n {\n \"name\": \"providerOptions\",\n \"type\": \"object\",\n \"required\": false,\n \"mapping\": \"provider-specific fields\",\n \"description\": \"插件命名空间内的厂商扩展字段。\"\n }\n ],\n \"validations\": [\n {\n \"assert\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$ref\": \"request.model\"\n }\n },\n [\n \"wan3.0-video-prime\",\n \"wan3.0-video\"\n ]\n ]\n },\n \"message\": \"Wan 3.0 Video 仅支持 wan3.0-video-prime 或 wan3.0-video\"\n },\n {\n \"assert\": {\n \"$lte\": [\n {\n \"$len\": {\n \"$filter\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"where\": {\n \"$in\": [\n {\n \"$ref\": \"media.role\"\n },\n [\n \"first_frame\"\n ]\n ]\n }\n }\n }\n },\n 1\n ]\n },\n \"message\": \"Wan 3.0 Video 最多只能有一个 first_frame\"\n },\n {\n \"assert\": {\n \"$lte\": [\n {\n \"$len\": {\n \"$filter\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"where\": {\n \"$in\": [\n {\n \"$ref\": \"media.role\"\n },\n [\n \"last_frame\"\n ]\n ]\n }\n }\n }\n },\n 1\n ]\n },\n \"message\": \"Wan 3.0 Video 最多只能有一个 last_frame\"\n }\n ],\n \"create\": {\n \"method\": \"POST\",\n \"path\": \"/api/v1/services/aigc/video-generation/video-synthesis\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"input\": {\n \"prompt\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.prompt\"\n }\n },\n \"media\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.dashscope-wan3-video.media\"\n },\n {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.inputs\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"type\": {\n \"$coalesce\": [\n {\n \"$ref\": \"media.role\"\n },\n {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"media.kind\"\n },\n \"image\"\n ]\n },\n \"then\": \"reference_image\",\n \"else\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"media.kind\"\n },\n \"video\"\n ]\n },\n \"then\": \"reference_video\",\n \"else\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"media.kind\"\n },\n \"audio\"\n ]\n },\n \"then\": \"reference_audio\",\n \"else\": {\n \"$ref\": \"media.kind\"\n }\n }\n }\n }\n }\n }\n }\n ]\n },\n \"url\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n ]\n }\n }\n },\n \"parameters\": {\n \"resolution\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.resolution\"\n }\n },\n \"ratio\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.aspectRatio\"\n }\n },\n \"duration\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.duration\"\n }\n },\n \"audio\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.dashscope-wan3-video.audio\"\n },\n {\n \"$ref\": \"request.generateAudio\"\n }\n ]\n },\n \"seed\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.dashscope-wan3-video.seed\"\n }\n },\n \"prompt_extend\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.dashscope-wan3-video.prompt_extend\"\n },\n true\n ]\n },\n \"watermark\": {\n \"$ref\": \"request.watermark\"\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 \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$lower\": {\n \"$ref\": \"response.output.task_status\"\n }\n },\n \"unknown\"\n ]\n },\n \"then\": \"failed\",\n \"else\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.output.task_status\"\n },\n {\n \"$ref\": \"response.status\"\n },\n \"pending\"\n ]\n }\n }\n },\n \"message\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.error.message\"\n },\n {\n \"$ref\": \"response.message\"\n },\n {\n \"$ref\": \"response.fail_reason\"\n }\n ]\n },\n \"videos\": {\n \"$ref\": \"response.output.video_url\"\n },\n \"errorPaths\": [\n \"code\",\n \"output.code\"\n ],\n \"resultEphemeral\": true,\n \"usage\": {\n \"$ref\": \"response.usage\"\n },\n \"messagePaths\": [\n \"message\",\n \"output.message\"\n ]\n }\n }\n ]\n }\n}\n```\n\n", "permissions": [ "generation.run", "media.read" @@ -289,9 +289,7 @@ "parameters": { "resolution": { "$omitEmpty": { - "$upper": { - "$ref": "request.resolution" - } + "$ref": "request.resolution" } }, "ratio": { diff --git a/plugin-packages/doubao-streaming-tts.yingce-plugin b/plugin-packages/doubao-streaming-tts.yingce-plugin new file mode 100644 index 0000000000000000000000000000000000000000..4ad3bde169baa510fdef19b51f018c5da476f48a GIT binary patch literal 9679 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 literal 0 HcmV?d00001 diff --git a/plugin-packages/doubao-streaming-tts/README.md b/plugin-packages/doubao-streaming-tts/README.md new file mode 100644 index 000000000..3f3ecd2a3 --- /dev/null +++ b/plugin-packages/doubao-streaming-tts/README.md @@ -0,0 +1,5 @@ +# 豆包音频生成 + +该目录是独立官方协议插件源码。后端从生成的 `doubao-streaming-tts.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。 + +完整接口见 [docs/interface.md](docs/interface.md)。 diff --git a/plugin-packages/doubao-streaming-tts/docs/interface.md b/plugin-packages/doubao-streaming-tts/docs/interface.md new file mode 100644 index 000000000..29c957e74 --- /dev/null +++ b/plugin-packages/doubao-streaming-tts/docs/interface.md @@ -0,0 +1,455 @@ +# 豆包音频生成 接口字段 + +## 协议身份 + +- 插件 ID:`doubao-streaming-tts`。 +- Provider ID:`doubao-streaming-tts`。 +- 能力:`audio`。 +- 默认 Base URL:`https://openspeech.bytedance.com`。 +- 鉴权驱动:`header`。 +- 创建:`POST /api/v3/tts/create`。 +- 生命周期:同步响应。 + +## 配置字段 + +| 字段 | 类型 | 必填 | 含义 | +| --- | --- | --- | --- | +| `apiKey` | secret | 是 | API Key | + +## 统一字段映射 + +| 统一字段 | 类型 | 必填 | 上游映射 | 说明 | +| --- | --- | --- | --- | --- | +| `model` | string | 是 | `model` | 音频模型 ID。 | +| `prompt` | string | 是 | `input` | 待合成文本。 | +| `providerOptions` | object | 否 | `provider-specific fields` | 插件命名空间内的厂商扩展字段。 | + +## 上游请求模板逐字段清单 + +下表由插件请求模板生成,覆盖 body、query、headers 和 multipart 文件声明中的每个字段。 + +| 上游位置 | 值或转换表达式 | +| --- | --- | +| `create.method` | `"POST"` | +| `create.path` | `"/api/v3/tts/create"` | +| `create.contentType` | `"application/json"` | +| `create.body.model` | `{"$ref":"request.model"}` | +| `create.body.text_prompt` | `{"$ref":"request.prompt"}` | +| `create.body.references` | `{"$omitEmpty":{"$if":{"condition":{"$gt":[{"$len":{"$ref":"request.images"}},0]},"then":{"$map":{"from":{"$ref":"request.images"},"as":"media","in":{"image_url":{"$omitEmpty":{"$if":{"condition":{"$eq":[{"$ref":"media.source.type"},"url"]},"then":{"$ref":"media.url"},"else":null}}},"image_data":{"$omitEmpty":{"$if":{"condition":{"$eq":[{"$ref":"media.source.type"},"data"]},"then":{"$dataPayload":{"$ref":"media.value"}},"else":null}}}}}},"else":{"$if":{"condition":{"$gt":[{"$len":{"$ref":"request.audios"}},0]},"then":{"$concatArrays":[{"$map":{"from":{"$ref":"request.audios"},"as":"media","in":{"audio_url":{"$omitEmpty":{"$if":{"condition":{"$eq":[{"$ref":"media.source.type"},"url"]},"then":{"$ref":"media.url"},"else":null}}},"audio_data":{"$omitEmpty":{"$if":{"condition":{"$eq":[{"$ref":"media.source.type"},"data"]},"then":{"$dataPayload":{"$ref":"media.value"}},"else":null}}}}}},{"$if":{"condition":{"$and":[{"$ref":"request.extra.audioVoice"},{"$lt":[{"$len":{"$ref":"request.audios"}},3]}]},"then":[{"speaker":{"$ref":"request.extra.audioVoice"}}],"else":[]}}]},"else":{"$if":{"condition":{"$ref":"request.extra.audioVoice"},"then":[{"speaker":{"$ref":"request.extra.audioVoice"}}],"else":null}}}}}}}` | +| `create.body.audio_config.format` | `{"$coalesce":[{"$ref":"request.extra.audioFormat"},"mp3"]}` | +| `create.body.audio_config.speech_rate` | `{"$omitEmpty":{"$if":{"condition":{"$ne":[{"$toFloat":{"$ref":"request.extra.audioSpeed"}},0]},"then":{"$add":[{"$multiply":[{"$toFloat":{"$ref":"request.extra.audioSpeed"}},100]},-100]},"else":null}}}` | +| `create.headers.X-Api-Request-Id` | `{"$ref":"request.extra.idempotencyKey"}` | + +## Provider 扩展键 + +- 无额外扩展键。 + +动态模型或工作流允许使用文档声明的完整 `parameters/input/extra_body` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。 + +## 响应映射逐字段清单 + +| 映射位置 | 上游路径或转换表达式 | +| --- | --- | +| `response.errorPaths[0]` | `"code"` | +| `response.messagePaths[0]` | `"message"` | +| `response.status` | `"succeeded"` | +| `response.resultKind` | `"audio"` | +| `response.audios` | `{"$if":{"condition":{"$ref":"response.url"},"then":{"url":{"$ref":"response.url"},"ephemeral":true},"else":{"$if":{"condition":{"$ref":"response.audio"},"then":{"dataUrl":{"$ref":"response.audio"},"mimeType":{"$if":{"condition":{"$eq":[{"$ref":"request.extra.audioFormat"},"wav"]},"then":"audio/wav","else":{"$if":{"condition":{"$eq":[{"$ref":"request.extra.audioFormat"},"pcm"]},"then":"audio/pcm","else":{"$if":{"condition":{"$eq":[{"$ref":"request.extra.audioFormat"},"ogg_opus"]},"then":"audio/ogg","else":"audio/mpeg"}}}}}}},"else":null}}}}` | + +## 响应与错误 + +插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。 + +## 兼容边界 + +火山引擎 seed-audio-1.0 非流式音频生成。POST /api/v3/tts/create。不传 references 为纯文本生成;连接音频时写入 references 的 audio_url 或 audio_data,最多 3 段,参考音频少于 3 段且指定了音色时再追加一个 speaker;连接图片时写入 image_url 或 image_data,最多 1 张。图片和音频不能同时使用。没有参考素材时,speaker 只在用户明确指定音色时发送,且官方仅接受语音合成 2.0 音色或复刻音色。text_prompt 引用参考音频时使用 @Audio1、@Audio2。响应是单个 JSON,audio 为 Base64,url 两小时过期。 + + +## Manifest 完整接口定义 + +以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。 + +```json +{ + "apiVersion": "yingce.plugin/v2", + "id": "doubao-streaming-tts", + "name": "豆包音频生成", + "version": "2.0.0", + "author": "Volcengine / 影策", + "description": "豆包音频生成 独立请求协议插件。", + "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": "doubao-streaming-tts", + "label": "豆包音频生成", + "capabilities": [ + "audio" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://openspeech.bytedance.com", + "requiresPublicMediaUrls": false, + "auth": { + "type": "header", + "field": "apiKey", + "header": "X-Api-Key" + }, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "音频模型 ID。" + }, + { + "name": "prompt", + "type": "string", + "required": true, + "mapping": "input", + "description": "待合成文本。" + }, + { + "name": "providerOptions", + "type": "object", + "required": false, + "mapping": "provider-specific fields", + "description": "插件命名空间内的厂商扩展字段。" + } + ], + "create": { + "method": "POST", + "path": "/api/v3/tts/create", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "text_prompt": { + "$ref": "request.prompt" + }, + "references": { + "$omitEmpty": { + "$if": { + "condition": { + "$gt": [ + { + "$len": { + "$ref": "request.images" + } + }, + 0 + ] + }, + "then": { + "$map": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "in": { + "image_url": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$ref": "media.source.type" + }, + "url" + ] + }, + "then": { + "$ref": "media.url" + }, + "else": null + } + } + }, + "image_data": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$ref": "media.source.type" + }, + "data" + ] + }, + "then": { + "$dataPayload": { + "$ref": "media.value" + } + }, + "else": null + } + } + } + } + } + }, + "else": { + "$if": { + "condition": { + "$gt": [ + { + "$len": { + "$ref": "request.audios" + } + }, + 0 + ] + }, + "then": { + "$concatArrays": [ + { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "audio_url": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$ref": "media.source.type" + }, + "url" + ] + }, + "then": { + "$ref": "media.url" + }, + "else": null + } + } + }, + "audio_data": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$ref": "media.source.type" + }, + "data" + ] + }, + "then": { + "$dataPayload": { + "$ref": "media.value" + } + }, + "else": null + } + } + } + } + } + }, + { + "$if": { + "condition": { + "$and": [ + { + "$ref": "request.extra.audioVoice" + }, + { + "$lt": [ + { + "$len": { + "$ref": "request.audios" + } + }, + 3 + ] + } + ] + }, + "then": [ + { + "speaker": { + "$ref": "request.extra.audioVoice" + } + } + ], + "else": [] + } + } + ] + }, + "else": { + "$if": { + "condition": { + "$ref": "request.extra.audioVoice" + }, + "then": [ + { + "speaker": { + "$ref": "request.extra.audioVoice" + } + } + ], + "else": null + } + } + } + } + } + } + }, + "audio_config": { + "format": { + "$coalesce": [ + { + "$ref": "request.extra.audioFormat" + }, + "mp3" + ] + }, + "speech_rate": { + "$omitEmpty": { + "$if": { + "condition": { + "$ne": [ + { + "$toFloat": { + "$ref": "request.extra.audioSpeed" + } + }, + 0 + ] + }, + "then": { + "$add": [ + { + "$multiply": [ + { + "$toFloat": { + "$ref": "request.extra.audioSpeed" + } + }, + 100 + ] + }, + -100 + ] + }, + "else": null + } + } + } + } + }, + "headers": { + "X-Api-Request-Id": { + "$ref": "request.extra.idempotencyKey" + } + } + }, + "response": { + "errorPaths": [ + "code" + ], + "messagePaths": [ + "message" + ], + "status": "succeeded", + "resultKind": "audio", + "audios": { + "$if": { + "condition": { + "$ref": "response.url" + }, + "then": { + "url": { + "$ref": "response.url" + }, + "ephemeral": true + }, + "else": { + "$if": { + "condition": { + "$ref": "response.audio" + }, + "then": { + "dataUrl": { + "$ref": "response.audio" + }, + "mimeType": { + "$if": { + "condition": { + "$eq": [ + { + "$ref": "request.extra.audioFormat" + }, + "wav" + ] + }, + "then": "audio/wav", + "else": { + "$if": { + "condition": { + "$eq": [ + { + "$ref": "request.extra.audioFormat" + }, + "pcm" + ] + }, + "then": "audio/pcm", + "else": { + "$if": { + "condition": { + "$eq": [ + { + "$ref": "request.extra.audioFormat" + }, + "ogg_opus" + ] + }, + "then": "audio/ogg", + "else": "audio/mpeg" + } + } + } + } + } + } + }, + "else": null + } + } + } + } + } + } + ] + } +} +``` + diff --git a/plugin-packages/doubao-streaming-tts/manifest.json b/plugin-packages/doubao-streaming-tts/manifest.json new file mode 100644 index 000000000..4003c358b --- /dev/null +++ b/plugin-packages/doubao-streaming-tts/manifest.json @@ -0,0 +1,381 @@ +{ + "apiVersion": "yingce.plugin/v2", + "id": "doubao-streaming-tts", + "name": "豆包音频生成", + "version": "2.0.0", + "author": "Volcengine / 影策", + "description": "豆包音频生成 独立请求协议插件。", + "documentation": "# 豆包音频生成\n\n该目录是独立官方协议插件源码。后端从生成的 `doubao-streaming-tts.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。\n\n完整接口见 [docs/interface.md](docs/interface.md)。\n\n---\n\n# 豆包音频生成 接口字段\n\n## 协议身份\n\n- 插件 ID:`doubao-streaming-tts`。\n- Provider ID:`doubao-streaming-tts`。\n- 能力:`audio`。\n- 默认 Base URL:`https://openspeech.bytedance.com`。\n- 鉴权驱动:`header`。\n- 创建:`POST /api/v3/tts/create`。\n- 生命周期:同步响应。\n\n## 配置字段\n\n| 字段 | 类型 | 必填 | 含义 |\n| --- | --- | --- | --- |\n| `apiKey` | secret | 是 | API Key |\n\n## 统一字段映射\n\n| 统一字段 | 类型 | 必填 | 上游映射 | 说明 |\n| --- | --- | --- | --- | --- |\n| `model` | string | 是 | `model` | 音频模型 ID。 |\n| `prompt` | string | 是 | `input` | 待合成文本。 |\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/v3/tts/create\"` |\n| `create.contentType` | `\"application/json\"` |\n| `create.body.model` | `{\"$ref\":\"request.model\"}` |\n| `create.body.text_prompt` | `{\"$ref\":\"request.prompt\"}` |\n| `create.body.references` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$gt\":[{\"$len\":{\"$ref\":\"request.images\"}},0]},\"then\":{\"$map\":{\"from\":{\"$ref\":\"request.images\"},\"as\":\"media\",\"in\":{\"image_url\":{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"media.source.type\"},\"url\"]},\"then\":{\"$ref\":\"media.url\"},\"else\":null}}},\"image_data\":{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"media.source.type\"},\"data\"]},\"then\":{\"$dataPayload\":{\"$ref\":\"media.value\"}},\"else\":null}}}}}},\"else\":{\"$if\":{\"condition\":{\"$gt\":[{\"$len\":{\"$ref\":\"request.audios\"}},0]},\"then\":{\"$concatArrays\":[{\"$map\":{\"from\":{\"$ref\":\"request.audios\"},\"as\":\"media\",\"in\":{\"audio_url\":{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"media.source.type\"},\"url\"]},\"then\":{\"$ref\":\"media.url\"},\"else\":null}}},\"audio_data\":{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"media.source.type\"},\"data\"]},\"then\":{\"$dataPayload\":{\"$ref\":\"media.value\"}},\"else\":null}}}}}},{\"$if\":{\"condition\":{\"$and\":[{\"$ref\":\"request.extra.audioVoice\"},{\"$lt\":[{\"$len\":{\"$ref\":\"request.audios\"}},3]}]},\"then\":[{\"speaker\":{\"$ref\":\"request.extra.audioVoice\"}}],\"else\":[]}}]},\"else\":{\"$if\":{\"condition\":{\"$ref\":\"request.extra.audioVoice\"},\"then\":[{\"speaker\":{\"$ref\":\"request.extra.audioVoice\"}}],\"else\":null}}}}}}}` |\n| `create.body.audio_config.format` | `{\"$coalesce\":[{\"$ref\":\"request.extra.audioFormat\"},\"mp3\"]}` |\n| `create.body.audio_config.speech_rate` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$ne\":[{\"$toFloat\":{\"$ref\":\"request.extra.audioSpeed\"}},0]},\"then\":{\"$add\":[{\"$multiply\":[{\"$toFloat\":{\"$ref\":\"request.extra.audioSpeed\"}},100]},-100]},\"else\":null}}}` |\n| `create.headers.X-Api-Request-Id` | `{\"$ref\":\"request.extra.idempotencyKey\"}` |\n\n## Provider 扩展键\n\n- 无额外扩展键。\n\n动态模型或工作流允许使用文档声明的完整 `parameters/input/extra_body` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。\n\n## 响应映射逐字段清单\n\n| 映射位置 | 上游路径或转换表达式 |\n| --- | --- |\n| `response.errorPaths[0]` | `\"code\"` |\n| `response.messagePaths[0]` | `\"message\"` |\n| `response.status` | `\"succeeded\"` |\n| `response.resultKind` | `\"audio\"` |\n| `response.audios` | `{\"$if\":{\"condition\":{\"$ref\":\"response.url\"},\"then\":{\"url\":{\"$ref\":\"response.url\"},\"ephemeral\":true},\"else\":{\"$if\":{\"condition\":{\"$ref\":\"response.audio\"},\"then\":{\"dataUrl\":{\"$ref\":\"response.audio\"},\"mimeType\":{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"request.extra.audioFormat\"},\"wav\"]},\"then\":\"audio/wav\",\"else\":{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"request.extra.audioFormat\"},\"pcm\"]},\"then\":\"audio/pcm\",\"else\":{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"request.extra.audioFormat\"},\"ogg_opus\"]},\"then\":\"audio/ogg\",\"else\":\"audio/mpeg\"}}}}}}},\"else\":null}}}}` |\n\n## 响应与错误\n\n插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。\n\n## 兼容边界\n\n火山引擎 seed-audio-1.0 非流式音频生成。POST /api/v3/tts/create。不传 references 为纯文本生成;连接音频时写入 references 的 audio_url 或 audio_data,最多 3 段,参考音频少于 3 段且指定了音色时再追加一个 speaker;连接图片时写入 image_url 或 image_data,最多 1 张。图片和音频不能同时使用。没有参考素材时,speaker 只在用户明确指定音色时发送,且官方仅接受语音合成 2.0 音色或复刻音色。text_prompt 引用参考音频时使用 @Audio1、@Audio2。响应是单个 JSON,audio 为 Base64,url 两小时过期。\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"doubao-streaming-tts\",\n \"name\": \"豆包音频生成\",\n \"version\": \"2.0.0\",\n \"author\": \"Volcengine / 影策\",\n \"description\": \"豆包音频生成 独立请求协议插件。\",\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\": \"doubao-streaming-tts\",\n \"label\": \"豆包音频生成\",\n \"capabilities\": [\n \"audio\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://openspeech.bytedance.com\",\n \"requiresPublicMediaUrls\": false,\n \"auth\": {\n \"type\": \"header\",\n \"field\": \"apiKey\",\n \"header\": \"X-Api-Key\"\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\": \"input\",\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/v3/tts/create\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"text_prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"references\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 0\n ]\n },\n \"then\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.images\"\n },\n \"as\": \"media\",\n \"in\": {\n \"image_url\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"media.source.type\"\n },\n \"url\"\n ]\n },\n \"then\": {\n \"$ref\": \"media.url\"\n },\n \"else\": null\n }\n }\n },\n \"image_data\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"media.source.type\"\n },\n \"data\"\n ]\n },\n \"then\": {\n \"$dataPayload\": {\n \"$ref\": \"media.value\"\n }\n },\n \"else\": null\n }\n }\n }\n }\n }\n },\n \"else\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$ref\": \"request.audios\"\n }\n },\n 0\n ]\n },\n \"then\": {\n \"$concatArrays\": [\n {\n \"$map\": {\n \"from\": {\n \"$ref\": \"request.audios\"\n },\n \"as\": \"media\",\n \"in\": {\n \"audio_url\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"media.source.type\"\n },\n \"url\"\n ]\n },\n \"then\": {\n \"$ref\": \"media.url\"\n },\n \"else\": null\n }\n }\n },\n \"audio_data\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"media.source.type\"\n },\n \"data\"\n ]\n },\n \"then\": {\n \"$dataPayload\": {\n \"$ref\": \"media.value\"\n }\n },\n \"else\": null\n }\n }\n }\n }\n }\n },\n {\n \"$if\": {\n \"condition\": {\n \"$and\": [\n {\n \"$ref\": \"request.extra.audioVoice\"\n },\n {\n \"$lt\": [\n {\n \"$len\": {\n \"$ref\": \"request.audios\"\n }\n },\n 3\n ]\n }\n ]\n },\n \"then\": [\n {\n \"speaker\": {\n \"$ref\": \"request.extra.audioVoice\"\n }\n }\n ],\n \"else\": []\n }\n }\n ]\n },\n \"else\": {\n \"$if\": {\n \"condition\": {\n \"$ref\": \"request.extra.audioVoice\"\n },\n \"then\": [\n {\n \"speaker\": {\n \"$ref\": \"request.extra.audioVoice\"\n }\n }\n ],\n \"else\": null\n }\n }\n }\n }\n }\n }\n },\n \"audio_config\": {\n \"format\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.extra.audioFormat\"\n },\n \"mp3\"\n ]\n },\n \"speech_rate\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$ne\": [\n {\n \"$toFloat\": {\n \"$ref\": \"request.extra.audioSpeed\"\n }\n },\n 0\n ]\n },\n \"then\": {\n \"$add\": [\n {\n \"$multiply\": [\n {\n \"$toFloat\": {\n \"$ref\": \"request.extra.audioSpeed\"\n }\n },\n 100\n ]\n },\n -100\n ]\n },\n \"else\": null\n }\n }\n }\n }\n },\n \"headers\": {\n \"X-Api-Request-Id\": {\n \"$ref\": \"request.extra.idempotencyKey\"\n }\n }\n },\n \"response\": {\n \"errorPaths\": [\n \"code\"\n ],\n \"messagePaths\": [\n \"message\"\n ],\n \"status\": \"succeeded\",\n \"resultKind\": \"audio\",\n \"audios\": {\n \"$if\": {\n \"condition\": {\n \"$ref\": \"response.url\"\n },\n \"then\": {\n \"url\": {\n \"$ref\": \"response.url\"\n },\n \"ephemeral\": true\n },\n \"else\": {\n \"$if\": {\n \"condition\": {\n \"$ref\": \"response.audio\"\n },\n \"then\": {\n \"dataUrl\": {\n \"$ref\": \"response.audio\"\n },\n \"mimeType\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"request.extra.audioFormat\"\n },\n \"wav\"\n ]\n },\n \"then\": \"audio/wav\",\n \"else\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"request.extra.audioFormat\"\n },\n \"pcm\"\n ]\n },\n \"then\": \"audio/pcm\",\n \"else\": {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"request.extra.audioFormat\"\n },\n \"ogg_opus\"\n ]\n },\n \"then\": \"audio/ogg\",\n \"else\": \"audio/mpeg\"\n }\n }\n }\n }\n }\n }\n },\n \"else\": null\n }\n }\n }\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": "doubao-streaming-tts", + "label": "豆包音频生成", + "capabilities": [ + "audio" + ], + "scopes": [ + "admin.system-channel", + "user.custom-channel", + "canvas", + "creation", + "agent" + ], + "baseUrl": "https://openspeech.bytedance.com", + "requiresPublicMediaUrls": false, + "auth": { + "type": "header", + "field": "apiKey", + "header": "X-Api-Key" + }, + "parameters": [ + { + "name": "model", + "type": "string", + "required": true, + "mapping": "model", + "description": "音频模型 ID。" + }, + { + "name": "prompt", + "type": "string", + "required": true, + "mapping": "input", + "description": "待合成文本。" + }, + { + "name": "providerOptions", + "type": "object", + "required": false, + "mapping": "provider-specific fields", + "description": "插件命名空间内的厂商扩展字段。" + } + ], + "create": { + "method": "POST", + "path": "/api/v3/tts/create", + "contentType": "application/json", + "body": { + "model": { + "$ref": "request.model" + }, + "text_prompt": { + "$ref": "request.prompt" + }, + "references": { + "$omitEmpty": { + "$if": { + "condition": { + "$gt": [ + { + "$len": { + "$ref": "request.images" + } + }, + 0 + ] + }, + "then": { + "$map": { + "from": { + "$ref": "request.images" + }, + "as": "media", + "in": { + "image_url": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$ref": "media.source.type" + }, + "url" + ] + }, + "then": { + "$ref": "media.url" + }, + "else": null + } + } + }, + "image_data": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$ref": "media.source.type" + }, + "data" + ] + }, + "then": { + "$dataPayload": { + "$ref": "media.value" + } + }, + "else": null + } + } + } + } + } + }, + "else": { + "$if": { + "condition": { + "$gt": [ + { + "$len": { + "$ref": "request.audios" + } + }, + 0 + ] + }, + "then": { + "$concatArrays": [ + { + "$map": { + "from": { + "$ref": "request.audios" + }, + "as": "media", + "in": { + "audio_url": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$ref": "media.source.type" + }, + "url" + ] + }, + "then": { + "$ref": "media.url" + }, + "else": null + } + } + }, + "audio_data": { + "$omitEmpty": { + "$if": { + "condition": { + "$eq": [ + { + "$ref": "media.source.type" + }, + "data" + ] + }, + "then": { + "$dataPayload": { + "$ref": "media.value" + } + }, + "else": null + } + } + } + } + } + }, + { + "$if": { + "condition": { + "$and": [ + { + "$ref": "request.extra.audioVoice" + }, + { + "$lt": [ + { + "$len": { + "$ref": "request.audios" + } + }, + 3 + ] + } + ] + }, + "then": [ + { + "speaker": { + "$ref": "request.extra.audioVoice" + } + } + ], + "else": [] + } + } + ] + }, + "else": { + "$if": { + "condition": { + "$ref": "request.extra.audioVoice" + }, + "then": [ + { + "speaker": { + "$ref": "request.extra.audioVoice" + } + } + ], + "else": null + } + } + } + } + } + } + }, + "audio_config": { + "format": { + "$coalesce": [ + { + "$ref": "request.extra.audioFormat" + }, + "mp3" + ] + }, + "speech_rate": { + "$omitEmpty": { + "$if": { + "condition": { + "$ne": [ + { + "$toFloat": { + "$ref": "request.extra.audioSpeed" + } + }, + 0 + ] + }, + "then": { + "$add": [ + { + "$multiply": [ + { + "$toFloat": { + "$ref": "request.extra.audioSpeed" + } + }, + 100 + ] + }, + -100 + ] + }, + "else": null + } + } + } + } + }, + "headers": { + "X-Api-Request-Id": { + "$ref": "request.extra.idempotencyKey" + } + } + }, + "response": { + "errorPaths": [ + "code" + ], + "messagePaths": [ + "message" + ], + "status": "succeeded", + "resultKind": "audio", + "audios": { + "$if": { + "condition": { + "$ref": "response.url" + }, + "then": { + "url": { + "$ref": "response.url" + }, + "ephemeral": true + }, + "else": { + "$if": { + "condition": { + "$ref": "response.audio" + }, + "then": { + "dataUrl": { + "$ref": "response.audio" + }, + "mimeType": { + "$if": { + "condition": { + "$eq": [ + { + "$ref": "request.extra.audioFormat" + }, + "wav" + ] + }, + "then": "audio/wav", + "else": { + "$if": { + "condition": { + "$eq": [ + { + "$ref": "request.extra.audioFormat" + }, + "pcm" + ] + }, + "then": "audio/pcm", + "else": { + "$if": { + "condition": { + "$eq": [ + { + "$ref": "request.extra.audioFormat" + }, + "ogg_opus" + ] + }, + "then": "audio/ogg", + "else": "audio/mpeg" + } + } + } + } + } + } + }, + "else": null + } + } + } + } + } + } + ] + } +} diff --git a/plugin-packages/generate-catalog.mjs b/plugin-packages/generate-catalog.mjs index 33d016489..362c3a247 100644 --- a/plugin-packages/generate-catalog.mjs +++ b/plugin-packages/generate-catalog.mjs @@ -7,6 +7,8 @@ const scopes = ["admin.system-channel", "user.custom-channel", "canvas", "creati const ref = (path) => ({ $ref: path }); const omit = (value) => ({ $omitEmpty: value }); const coalesce = (...values) => ({ $coalesce: values }); +const addNumbers = (...values) => ({ $add: values }); +const multiply = (...values) => ({ $multiply: values }); const map = (from, as, body) => ({ $map: { from, as, in: body } }); const filter = (from, as, where) => ({ $filter: { from, as, where } }); const eq = (left, right) => ({ $eq: [left, right] }); @@ -565,6 +567,58 @@ add({ response: asyncResponse("image", { taskId: coalesce(ref("response.data.task_id"), ref("response.task_id"), ref("taskId")), status: coalesce(ref("response.data.status"), ref("response.status"), "pending"), images: coalesce(ref("response.data.image_urls"), ref("response.data.binary_data_base64")), errorPaths: ["code"], messagePaths: ["message"] }) }); +add({ + id: "doubao-streaming-tts", providerId: "doubao-streaming-tts", name: "豆包音频生成", vendor: "Volcengine", capability: "audio", + baseUrl: "https://openspeech.bytedance.com", auth: { type: "header", field: "apiKey", header: "X-Api-Key" }, params: audioParams, + notes: "火山引擎 seed-audio-1.0 非流式音频生成。POST /api/v3/tts/create。不传 references 为纯文本生成;连接音频时写入 references 的 audio_url 或 audio_data,最多 3 段,参考音频少于 3 段且指定了音色时再追加一个 speaker;连接图片时写入 image_url 或 image_data,最多 1 张。图片和音频不能同时使用。没有参考素材时,speaker 只在用户明确指定音色时发送,且官方仅接受语音合成 2.0 音色或复刻音色。text_prompt 引用参考音频时使用 @Audio1、@Audio2。响应是单个 JSON,audio 为 Base64,url 两小时过期。", + create: jsonCreate("/api/v3/tts/create", { + model: ref("request.model"), + text_prompt: ref("request.prompt"), + references: omit(conditional( + gt(len(ref("request.images")), 0), + map(ref("request.images"), "media", { + image_url: omit(conditional(eq(ref("media.source.type"), "url"), ref("media.url"))), + image_data: omit(conditional(eq(ref("media.source.type"), "data"), { $dataPayload: ref("media.value") })) + }), + conditional( + gt(len(ref("request.audios")), 0), + { + $concatArrays: [ + map(ref("request.audios"), "media", { + audio_url: omit(conditional(eq(ref("media.source.type"), "url"), ref("media.url"))), + audio_data: omit(conditional(eq(ref("media.source.type"), "data"), { $dataPayload: ref("media.value") })) + }), + conditional(and(ref("request.extra.audioVoice"), lt(len(ref("request.audios")), 3)), [{ speaker: ref("request.extra.audioVoice") }], []) + ] + }, + conditional(ref("request.extra.audioVoice"), [{ speaker: ref("request.extra.audioVoice") }], null) + ) + )), + audio_config: { + format: coalesce(ref("request.extra.audioFormat"), "mp3"), + speech_rate: omit(conditional(ne(toFloat(ref("request.extra.audioSpeed")), 0), addNumbers(multiply(toFloat(ref("request.extra.audioSpeed")), 100), -100))) + } + }, { + headers: { + "X-Api-Request-Id": ref("request.extra.idempotencyKey") + } + }), + response: { + errorPaths: ["code"], + messagePaths: ["message"], + status: "succeeded", + resultKind: "audio", + audios: conditional( + ref("response.url"), + { url: ref("response.url"), ephemeral: true }, + conditional(ref("response.audio"), { + dataUrl: ref("response.audio"), + mimeType: conditional(eq(ref("request.extra.audioFormat"), "wav"), "audio/wav", conditional(eq(ref("request.extra.audioFormat"), "pcm"), "audio/pcm", conditional(eq(ref("request.extra.audioFormat"), "ogg_opus"), "audio/ogg", "audio/mpeg"))) + }) + ) + } +}); + add({ id: "openai-audio", providerId: "openai-audio", name: "OpenAI Audio Speech", vendor: "OpenAI", capability: "audio", baseUrl: "https://api.openai.com", auth: bearer, params: audioParams, diff --git a/plugin-packages/newapi-video-generations-v1/docs/interface.md b/plugin-packages/newapi-video-generations-v1/docs/interface.md index 4ee4c7153..34d20c7c6 100644 --- a/plugin-packages/newapi-video-generations-v1/docs/interface.md +++ b/plugin-packages/newapi-video-generations-v1/docs/interface.md @@ -67,7 +67,7 @@ | `response.taskId` | `{"$coalesce":[{"$ref":"response.data.task_id"},{"$ref":"response.data.taskId"},{"$ref":"response.task_id"},{"$ref":"response.taskId"},{"$ref":"response.data.id"},{"$ref":"response.id"},{"$ref":"taskId"}]}` | | `response.status` | `{"$coalesce":[{"$ref":"response.status"},{"$ref":"response.state"},{"$ref":"response.data.status"},"pending"]}` | | `response.message` | `{"$coalesce":[{"$ref":"response.error.message"},{"$ref":"response.message"},{"$ref":"response.fail_reason"}]}` | -| `response.videos` | `{"$coalesce":[{"$map":{"from":{"$ref":"response.data"},"as":"item","in":{"$coalesce":[{"$ref":"item.url"},{"$ref":"item.video_url"},{"$ref":"item.videoUrl"},{"$ref":"item.output_url"},{"$ref":"item.outputUrl"}]}}},{"$ref":"response.data.result_url"},{"$ref":"response.data.video_url"},{"$ref":"response.data.output_url"},{"$ref":"response.data.url"},{"$ref":"response.data.metadata.url"},{"$ref":"response.data.data.video_url"},{"$ref":"response.data.data.output_url"},{"$ref":"response.data.data.result_url"},{"$ref":"response.data.data.url"},{"$ref":"response.data.data.metadata.url"},{"$ref":"response.video_url"},{"$ref":"response.videoUrl"},{"$ref":"response.result_url"},{"$ref":"response.output_url"},{"$ref":"response.url"},{"$ref":"response.metadata.url"},{"$ref":"response.output.url"}]}` | +| `response.videos` | `{"$coalesce":[{"$ref":"response.data.result_url"},{"$ref":"response.data.video_url"},{"$ref":"response.data.output_url"},{"$ref":"response.data.url"},{"$ref":"response.data.metadata.url"},{"$ref":"response.data.data.video_url"},{"$ref":"response.data.data.output_url"},{"$ref":"response.data.data.result_url"},{"$ref":"response.data.data.url"},{"$ref":"response.data.data.metadata.url"},{"$ref":"response.video_url"},{"$ref":"response.videoUrl"},{"$ref":"response.result_url"},{"$ref":"response.output_url"},{"$ref":"response.url"},{"$ref":"response.metadata.url"},{"$ref":"response.output.url"}]}` | | `response.errorPaths[0]` | `"error.code"` | | `response.resultEphemeral` | `true` | @@ -345,33 +345,6 @@ }, "videos": { "$coalesce": [ - { - "$map": { - "from": { - "$ref": "response.data" - }, - "as": "item", - "in": { - "$coalesce": [ - { - "$ref": "item.url" - }, - { - "$ref": "item.video_url" - }, - { - "$ref": "item.videoUrl" - }, - { - "$ref": "item.output_url" - }, - { - "$ref": "item.outputUrl" - } - ] - } - } - }, { "$ref": "response.data.result_url" }, diff --git a/plugin-packages/newapi-video-generations-v1/manifest.json b/plugin-packages/newapi-video-generations-v1/manifest.json index 58eeae9d7..b8b5bb0a2 100644 --- a/plugin-packages/newapi-video-generations-v1/manifest.json +++ b/plugin-packages/newapi-video-generations-v1/manifest.json @@ -5,7 +5,7 @@ "version": "2.0.0", "author": "NewAPI / 影策", "description": "NewAPI Video Generations Channel 2 独立请求协议插件。", - "documentation": "# NewAPI Video Generations Channel 2\n\n该目录是独立官方协议插件源码。后端从生成的 `newapi-video-generations-v1.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。\n\n完整接口见 [docs/interface.md](docs/interface.md)。\n\n---\n\n# NewAPI Video Generations Channel 2 接口字段\n\n## 协议身份\n\n- 插件 ID:`newapi-video-generations-v1`。\n- Provider ID:`newapi-channel-2`。\n- 能力:`video`。\n- 默认 Base URL:`http://127.0.0.1:3000`。\n- 鉴权驱动:`bearer`。\n- 创建:`POST /v1/video/generations`。\n- 查询:`GET /v1/video/generations/{{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/content/input` | 视频提示词。 |\n| `images` | media[] | 否 | `first/last/reference image` | 显式 role 图片输入。 |\n| `videos` | media[] | 否 | `reference video` | 参考视频。 |\n| `audios` | media[] | 否 | `reference audio/voice` | 参考音频或音色。 |\n| `duration` | integer | 否 | `duration/seconds` | 时长秒数。 |\n| `aspectRatio` | string | 否 | `ratio/aspect_ratio/size` | 画幅比例或尺寸。 |\n| `resolution` | string | 否 | `resolution` | 分辨率档位。 |\n| `generateAudio` | boolean | 否 | `generate_audio` | 是否生成音频。 |\n| `watermark` | boolean | 否 | `watermark` | 水印开关。 |\n| `providerOptions` | object | 否 | `provider-specific fields` | 插件命名空间内的厂商扩展字段。 |\n\n## 上游请求模板逐字段清单\n\n下表由插件请求模板生成,覆盖 body、query、headers 和 multipart 文件声明中的每个字段。\n\n| 上游位置 | 值或转换表达式 |\n| --- | --- |\n| `create.method` | `\"POST\"` |\n| `create.path` | `\"/v1/video/generations\"` |\n| `create.contentType` | `\"application/json\"` |\n| `create.body.model` | `{\"$ref\":\"request.model\"}` |\n| `create.body.prompt` | `{\"$ref\":\"request.prompt\"}` |\n| `create.body.seconds` | `{\"$toString\":{\"$ref\":\"request.duration\"}}` |\n| `create.body.aspect_ratio` | `{\"$coalesce\":[{\"$ref\":\"request.aspectRatio\"},\"16:9\"]}` |\n| `create.body.resolution` | `{\"$omitEmpty\":{\"$ref\":\"request.resolution\"}}` |\n| `create.body.generate_audio` | `{\"$ref\":\"request.generateAudio\"}` |\n| `create.body.image_urls` | `{\"$omitEmpty\":{\"$map\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.images\"}},\"as\":\"media\",\"in\":{\"$ref\":\"media.value\"}}}}` |\n| `create.body.video_urls` | `{\"$omitEmpty\":{\"$map\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.videos\"}},\"as\":\"media\",\"in\":{\"$ref\":\"media.value\"}}}}` |\n| `create.body.audio_urls` | `{\"$omitEmpty\":{\"$map\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.audios\"}},\"as\":\"media\",\"in\":{\"$ref\":\"media.value\"}}}}` |\n| `poll.method` | `\"GET\"` |\n| `poll.path` | `\"/v1/video/generations/{{taskId}}\"` |\n| `poll.contentType` | `\"application/json\"` |\n\n## Provider 扩展键\n\n- 无额外扩展键。\n\n动态模型或工作流允许使用文档声明的完整 `parameters/input/extra_body` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。\n\n## 响应映射逐字段清单\n\n| 映射位置 | 上游路径或转换表达式 |\n| --- | --- |\n| `response.taskId` | `{\"$coalesce\":[{\"$ref\":\"response.data.task_id\"},{\"$ref\":\"response.data.taskId\"},{\"$ref\":\"response.task_id\"},{\"$ref\":\"response.taskId\"},{\"$ref\":\"response.data.id\"},{\"$ref\":\"response.id\"},{\"$ref\":\"taskId\"}]}` |\n| `response.status` | `{\"$coalesce\":[{\"$ref\":\"response.status\"},{\"$ref\":\"response.state\"},{\"$ref\":\"response.data.status\"},\"pending\"]}` |\n| `response.message` | `{\"$coalesce\":[{\"$ref\":\"response.error.message\"},{\"$ref\":\"response.message\"},{\"$ref\":\"response.fail_reason\"}]}` |\n| `response.videos` | `{\"$coalesce\":[{\"$map\":{\"from\":{\"$ref\":\"response.data\"},\"as\":\"item\",\"in\":{\"$coalesce\":[{\"$ref\":\"item.url\"},{\"$ref\":\"item.video_url\"},{\"$ref\":\"item.videoUrl\"},{\"$ref\":\"item.output_url\"},{\"$ref\":\"item.outputUrl\"}]}}},{\"$ref\":\"response.data.result_url\"},{\"$ref\":\"response.data.video_url\"},{\"$ref\":\"response.data.output_url\"},{\"$ref\":\"response.data.url\"},{\"$ref\":\"response.data.metadata.url\"},{\"$ref\":\"response.data.data.video_url\"},{\"$ref\":\"response.data.data.output_url\"},{\"$ref\":\"response.data.data.result_url\"},{\"$ref\":\"response.data.data.url\"},{\"$ref\":\"response.data.data.metadata.url\"},{\"$ref\":\"response.video_url\"},{\"$ref\":\"response.videoUrl\"},{\"$ref\":\"response.result_url\"},{\"$ref\":\"response.output_url\"},{\"$ref\":\"response.url\"},{\"$ref\":\"response.metadata.url\"},{\"$ref\":\"response.output.url\"}]}` |\n| `response.errorPaths[0]` | `\"error.code\"` |\n| `response.resultEphemeral` | `true` |\n\n## 响应与错误\n\n插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。\n\n## 兼容边界\n\n该包只代表上述线协议 profile;同一品牌的其他 endpoint、云区域或网关包装必须使用独立插件,不能根据模型名猜测。\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"newapi-video-generations-v1\",\n \"name\": \"NewAPI Video Generations Channel 2\",\n \"version\": \"2.0.0\",\n \"author\": \"NewAPI / 影策\",\n \"description\": \"NewAPI Video Generations Channel 2 独立请求协议插件。\",\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\": \"newapi-channel-2\",\n \"label\": \"NewAPI Video Generations Channel 2\",\n \"capabilities\": [\n \"video\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"http://127.0.0.1:3000\",\n \"requiresPublicMediaUrls\": true,\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/content/input\",\n \"description\": \"视频提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"first/last/reference image\",\n \"description\": \"显式 role 图片输入。\"\n },\n {\n \"name\": \"videos\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference video\",\n \"description\": \"参考视频。\"\n },\n {\n \"name\": \"audios\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference audio/voice\",\n \"description\": \"参考音频或音色。\"\n },\n {\n \"name\": \"duration\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"duration/seconds\",\n \"description\": \"时长秒数。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"ratio/aspect_ratio/size\",\n \"description\": \"画幅比例或尺寸。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution\",\n \"description\": \"分辨率档位。\"\n },\n {\n \"name\": \"generateAudio\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"generate_audio\",\n \"description\": \"是否生成音频。\"\n },\n {\n \"name\": \"watermark\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"watermark\",\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\": \"/v1/video/generations\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"seconds\": {\n \"$toString\": {\n \"$ref\": \"request.duration\"\n }\n },\n \"aspect_ratio\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"16:9\"\n ]\n },\n \"resolution\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.resolution\"\n }\n },\n \"generate_audio\": {\n \"$ref\": \"request.generateAudio\"\n },\n \"image_urls\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"video_urls\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.videos\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"audio_urls\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.audios\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n }\n },\n \"poll\": {\n \"method\": \"GET\",\n \"path\": \"/v1/video/generations/{{taskId}}\"\n },\n \"response\": {\n \"taskId\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.data.task_id\"\n },\n {\n \"$ref\": \"response.data.taskId\"\n },\n {\n \"$ref\": \"response.task_id\"\n },\n {\n \"$ref\": \"response.taskId\"\n },\n {\n \"$ref\": \"response.data.id\"\n },\n {\n \"$ref\": \"response.id\"\n },\n {\n \"$ref\": \"taskId\"\n }\n ]\n },\n \"status\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.status\"\n },\n {\n \"$ref\": \"response.state\"\n },\n {\n \"$ref\": \"response.data.status\"\n },\n \"pending\"\n ]\n },\n \"message\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.error.message\"\n },\n {\n \"$ref\": \"response.message\"\n },\n {\n \"$ref\": \"response.fail_reason\"\n }\n ]\n },\n \"videos\": {\n \"$coalesce\": [\n {\n \"$map\": {\n \"from\": {\n \"$ref\": \"response.data\"\n },\n \"as\": \"item\",\n \"in\": {\n \"$coalesce\": [\n {\n \"$ref\": \"item.url\"\n },\n {\n \"$ref\": \"item.video_url\"\n },\n {\n \"$ref\": \"item.videoUrl\"\n },\n {\n \"$ref\": \"item.output_url\"\n },\n {\n \"$ref\": \"item.outputUrl\"\n }\n ]\n }\n }\n },\n {\n \"$ref\": \"response.data.result_url\"\n },\n {\n \"$ref\": \"response.data.video_url\"\n },\n {\n \"$ref\": \"response.data.output_url\"\n },\n {\n \"$ref\": \"response.data.url\"\n },\n {\n \"$ref\": \"response.data.metadata.url\"\n },\n {\n \"$ref\": \"response.data.data.video_url\"\n },\n {\n \"$ref\": \"response.data.data.output_url\"\n },\n {\n \"$ref\": \"response.data.data.result_url\"\n },\n {\n \"$ref\": \"response.data.data.url\"\n },\n {\n \"$ref\": \"response.data.data.metadata.url\"\n },\n {\n \"$ref\": \"response.video_url\"\n },\n {\n \"$ref\": \"response.videoUrl\"\n },\n {\n \"$ref\": \"response.result_url\"\n },\n {\n \"$ref\": \"response.output_url\"\n },\n {\n \"$ref\": \"response.url\"\n },\n {\n \"$ref\": \"response.metadata.url\"\n },\n {\n \"$ref\": \"response.output.url\"\n }\n ]\n },\n \"errorPaths\": [\n \"error.code\"\n ],\n \"resultEphemeral\": true\n }\n }\n ]\n }\n}\n```\n\n", + "documentation": "# NewAPI Video Generations Channel 2\n\n该目录是独立官方协议插件源码。后端从生成的 `newapi-video-generations-v1.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。\n\n完整接口见 [docs/interface.md](docs/interface.md)。\n\n---\n\n# NewAPI Video Generations Channel 2 接口字段\n\n## 协议身份\n\n- 插件 ID:`newapi-video-generations-v1`。\n- Provider ID:`newapi-channel-2`。\n- 能力:`video`。\n- 默认 Base URL:`http://127.0.0.1:3000`。\n- 鉴权驱动:`bearer`。\n- 创建:`POST /v1/video/generations`。\n- 查询:`GET /v1/video/generations/{{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/content/input` | 视频提示词。 |\n| `images` | media[] | 否 | `first/last/reference image` | 显式 role 图片输入。 |\n| `videos` | media[] | 否 | `reference video` | 参考视频。 |\n| `audios` | media[] | 否 | `reference audio/voice` | 参考音频或音色。 |\n| `duration` | integer | 否 | `duration/seconds` | 时长秒数。 |\n| `aspectRatio` | string | 否 | `ratio/aspect_ratio/size` | 画幅比例或尺寸。 |\n| `resolution` | string | 否 | `resolution` | 分辨率档位。 |\n| `generateAudio` | boolean | 否 | `generate_audio` | 是否生成音频。 |\n| `watermark` | boolean | 否 | `watermark` | 水印开关。 |\n| `providerOptions` | object | 否 | `provider-specific fields` | 插件命名空间内的厂商扩展字段。 |\n\n## 上游请求模板逐字段清单\n\n下表由插件请求模板生成,覆盖 body、query、headers 和 multipart 文件声明中的每个字段。\n\n| 上游位置 | 值或转换表达式 |\n| --- | --- |\n| `create.method` | `\"POST\"` |\n| `create.path` | `\"/v1/video/generations\"` |\n| `create.contentType` | `\"application/json\"` |\n| `create.body.model` | `{\"$ref\":\"request.model\"}` |\n| `create.body.prompt` | `{\"$ref\":\"request.prompt\"}` |\n| `create.body.seconds` | `{\"$toString\":{\"$ref\":\"request.duration\"}}` |\n| `create.body.aspect_ratio` | `{\"$coalesce\":[{\"$ref\":\"request.aspectRatio\"},\"16:9\"]}` |\n| `create.body.resolution` | `{\"$omitEmpty\":{\"$ref\":\"request.resolution\"}}` |\n| `create.body.generate_audio` | `{\"$ref\":\"request.generateAudio\"}` |\n| `create.body.image_urls` | `{\"$omitEmpty\":{\"$map\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.images\"}},\"as\":\"media\",\"in\":{\"$ref\":\"media.value\"}}}}` |\n| `create.body.video_urls` | `{\"$omitEmpty\":{\"$map\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.videos\"}},\"as\":\"media\",\"in\":{\"$ref\":\"media.value\"}}}}` |\n| `create.body.audio_urls` | `{\"$omitEmpty\":{\"$map\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.audios\"}},\"as\":\"media\",\"in\":{\"$ref\":\"media.value\"}}}}` |\n| `poll.method` | `\"GET\"` |\n| `poll.path` | `\"/v1/video/generations/{{taskId}}\"` |\n| `poll.contentType` | `\"application/json\"` |\n\n## Provider 扩展键\n\n- 无额外扩展键。\n\n动态模型或工作流允许使用文档声明的完整 `parameters/input/extra_body` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。\n\n## 响应映射逐字段清单\n\n| 映射位置 | 上游路径或转换表达式 |\n| --- | --- |\n| `response.taskId` | `{\"$coalesce\":[{\"$ref\":\"response.data.task_id\"},{\"$ref\":\"response.data.taskId\"},{\"$ref\":\"response.task_id\"},{\"$ref\":\"response.taskId\"},{\"$ref\":\"response.data.id\"},{\"$ref\":\"response.id\"},{\"$ref\":\"taskId\"}]}` |\n| `response.status` | `{\"$coalesce\":[{\"$ref\":\"response.status\"},{\"$ref\":\"response.state\"},{\"$ref\":\"response.data.status\"},\"pending\"]}` |\n| `response.message` | `{\"$coalesce\":[{\"$ref\":\"response.error.message\"},{\"$ref\":\"response.message\"},{\"$ref\":\"response.fail_reason\"}]}` |\n| `response.videos` | `{\"$coalesce\":[{\"$ref\":\"response.data.result_url\"},{\"$ref\":\"response.data.video_url\"},{\"$ref\":\"response.data.output_url\"},{\"$ref\":\"response.data.url\"},{\"$ref\":\"response.data.metadata.url\"},{\"$ref\":\"response.data.data.video_url\"},{\"$ref\":\"response.data.data.output_url\"},{\"$ref\":\"response.data.data.result_url\"},{\"$ref\":\"response.data.data.url\"},{\"$ref\":\"response.data.data.metadata.url\"},{\"$ref\":\"response.video_url\"},{\"$ref\":\"response.videoUrl\"},{\"$ref\":\"response.result_url\"},{\"$ref\":\"response.output_url\"},{\"$ref\":\"response.url\"},{\"$ref\":\"response.metadata.url\"},{\"$ref\":\"response.output.url\"}]}` |\n| `response.errorPaths[0]` | `\"error.code\"` |\n| `response.resultEphemeral` | `true` |\n\n## 响应与错误\n\n插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。\n\n## 兼容边界\n\n该包只代表上述线协议 profile;同一品牌的其他 endpoint、云区域或网关包装必须使用独立插件,不能根据模型名猜测。\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"newapi-video-generations-v1\",\n \"name\": \"NewAPI Video Generations Channel 2\",\n \"version\": \"2.0.0\",\n \"author\": \"NewAPI / 影策\",\n \"description\": \"NewAPI Video Generations Channel 2 独立请求协议插件。\",\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\": \"newapi-channel-2\",\n \"label\": \"NewAPI Video Generations Channel 2\",\n \"capabilities\": [\n \"video\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"http://127.0.0.1:3000\",\n \"requiresPublicMediaUrls\": true,\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/content/input\",\n \"description\": \"视频提示词。\"\n },\n {\n \"name\": \"images\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"first/last/reference image\",\n \"description\": \"显式 role 图片输入。\"\n },\n {\n \"name\": \"videos\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference video\",\n \"description\": \"参考视频。\"\n },\n {\n \"name\": \"audios\",\n \"type\": \"media[]\",\n \"required\": false,\n \"mapping\": \"reference audio/voice\",\n \"description\": \"参考音频或音色。\"\n },\n {\n \"name\": \"duration\",\n \"type\": \"integer\",\n \"required\": false,\n \"mapping\": \"duration/seconds\",\n \"description\": \"时长秒数。\"\n },\n {\n \"name\": \"aspectRatio\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"ratio/aspect_ratio/size\",\n \"description\": \"画幅比例或尺寸。\"\n },\n {\n \"name\": \"resolution\",\n \"type\": \"string\",\n \"required\": false,\n \"mapping\": \"resolution\",\n \"description\": \"分辨率档位。\"\n },\n {\n \"name\": \"generateAudio\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"generate_audio\",\n \"description\": \"是否生成音频。\"\n },\n {\n \"name\": \"watermark\",\n \"type\": \"boolean\",\n \"required\": false,\n \"mapping\": \"watermark\",\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\": \"/v1/video/generations\",\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"seconds\": {\n \"$toString\": {\n \"$ref\": \"request.duration\"\n }\n },\n \"aspect_ratio\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.aspectRatio\"\n },\n \"16:9\"\n ]\n },\n \"resolution\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.resolution\"\n }\n },\n \"generate_audio\": {\n \"$ref\": \"request.generateAudio\"\n },\n \"image_urls\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"video_urls\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.videos\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"audio_urls\": {\n \"$omitEmpty\": {\n \"$map\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.audios\"\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n }\n },\n \"poll\": {\n \"method\": \"GET\",\n \"path\": \"/v1/video/generations/{{taskId}}\"\n },\n \"response\": {\n \"taskId\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.data.task_id\"\n },\n {\n \"$ref\": \"response.data.taskId\"\n },\n {\n \"$ref\": \"response.task_id\"\n },\n {\n \"$ref\": \"response.taskId\"\n },\n {\n \"$ref\": \"response.data.id\"\n },\n {\n \"$ref\": \"response.id\"\n },\n {\n \"$ref\": \"taskId\"\n }\n ]\n },\n \"status\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.status\"\n },\n {\n \"$ref\": \"response.state\"\n },\n {\n \"$ref\": \"response.data.status\"\n },\n \"pending\"\n ]\n },\n \"message\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.error.message\"\n },\n {\n \"$ref\": \"response.message\"\n },\n {\n \"$ref\": \"response.fail_reason\"\n }\n ]\n },\n \"videos\": {\n \"$coalesce\": [\n {\n \"$ref\": \"response.data.result_url\"\n },\n {\n \"$ref\": \"response.data.video_url\"\n },\n {\n \"$ref\": \"response.data.output_url\"\n },\n {\n \"$ref\": \"response.data.url\"\n },\n {\n \"$ref\": \"response.data.metadata.url\"\n },\n {\n \"$ref\": \"response.data.data.video_url\"\n },\n {\n \"$ref\": \"response.data.data.output_url\"\n },\n {\n \"$ref\": \"response.data.data.result_url\"\n },\n {\n \"$ref\": \"response.data.data.url\"\n },\n {\n \"$ref\": \"response.data.data.metadata.url\"\n },\n {\n \"$ref\": \"response.video_url\"\n },\n {\n \"$ref\": \"response.videoUrl\"\n },\n {\n \"$ref\": \"response.result_url\"\n },\n {\n \"$ref\": \"response.output_url\"\n },\n {\n \"$ref\": \"response.url\"\n },\n {\n \"$ref\": \"response.metadata.url\"\n },\n {\n \"$ref\": \"response.output.url\"\n }\n ]\n },\n \"errorPaths\": [\n \"error.code\"\n ],\n \"resultEphemeral\": true\n }\n }\n ]\n }\n}\n```\n\n", "permissions": [ "generation.run", "media.read" @@ -258,33 +258,6 @@ }, "videos": { "$coalesce": [ - { - "$map": { - "from": { - "$ref": "response.data" - }, - "as": "item", - "in": { - "$coalesce": [ - { - "$ref": "item.url" - }, - { - "$ref": "item.video_url" - }, - { - "$ref": "item.videoUrl" - }, - { - "$ref": "item.output_url" - }, - { - "$ref": "item.outputUrl" - } - ] - } - } - }, { "$ref": "response.data.result_url" }, diff --git a/plugin-packages/openai-images/README.md b/plugin-packages/openai-images/README.md index 0d28d0dbe..7eca39305 100644 --- a/plugin-packages/openai-images/README.md +++ b/plugin-packages/openai-images/README.md @@ -3,5 +3,3 @@ 该目录是独立官方协议插件源码。后端从生成的 `openai-images.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。 完整接口见 [docs/interface.md](docs/interface.md)。 - -图片编辑的 JSON 请求支持公开图片 URL 和 Base64 data URL。本地角色卡参考图由后端在归属校验后读取为内嵌数据,不要求配置公开存储地址;不会因此将本地服务暴露到公网。 diff --git a/plugin-packages/openai-images/docs/interface.md b/plugin-packages/openai-images/docs/interface.md index fce6410fd..2cce8744a 100644 --- a/plugin-packages/openai-images/docs/interface.md +++ b/plugin-packages/openai-images/docs/interface.md @@ -93,7 +93,7 @@ "apiVersion": "yingce.plugin/v2", "id": "openai-images", "name": "OpenAI Images", - "version": "2.0.1", + "version": "2.0.0", "author": "OpenAI / 影策", "description": "OpenAI Images 独立请求协议插件。", "documentation": "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>", @@ -127,7 +127,7 @@ "agent" ], "baseUrl": "https://api.openai.com", - "requiresPublicMediaUrls": false, + "requiresPublicMediaUrls": true, "auth": { "type": "bearer", "field": "apiKey" diff --git a/plugin-packages/openai-images/manifest.json b/plugin-packages/openai-images/manifest.json index 74c58a1ea..16d42fe95 100644 --- a/plugin-packages/openai-images/manifest.json +++ b/plugin-packages/openai-images/manifest.json @@ -2,10 +2,10 @@ "apiVersion": "yingce.plugin/v2", "id": "openai-images", "name": "OpenAI Images", - "version": "2.0.1", + "version": "2.0.0", "author": "OpenAI / 影策", "description": "OpenAI Images 独立请求协议插件。", - "documentation": "# OpenAI Images\n\n该目录是独立官方协议插件源码。后端从生成的 `openai-images.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。\n\n完整接口见 [docs/interface.md](docs/interface.md)。\n\n图片编辑的 JSON 请求支持公开图片 URL 和 Base64 data URL。本地角色卡参考图由后端在归属校验后读取为内嵌数据,不要求配置公开存储地址;不会因此将本地服务暴露到公网。\n\n---\n\n# OpenAI Images 接口字段\n\n## 协议身份\n\n- 插件 ID:`openai-images`。\n- Provider ID:`openai-image`。\n- 能力:`image`。\n- 默认 Base URL:`https://api.openai.com`。\n- 鉴权驱动:`bearer`。\n- 创建:`POST /v1/images/generations`。\n- 生命周期:同步响应。\n\n## 配置字段\n\n| 字段 | 类型 | 必填 | 含义 |\n| --- | --- | --- | --- |\n| `apiKey` | secret | 是 | API Key |\n\n## 统一字段映射\n\n| 统一字段 | 类型 | 必填 | 上游映射 | 说明 |\n| --- | --- | --- | --- | --- |\n| `model` | string | 是 | `model` | 图片模型 ID。 |\n| `prompt` | string | 是 | `prompt` | 图片提示词。 |\n| `images` | media[] | 否 | `provider image/reference fields` | 参考图或编辑源图,role 由业务层确定。 |\n| `imageCount` | integer | 否 | `n/sample_count` | 输出数量。 |\n| `aspectRatio` | string | 否 | `size/aspect_ratio` | 比例或尺寸,语义按协议说明。 |\n| `resolution` | string | 否 | `resolution/imageSize` | 分辨率档位。 |\n| `quality` | string | 否 | `quality` | 质量档位。 |\n| `providerOptions` | object | 否 | `provider-specific fields` | 插件命名空间内的厂商扩展字段。 |\n\n## 上游请求模板逐字段清单\n\n下表由插件请求模板生成,覆盖 body、query、headers 和 multipart 文件声明中的每个字段。\n\n| 上游位置 | 值或转换表达式 |\n| --- | --- |\n| `create.method` | `\"POST\"` |\n| `create.path` | `\"/v1/images/generations\"` |\n| `create.contentType` | `\"application/json\"` |\n| `create.body.model` | `{\"$ref\":\"request.model\"}` |\n| `create.body.prompt` | `{\"$ref\":\"request.prompt\"}` |\n| `create.body.images` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$gt\":[{\"$len\":{\"$ref\":\"request.images\"}},0]},\"then\":{\"$map\":{\"from\":{\"$filter\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.images\"}},\"as\":\"media\",\"where\":{\"$ne\":[{\"$ref\":\"media.role\"},\"mask\"]}}},\"as\":\"media\",\"in\":{\"image_url\":{\"$ref\":\"media.value\"}}}},\"else\":null}}}` |\n| `create.body.mask` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$gt\":[{\"$len\":{\"$filter\":{\"from\":{\"$ref\":\"request.images\"},\"as\":\"media\",\"where\":{\"$eq\":[{\"$ref\":\"media.role\"},\"mask\"]}}}},0]},\"then\":{\"image_url\":{\"$first\":{\"$map\":{\"from\":{\"$filter\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.images\"}},\"as\":\"media\",\"where\":{\"$eq\":[{\"$ref\":\"media.role\"},\"mask\"]}}},\"as\":\"media\",\"in\":{\"$ref\":\"media.value\"}}}}},\"else\":null}}}` |\n| `create.body.n` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$gt\":[{\"$ref\":\"request.imageCount\"},0]},\"then\":{\"$ref\":\"request.imageCount\"},\"else\":1}}}` |\n| `create.body.size` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.aspectRatio\"}}},[\"\",\"auto\"]]},\"then\":null,\"else\":{\"$ref\":\"request.aspectRatio\"}}}}` |\n| `create.body.quality` | `{\"$omitEmpty\":{\"$switch\":{\"cases\":[{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"1k\"]]},\"then\":\"low\"},{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"2k\"]]},\"then\":\"medium\"},{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"4k\"]]},\"then\":\"high\"},{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"\",\"auto\"]]},\"then\":null}],\"default\":{\"$ref\":\"request.quality\"}}}}` |\n| `create.body.background` | `{\"$omitEmpty\":{\"$coalesce\":[{\"$ref\":\"request.providerOptions.openai-image.background\"},{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"request.extra.transparentBackground\"},\"true\"]},\"then\":\"transparent\",\"else\":null}}]}}` |\n| `create.body.output_format` | `{\"$omitEmpty\":{\"$coalesce\":[{\"$ref\":\"request.providerOptions.openai-image.output_format\"},\"png\"]}}` |\n| `create.body.output_compression` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.output_compression\"}}` |\n| `create.body.moderation` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.moderation\"}}` |\n| `create.body.response_format` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.response_format\"}}` |\n| `create.body.style` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.style\"}}` |\n| `create.body.user` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.user\"}}` |\n\n## Provider 扩展键\n\n- `providerOptions.openai-image.background`\n- `providerOptions.openai-image.moderation`\n- `providerOptions.openai-image.output_compression`\n- `providerOptions.openai-image.output_format`\n- `providerOptions.openai-image.response_format`\n- `providerOptions.openai-image.style`\n- `providerOptions.openai-image.user`\n\n动态模型或工作流允许使用文档声明的完整 `parameters/input/extra_body` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。\n\n## 响应映射逐字段清单\n\n| 映射位置 | 上游路径或转换表达式 |\n| --- | --- |\n| `response.status` | `\"succeeded\"` |\n| `response.images` | `{\"$map\":{\"from\":{\"$ref\":\"response.data\"},\"as\":\"item\",\"in\":{\"url\":{\"$omitEmpty\":{\"$ref\":\"item.url\"}},\"dataUrl\":{\"$if\":{\"condition\":{\"$ref\":\"item.b64_json\"},\"then\":{\"$concat\":[\"data:image/png;base64,\",{\"$ref\":\"item.b64_json\"}]},\"else\":null}}}}}` |\n| `response.usage` | `{\"$ref\":\"response.usage\"}` |\n| `response.errorPaths[0]` | `\"error.code\"` |\n| `response.messagePaths[0]` | `\"error.message\"` |\n\n## 响应与错误\n\n插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。\n\n## 兼容边界\n\n无参考图走 JSON generations;有参考图或蒙版走 JSON edits,并按官方 images 数组传入多张 image_url。quality 的 1k/2k/4k 映射为 OpenAI low/medium/high。\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"openai-images\",\n \"name\": \"OpenAI Images\",\n \"version\": \"2.0.1\",\n \"author\": \"OpenAI / 影策\",\n \"description\": \"OpenAI Images 独立请求协议插件。\",\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\": \"openai-image\",\n \"label\": \"OpenAI Images\",\n \"capabilities\": [\n \"image\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://api.openai.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\": \"/v1/images/generations\",\n \"pathTemplate\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 0\n ]\n },\n \"then\": \"/v1/images/edits\",\n \"else\": \"/v1/images/generations\"\n }\n },\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"images\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 0\n ]\n },\n \"then\": {\n \"$map\": {\n \"from\": {\n \"$filter\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"where\": {\n \"$ne\": [\n {\n \"$ref\": \"media.role\"\n },\n \"mask\"\n ]\n }\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"image_url\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"else\": null\n }\n }\n },\n \"mask\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$filter\": {\n \"from\": {\n \"$ref\": \"request.images\"\n },\n \"as\": \"media\",\n \"where\": {\n \"$eq\": [\n {\n \"$ref\": \"media.role\"\n },\n \"mask\"\n ]\n }\n }\n }\n },\n 0\n ]\n },\n \"then\": {\n \"image_url\": {\n \"$first\": {\n \"$map\": {\n \"from\": {\n \"$filter\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"where\": {\n \"$eq\": [\n {\n \"$ref\": \"media.role\"\n },\n \"mask\"\n ]\n }\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n },\n \"else\": null\n }\n }\n },\n \"n\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$ref\": \"request.imageCount\"\n },\n 0\n ]\n },\n \"then\": {\n \"$ref\": \"request.imageCount\"\n },\n \"else\": 1\n }\n }\n },\n \"size\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.aspectRatio\"\n }\n }\n },\n [\n \"\",\n \"auto\"\n ]\n ]\n },\n \"then\": null,\n \"else\": {\n \"$ref\": \"request.aspectRatio\"\n }\n }\n }\n },\n \"quality\": {\n \"$omitEmpty\": {\n \"$switch\": {\n \"cases\": [\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"1k\"\n ]\n ]\n },\n \"then\": \"low\"\n },\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"2k\"\n ]\n ]\n },\n \"then\": \"medium\"\n },\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"4k\"\n ]\n ]\n },\n \"then\": \"high\"\n },\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"\",\n \"auto\"\n ]\n ]\n },\n \"then\": null\n }\n ],\n \"default\": {\n \"$ref\": \"request.quality\"\n }\n }\n }\n },\n \"background\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.openai-image.background\"\n },\n {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"request.extra.transparentBackground\"\n },\n \"true\"\n ]\n },\n \"then\": \"transparent\",\n \"else\": null\n }\n }\n ]\n }\n },\n \"output_format\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.openai-image.output_format\"\n },\n \"png\"\n ]\n }\n },\n \"output_compression\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.output_compression\"\n }\n },\n \"moderation\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.moderation\"\n }\n },\n \"response_format\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.response_format\"\n }\n },\n \"style\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.style\"\n }\n },\n \"user\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.user\"\n }\n }\n }\n },\n \"response\": {\n \"status\": \"succeeded\",\n \"images\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"response.data\"\n },\n \"as\": \"item\",\n \"in\": {\n \"url\": {\n \"$omitEmpty\": {\n \"$ref\": \"item.url\"\n }\n },\n \"dataUrl\": {\n \"$if\": {\n \"condition\": {\n \"$ref\": \"item.b64_json\"\n },\n \"then\": {\n \"$concat\": [\n \"data:image/png;base64,\",\n {\n \"$ref\": \"item.b64_json\"\n }\n ]\n },\n \"else\": null\n }\n }\n }\n }\n },\n \"usage\": {\n \"$ref\": \"response.usage\"\n },\n \"errorPaths\": [\n \"error.code\"\n ],\n \"messagePaths\": [\n \"error.message\"\n ]\n }\n }\n ]\n }\n}\n```\n\n", + "documentation": "# OpenAI Images\n\n该目录是独立官方协议插件源码。后端从生成的 `openai-images.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。\n\n完整接口见 [docs/interface.md](docs/interface.md)。\n\n---\n\n# OpenAI Images 接口字段\n\n## 协议身份\n\n- 插件 ID:`openai-images`。\n- Provider ID:`openai-image`。\n- 能力:`image`。\n- 默认 Base URL:`https://api.openai.com`。\n- 鉴权驱动:`bearer`。\n- 创建:`POST /v1/images/generations`。\n- 生命周期:同步响应。\n\n## 配置字段\n\n| 字段 | 类型 | 必填 | 含义 |\n| --- | --- | --- | --- |\n| `apiKey` | secret | 是 | API Key |\n\n## 统一字段映射\n\n| 统一字段 | 类型 | 必填 | 上游映射 | 说明 |\n| --- | --- | --- | --- | --- |\n| `model` | string | 是 | `model` | 图片模型 ID。 |\n| `prompt` | string | 是 | `prompt` | 图片提示词。 |\n| `images` | media[] | 否 | `provider image/reference fields` | 参考图或编辑源图,role 由业务层确定。 |\n| `imageCount` | integer | 否 | `n/sample_count` | 输出数量。 |\n| `aspectRatio` | string | 否 | `size/aspect_ratio` | 比例或尺寸,语义按协议说明。 |\n| `resolution` | string | 否 | `resolution/imageSize` | 分辨率档位。 |\n| `quality` | string | 否 | `quality` | 质量档位。 |\n| `providerOptions` | object | 否 | `provider-specific fields` | 插件命名空间内的厂商扩展字段。 |\n\n## 上游请求模板逐字段清单\n\n下表由插件请求模板生成,覆盖 body、query、headers 和 multipart 文件声明中的每个字段。\n\n| 上游位置 | 值或转换表达式 |\n| --- | --- |\n| `create.method` | `\"POST\"` |\n| `create.path` | `\"/v1/images/generations\"` |\n| `create.contentType` | `\"application/json\"` |\n| `create.body.model` | `{\"$ref\":\"request.model\"}` |\n| `create.body.prompt` | `{\"$ref\":\"request.prompt\"}` |\n| `create.body.images` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$gt\":[{\"$len\":{\"$ref\":\"request.images\"}},0]},\"then\":{\"$map\":{\"from\":{\"$filter\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.images\"}},\"as\":\"media\",\"where\":{\"$ne\":[{\"$ref\":\"media.role\"},\"mask\"]}}},\"as\":\"media\",\"in\":{\"image_url\":{\"$ref\":\"media.value\"}}}},\"else\":null}}}` |\n| `create.body.mask` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$gt\":[{\"$len\":{\"$filter\":{\"from\":{\"$ref\":\"request.images\"},\"as\":\"media\",\"where\":{\"$eq\":[{\"$ref\":\"media.role\"},\"mask\"]}}}},0]},\"then\":{\"image_url\":{\"$first\":{\"$map\":{\"from\":{\"$filter\":{\"from\":{\"$sortByOrder\":{\"$ref\":\"request.images\"}},\"as\":\"media\",\"where\":{\"$eq\":[{\"$ref\":\"media.role\"},\"mask\"]}}},\"as\":\"media\",\"in\":{\"$ref\":\"media.value\"}}}}},\"else\":null}}}` |\n| `create.body.n` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$gt\":[{\"$ref\":\"request.imageCount\"},0]},\"then\":{\"$ref\":\"request.imageCount\"},\"else\":1}}}` |\n| `create.body.size` | `{\"$omitEmpty\":{\"$if\":{\"condition\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.aspectRatio\"}}},[\"\",\"auto\"]]},\"then\":null,\"else\":{\"$ref\":\"request.aspectRatio\"}}}}` |\n| `create.body.quality` | `{\"$omitEmpty\":{\"$switch\":{\"cases\":[{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"1k\"]]},\"then\":\"low\"},{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"2k\"]]},\"then\":\"medium\"},{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"4k\"]]},\"then\":\"high\"},{\"when\":{\"$in\":[{\"$lower\":{\"$trim\":{\"$ref\":\"request.quality\"}}},[\"\",\"auto\"]]},\"then\":null}],\"default\":{\"$ref\":\"request.quality\"}}}}` |\n| `create.body.background` | `{\"$omitEmpty\":{\"$coalesce\":[{\"$ref\":\"request.providerOptions.openai-image.background\"},{\"$if\":{\"condition\":{\"$eq\":[{\"$ref\":\"request.extra.transparentBackground\"},\"true\"]},\"then\":\"transparent\",\"else\":null}}]}}` |\n| `create.body.output_format` | `{\"$omitEmpty\":{\"$coalesce\":[{\"$ref\":\"request.providerOptions.openai-image.output_format\"},\"png\"]}}` |\n| `create.body.output_compression` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.output_compression\"}}` |\n| `create.body.moderation` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.moderation\"}}` |\n| `create.body.response_format` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.response_format\"}}` |\n| `create.body.style` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.style\"}}` |\n| `create.body.user` | `{\"$omitEmpty\":{\"$ref\":\"request.providerOptions.openai-image.user\"}}` |\n\n## Provider 扩展键\n\n- `providerOptions.openai-image.background`\n- `providerOptions.openai-image.moderation`\n- `providerOptions.openai-image.output_compression`\n- `providerOptions.openai-image.output_format`\n- `providerOptions.openai-image.response_format`\n- `providerOptions.openai-image.style`\n- `providerOptions.openai-image.user`\n\n动态模型或工作流允许使用文档声明的完整 `parameters/input/extra_body` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。\n\n## 响应映射逐字段清单\n\n| 映射位置 | 上游路径或转换表达式 |\n| --- | --- |\n| `response.status` | `\"succeeded\"` |\n| `response.images` | `{\"$map\":{\"from\":{\"$ref\":\"response.data\"},\"as\":\"item\",\"in\":{\"url\":{\"$omitEmpty\":{\"$ref\":\"item.url\"}},\"dataUrl\":{\"$if\":{\"condition\":{\"$ref\":\"item.b64_json\"},\"then\":{\"$concat\":[\"data:image/png;base64,\",{\"$ref\":\"item.b64_json\"}]},\"else\":null}}}}}` |\n| `response.usage` | `{\"$ref\":\"response.usage\"}` |\n| `response.errorPaths[0]` | `\"error.code\"` |\n| `response.messagePaths[0]` | `\"error.message\"` |\n\n## 响应与错误\n\n插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。\n\n## 兼容边界\n\n无参考图走 JSON generations;有参考图或蒙版走 JSON edits,并按官方 images 数组传入多张 image_url。quality 的 1k/2k/4k 映射为 OpenAI low/medium/high。\n\n\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v2\",\n \"id\": \"openai-images\",\n \"name\": \"OpenAI Images\",\n \"version\": \"2.0.0\",\n \"author\": \"OpenAI / 影策\",\n \"description\": \"OpenAI Images 独立请求协议插件。\",\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\": \"openai-image\",\n \"label\": \"OpenAI Images\",\n \"capabilities\": [\n \"image\"\n ],\n \"scopes\": [\n \"admin.system-channel\",\n \"user.custom-channel\",\n \"canvas\",\n \"creation\",\n \"agent\"\n ],\n \"baseUrl\": \"https://api.openai.com\",\n \"requiresPublicMediaUrls\": true,\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\": \"/v1/images/generations\",\n \"pathTemplate\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 0\n ]\n },\n \"then\": \"/v1/images/edits\",\n \"else\": \"/v1/images/generations\"\n }\n },\n \"contentType\": \"application/json\",\n \"body\": {\n \"model\": {\n \"$ref\": \"request.model\"\n },\n \"prompt\": {\n \"$ref\": \"request.prompt\"\n },\n \"images\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$ref\": \"request.images\"\n }\n },\n 0\n ]\n },\n \"then\": {\n \"$map\": {\n \"from\": {\n \"$filter\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"where\": {\n \"$ne\": [\n {\n \"$ref\": \"media.role\"\n },\n \"mask\"\n ]\n }\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"image_url\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n },\n \"else\": null\n }\n }\n },\n \"mask\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$len\": {\n \"$filter\": {\n \"from\": {\n \"$ref\": \"request.images\"\n },\n \"as\": \"media\",\n \"where\": {\n \"$eq\": [\n {\n \"$ref\": \"media.role\"\n },\n \"mask\"\n ]\n }\n }\n }\n },\n 0\n ]\n },\n \"then\": {\n \"image_url\": {\n \"$first\": {\n \"$map\": {\n \"from\": {\n \"$filter\": {\n \"from\": {\n \"$sortByOrder\": {\n \"$ref\": \"request.images\"\n }\n },\n \"as\": \"media\",\n \"where\": {\n \"$eq\": [\n {\n \"$ref\": \"media.role\"\n },\n \"mask\"\n ]\n }\n }\n },\n \"as\": \"media\",\n \"in\": {\n \"$ref\": \"media.value\"\n }\n }\n }\n }\n },\n \"else\": null\n }\n }\n },\n \"n\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$gt\": [\n {\n \"$ref\": \"request.imageCount\"\n },\n 0\n ]\n },\n \"then\": {\n \"$ref\": \"request.imageCount\"\n },\n \"else\": 1\n }\n }\n },\n \"size\": {\n \"$omitEmpty\": {\n \"$if\": {\n \"condition\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.aspectRatio\"\n }\n }\n },\n [\n \"\",\n \"auto\"\n ]\n ]\n },\n \"then\": null,\n \"else\": {\n \"$ref\": \"request.aspectRatio\"\n }\n }\n }\n },\n \"quality\": {\n \"$omitEmpty\": {\n \"$switch\": {\n \"cases\": [\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"1k\"\n ]\n ]\n },\n \"then\": \"low\"\n },\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"2k\"\n ]\n ]\n },\n \"then\": \"medium\"\n },\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"4k\"\n ]\n ]\n },\n \"then\": \"high\"\n },\n {\n \"when\": {\n \"$in\": [\n {\n \"$lower\": {\n \"$trim\": {\n \"$ref\": \"request.quality\"\n }\n }\n },\n [\n \"\",\n \"auto\"\n ]\n ]\n },\n \"then\": null\n }\n ],\n \"default\": {\n \"$ref\": \"request.quality\"\n }\n }\n }\n },\n \"background\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.openai-image.background\"\n },\n {\n \"$if\": {\n \"condition\": {\n \"$eq\": [\n {\n \"$ref\": \"request.extra.transparentBackground\"\n },\n \"true\"\n ]\n },\n \"then\": \"transparent\",\n \"else\": null\n }\n }\n ]\n }\n },\n \"output_format\": {\n \"$omitEmpty\": {\n \"$coalesce\": [\n {\n \"$ref\": \"request.providerOptions.openai-image.output_format\"\n },\n \"png\"\n ]\n }\n },\n \"output_compression\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.output_compression\"\n }\n },\n \"moderation\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.moderation\"\n }\n },\n \"response_format\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.response_format\"\n }\n },\n \"style\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.style\"\n }\n },\n \"user\": {\n \"$omitEmpty\": {\n \"$ref\": \"request.providerOptions.openai-image.user\"\n }\n }\n }\n },\n \"response\": {\n \"status\": \"succeeded\",\n \"images\": {\n \"$map\": {\n \"from\": {\n \"$ref\": \"response.data\"\n },\n \"as\": \"item\",\n \"in\": {\n \"url\": {\n \"$omitEmpty\": {\n \"$ref\": \"item.url\"\n }\n },\n \"dataUrl\": {\n \"$if\": {\n \"condition\": {\n \"$ref\": \"item.b64_json\"\n },\n \"then\": {\n \"$concat\": [\n \"data:image/png;base64,\",\n {\n \"$ref\": \"item.b64_json\"\n }\n ]\n },\n \"else\": null\n }\n }\n }\n }\n },\n \"usage\": {\n \"$ref\": \"response.usage\"\n },\n \"errorPaths\": [\n \"error.code\"\n ],\n \"messagePaths\": [\n \"error.message\"\n ]\n }\n }\n ]\n }\n}\n```\n\n", "permissions": [ "generation.run", "media.read" @@ -36,7 +36,7 @@ "agent" ], "baseUrl": "https://api.openai.com", - "requiresPublicMediaUrls": false, + "requiresPublicMediaUrls": true, "auth": { "type": "bearer", "field": "apiKey" diff --git a/web/src/components/assets/asset-library-picker-modal.tsx b/web/src/components/assets/asset-library-picker-modal.tsx index b30b8d527..5982f5511 100644 --- a/web/src/components/assets/asset-library-picker-modal.tsx +++ b/web/src/components/assets/asset-library-picker-modal.tsx @@ -3,10 +3,11 @@ import type { MenuProps } from "antd"; import { AppModal } from "@/components/ui/product/app-modal"; import { Check, ChevronDown, FileText, FolderOpen, HardDrive, Image as ImageIcon, LoaderCircle, Music2, Puzzle, RotateCcw, Search, Trash2, Upload, UserRound, Video } from "lucide-react"; import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; -import { useQuery } from "@tanstack/react-query"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { useUserStore } from "@/stores/use-user-store"; import { AssetMediaPreview } from "@/components/asset-media-preview"; +import { AudioPlayButton, AudioWaveArt, CharacterAssetCover } from "@/components/assets/asset-rich-cover"; import { AssetLibraryCard } from "@/components/assets/asset-library-card"; import { CachedResourceImage } from "@/components/cached-resource-image"; import { PaginationBar } from "@/components/layout/workspace-page"; @@ -49,6 +50,10 @@ export type AssetLibraryPickerFolder = { type Props = { remoteLibrary?: boolean; remoteKind?: string; + /** 远端素材库里的角色卡是否展示并可选(插入画布时会作为角色卡节点)。 */ + allowCharacters?: boolean; + /** 调用方能处理 onConfirm 的 pickedItems 时设为 true:远端分页里不在本地缓存的素材也可选。 */ + acceptRemoteItems?: boolean; /** 左侧「媒体类型」筛选项;只有一种类型或已由 remoteKind 固定时不展示该分组。 */ mediaKinds?: AssetPickerMediaKind[]; open: boolean; @@ -80,13 +85,16 @@ type Props = { }; }; onClose: () => void; - onConfirm: (ids: string[]) => Promise | void; + /** pickedItems 包含远端分页里选中的条目(它们不在调用方传入的 items 里)。 */ + onConfirm: (ids: string[], pickedItems?: AssetLibraryPickerItem[]) => Promise | void; onFolderAction?: (folderId: string) => Promise | void; }; export function AssetLibraryPickerModal({ remoteLibrary = false, remoteKind, + allowCharacters = false, + acceptRemoteItems = false, mediaKinds = DEFAULT_MEDIA_KINDS, open, items, @@ -138,15 +146,43 @@ export function AssetLibraryPickerModal({ const remoteQueryKind = remoteKind || (mediaKind === "all" ? undefined : mediaKind); const remoteQuery = useQuery({ queryKey: ["asset-picker", userId, remotePage, remotePageSize, category, remoteKeyword, remoteQueryKind], - queryFn: ({ signal }) => loadAssetLibraryPage({ page: remotePage, pageSize: remotePageSize, kind: remoteQueryKind, category: category === "all" || category === "archived" || category === remoteQueryKind ? undefined : category, status: category === "archived" ? "archived" : "active", query: remoteKeyword, signal }), + queryFn: ({ signal }) => + loadAssetLibraryPage({ + page: remotePage, + pageSize: remotePageSize, + kind: remoteQueryKind, + category: category === "all" || category === "archived" || category === remoteQueryKind ? undefined : category, + status: category === "archived" ? "archived" : "active", + query: remoteKeyword, + signal, + }), enabled: remoteEnabled && open && sessionHydrated, + // 切分类/翻页时保留上一页,避免加载间隙退回本地数据源、左侧分类被重置(点"角色"闪一下又跳回全部)。 + placeholderData: keepPreviousData, }); - const remoteItems = useMemo(() => (remoteQuery.data?.assets || []).filter((asset) => asset.kind !== "entity" && asset.kind !== "model").map((asset) => ({ - id: asset.id, title: asset.title, category: asset.category || "other", archived: asset.status === "archived", asset, - kindLabel: asset.kind === "image" ? "图片" : asset.kind === "video" ? "视频" : asset.kind === "audio" ? "音频" : "文本", searchText: (asset.tags ?? []).join(" "), - ...(items.find((item) => item.id === asset.id) || { disabledReason: "此素材不适用于当前操作" }), - })), [remoteQuery.data, items]); + // 调用方的 items 只是本地缓存,不代表可插入集合(分页结果大多不在缓存里)。能接收 pickedItems 的 + // 调用方(acceptRemoteItems)允许直接选远端条目;其余调用方保持"不在 items 里就不可选"的旧约束。 + const remoteItems = useMemo( + () => + (remoteQuery.data?.assets || []) + .filter((asset) => asset.kind !== "model" && (asset.kind !== "entity" || allowCharacters)) + .map((asset) => { + const known = items.find((item) => item.id === asset.id); + return { + id: asset.id, + title: asset.title, + category: asset.category || "other", + archived: asset.status === "archived", + kindLabel: asset.kind === "image" ? "图片" : asset.kind === "video" ? "视频" : asset.kind === "audio" ? "音频" : asset.kind === "entity" ? "角色" : "文本", + searchText: (asset.tags ?? []).join(" "), + ...(known || (acceptRemoteItems ? {} : { disabledReason: "此素材不适用于当前操作" })), + asset, + }; + }), + [acceptRemoteItems, allowCharacters, remoteQuery.data, items], + ); const uploadInputRef = useRef(null); + const pickedRemoteRef = useRef(new Map()); const initialSelectedIdsRef = useRef(initialSelectedIds); const itemsRef = useRef(items); initialSelectedIdsRef.current = initialSelectedIds; @@ -163,7 +199,17 @@ export function AssetLibraryPickerModal({ const preferLocalUnsynced = remoteReady && remoteTotal === 0 && localItems.length > 0; const remoteEntityOnlyPage = remoteReady && remoteItems.length === 0 && remoteTotal > 0; const useRemoteItems = remoteReady && !preferLocalUnsynced && !remoteEntityOnlyPage && (remoteItems.length > 0 || remoteTotal === 0); - const effectivePagination = useRemoteItems ? { current: remotePage, pageSize: remotePageSize, total: remoteTotal, onChange: (page: number, pageSize: number) => { setRemotePage(page); setRemotePageSize(pageSize); } } : pagination; + const effectivePagination = useRemoteItems + ? { + current: remotePage, + pageSize: remotePageSize, + total: remoteTotal, + onChange: (page: number, pageSize: number) => { + setRemotePage(page); + setRemotePageSize(pageSize); + }, + } + : pagination; const pluginItems = useMemo(() => allItems.filter((item) => Boolean(item.external)), [allItems]); const hasPluginSource = useMemo(() => Object.keys(categoryLabels).some((value) => value.startsWith("external:")) || pluginItems.some((item) => item.category.startsWith("external:")), [categoryLabels, pluginItems]); // 媒体类型在分类之前收窄数据源,让左侧分类计数、网格和分页始终描述同一批素材。 @@ -177,7 +223,13 @@ export function AssetLibraryPickerModal({ const mediaKindOptions = useMemo(() => (remoteKind ? [] : Array.from(new Set(mediaKinds))), [mediaKinds, remoteKind]); const sourceFolders = source === "plugin" ? folders : []; const showCategories = source === "local" || !sourceFolders.length; - const normalCategories = useMemo(() => useRemoteItems ? Object.keys(categoryLabels).filter((value) => value !== "archived" && !value.startsWith("external:")) : ["all", ...Array.from(new Set(activeSourceItems.map((item) => item.category || "other"))).filter((value) => value !== "all")], [activeSourceItems, categoryLabels, useRemoteItems]); + const normalCategories = useMemo( + () => + useRemoteItems + ? Object.keys(categoryLabels).filter((value) => value !== "archived" && !value.startsWith("external:")) + : ["all", ...Array.from(new Set(activeSourceItems.map((item) => item.category || "other"))).filter((value) => value !== "all")], + [activeSourceItems, categoryLabels, useRemoteItems], + ); const archivedCount = archivedItems.length; const isRecycleBin = category === "archived"; @@ -212,6 +264,7 @@ export function AssetLibraryPickerModal({ setSource("local"); setKeyword(""); setUploadedItems([]); + pickedRemoteRef.current.clear(); const selectableIds = new Set(itemsRef.current.filter((item) => !item.disabledReason).map((item) => item.id)); setSelected(new Set(Array.from(initialSelectedIdsRef.current || []).filter((id) => selectableIds.has(id)))); setWorking(false); @@ -221,8 +274,10 @@ export function AssetLibraryPickerModal({ useEffect(() => { if (category === "all" || category === "archived" || normalCategories.includes(category)) return; + // 远端分页还在加载时分类列表是临时的,不能据此把用户刚点的分类改回"全部"。 + if (remoteEnabled && (remoteQuery.isFetching || !remoteQuery.isSuccess)) return; setCategory("all"); - }, [normalCategories, category]); + }, [normalCategories, category, remoteEnabled, remoteQuery.isFetching, remoteQuery.isSuccess]); useEffect(() => { if (hasPluginSource || source === "local") return; @@ -244,6 +299,8 @@ export function AssetLibraryPickerModal({ const toggle = (item: AssetLibraryPickerItem) => { if (item.disabledReason || working) return; + // 远端分页条目翻页后就不在列表里了;选中时记下来,确认时仍能转成插入内容。 + if (item.asset && !allItems.some((known) => known.id === item.id)) pickedRemoteRef.current.set(item.id, item); setError(""); setSelected((current) => { if (!multiple) return current.has(item.id) ? new Set() : new Set([item.id]); @@ -259,7 +316,8 @@ export function AssetLibraryPickerModal({ setWorking(true); setError(""); try { - await onConfirm(selectedIds); + const known = new Set(allItems.map((item) => item.id)); + await onConfirm(selectedIds, [...allItems, ...[...pickedRemoteRef.current.values()].filter((item) => !known.has(item.id))]); } catch (reason) { setError(reason instanceof Error ? reason.message : "素材操作失败,请重试"); } finally { @@ -354,7 +412,12 @@ export function AssetLibraryPickerModal({ } }; - const countFor = (value: string) => (value === "all" ? activeSourceItems.length : activeSourceItems.filter((item) => item.category === value).length); + // 远端模式下列表是分页的,按当前页数会把"角色 4"之类的数字算错;未叠加类型筛选时用服务端分类计数。 + const remoteCategoryCounts = useRemoteItems && mediaKind === "all" && !remoteKind ? remoteQuery.data?.categoryCounts : undefined; + const countFor = (value: string) => { + if (remoteCategoryCounts) return value === "all" ? Object.values(remoteCategoryCounts).reduce((sum, count) => sum + count, 0) : remoteCategoryCounts[value] || 0; + return value === "all" ? activeSourceItems.length : activeSourceItems.filter((item) => item.category === value).length; + }; const sourceLabel = source === "plugin" ? "插件来源" : "本地素材"; const sourceMenuItems: MenuProps["items"] = [ { @@ -486,7 +549,14 @@ export function AssetLibraryPickerModal({
- {remoteEnabled && remoteQuery.isError && !localItems.length ?
素材读取失败服务端暂时不可用,重试不会影响本地已保存素材。
: loading || (useRemoteItems && remoteQuery.isFetching) ? ( + {remoteEnabled && remoteQuery.isError && !localItems.length ? ( +
+ + 素材读取失败 + 服务端暂时不可用,重试不会影响本地已保存素材。 + +
+ ) : loading || (useRemoteItems && remoteQuery.isFetching) ? (
正在读取素材 @@ -502,7 +572,9 @@ export function AssetLibraryPickerModal({
)}
- {effectivePagination ? : null} + {effectivePagination ? ( + + ) : null}