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 a163beb68..b21f866c2 100644 Binary files a/plugin-packages/autodl-comfyui.yingce-plugin and b/plugin-packages/autodl-comfyui.yingce-plugin differ 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 000000000..4ad3bde16 Binary files /dev/null and b/plugin-packages/doubao-streaming-tts.yingce-plugin differ 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}