feat(media): Dola-pool 视频插件 - 新增独立 Seedance 任务协议并避免 Kling 轮询路径不兼容
新增 Dola-pool 官方插件包,按其接口文档配置创建、轮询和视频下载路径,并补充显式 duration、参考图映射与协议契约测试。
This commit is contained in:
82
backend/internal/protocol/dola_pool_video_test.go
Normal file
82
backend/internal/protocol/dola_pool_video_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOfficialDolaPoolVideoProfile(t *testing.T) {
|
||||
adapter := officialPackageAdapter(t, "dola-pool.yingce-plugin", "dola-pool")
|
||||
request := GenerationRequest{
|
||||
Capability: CapabilityVideo,
|
||||
Model: "seedance-2.5",
|
||||
Prompt: "A cat chasing butterflies in a meadow",
|
||||
Duration: 30,
|
||||
AspectRatio: "720x1280",
|
||||
Images: []MediaReference{{URL: "https://assets.example/character.png", Kind: "image"}},
|
||||
}
|
||||
|
||||
create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: request})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if create.Method != "POST" || create.Path != "/v1/videos/generations" || create.ContentType != "application/json" {
|
||||
t.Fatalf("Dola-pool create = %#v", create)
|
||||
}
|
||||
body := manifestTestBody(t, create)
|
||||
if body["model"] != request.Model || body["prompt"] != request.Prompt || body["duration"] != float64(30) {
|
||||
t.Fatalf("Dola-pool create body = %#v", body)
|
||||
}
|
||||
if body["ratio"] != request.AspectRatio || body["size"] != request.AspectRatio {
|
||||
t.Fatalf("Dola-pool frame fields = %#v", body)
|
||||
}
|
||||
images, _ := body["reference_images"].([]any)
|
||||
if len(images) != 1 || images[0] != request.Images[0].URL {
|
||||
t.Fatalf("Dola-pool reference_images = %#v", images)
|
||||
}
|
||||
|
||||
created, err := adapter.ParseCreate(context.Background(), []byte(`{"id":"video-task-1","status":"queued"}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if created.TaskID != "video-task-1" || created.Status != StatusPending {
|
||||
t.Fatalf("Dola-pool create result = %#v", created)
|
||||
}
|
||||
poll, err := adapter.BuildPoll(context.Background(), PollContext{TaskID: created.TaskID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if poll.Method != "GET" || poll.Path != "/v1/videos/video-task-1" {
|
||||
t.Fatalf("Dola-pool poll = %#v", poll)
|
||||
}
|
||||
state, err := adapter.ParsePoll(context.Background(), PollContext{TaskID: created.TaskID}, []byte(`{"id":"video-task-1","status":"completed","video_url":"https://dolasd.xyz/videos/result.mp4"}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if state.Status != StatusSucceeded || state.Result == nil || len(state.Result.Videos) != 1 || state.Result.Videos[0].URL != "https://dolasd.xyz/videos/result.mp4" {
|
||||
t.Fatalf("Dola-pool poll result = %#v", state)
|
||||
}
|
||||
|
||||
resultAdapter, ok := adapter.(ResultAdapter)
|
||||
if !ok {
|
||||
t.Fatal("Dola-pool adapter does not expose binary result download")
|
||||
}
|
||||
result, err := resultAdapter.BuildResult(context.Background(), PollContext{TaskID: created.TaskID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Method != "GET" || result.Path != "/v1/videos/video-task-1/content" || result.Headers["Accept"] != "video/mp4" {
|
||||
t.Fatalf("Dola-pool result download = %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOfficialDolaPoolRequiresExplicitDuration(t *testing.T) {
|
||||
adapter := officialPackageAdapter(t, "dola-pool.yingce-plugin", "dola-pool")
|
||||
_, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
|
||||
Model: "seedance-2.5",
|
||||
Prompt: "A cat chasing butterflies in a meadow",
|
||||
}})
|
||||
if err == nil {
|
||||
t.Fatal("Dola-pool accepted a request without an explicit duration")
|
||||
}
|
||||
}
|
||||
BIN
plugin-packages/dola-pool.yingce-plugin
Normal file
BIN
plugin-packages/dola-pool.yingce-plugin
Normal file
Binary file not shown.
7
plugin-packages/dola-pool/README.md
Normal file
7
plugin-packages/dola-pool/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Dola-pool Seedance Video
|
||||
|
||||
该独立视频协议插件对接 `https://dolasd.xyz` 的 Dola-pool 视频任务 API。
|
||||
|
||||
在渠道配置中选择 `Dola-pool Seedance Video`,填写 Dola-pool API Key。任务使用 `/v1/videos/generations` 创建、`/v1/videos/{task_id}` 查询;不要使用 Kling Video 插件,Kling 的轮询 URL 与 Dola-pool 不同。
|
||||
|
||||
接口细节见 [docs/interface.md](docs/interface.md)。
|
||||
297
plugin-packages/dola-pool/docs/interface.md
Normal file
297
plugin-packages/dola-pool/docs/interface.md
Normal file
@@ -0,0 +1,297 @@
|
||||
# Dola-pool Seedance Video 接口
|
||||
|
||||
## 协议身份
|
||||
|
||||
- 插件 ID / Provider ID:`dola-pool`
|
||||
- 默认 Base URL:`https://dolasd.xyz`
|
||||
- 鉴权:`Authorization: Bearer <API key>`
|
||||
- 创建:`POST /v1/videos/generations`
|
||||
- 查询:`GET /v1/videos/{task_id}`
|
||||
- 下载:`GET /v1/videos/{task_id}/content`
|
||||
|
||||
## 创建请求
|
||||
|
||||
插件以 JSON 发送 `model`、`prompt` 和 `duration`。时长必须显式提供,不要依赖服务端默认值;只提交 `GET /v1/models` 中该模型当前开放的 `durations`。接口当前示例档位为 `seedance-2.5`、30 秒,开放档位由服务端定价配置控制。
|
||||
|
||||
`aspectRatio` 同时映射到 `ratio` 和 `size`,因此可填写比例(如 `16:9`、`9:16`)或接口接受的像素尺寸(如 `720x1280`)。参考图映射至 `reference_images`,支持公网 HTTPS URL 或 `data:image/...;base64,...`。建议优先使用公网 URL,单图不超过 15 MB、最多 30 张,格式限 JPEG、PNG、WEBP。
|
||||
|
||||
额外请求字段可通过 `providerOptions.dola-pool.body` 或 `providerOptions.dola-pool.extra_body` 传入;扩展对象与标准字段合并,扩展对象中的同名字段优先。
|
||||
|
||||
## 轮询与下载
|
||||
|
||||
创建响应中的 `id` 是后续查询使用的任务 ID。状态为 `queued`、`processing`、`completed` 或 `failed`;插件将接口状态映射为平台统一状态。建议每 5 至 10 秒查询一次,至少持续 20 分钟再判定超时。`timeout` 或 `download` 类型失败在退款后仍可能于 20 分钟内补回为成功,期间应继续查询,避免重复提交。
|
||||
|
||||
完成后优先使用查询响应的 `video_url`。也可调用 `/v1/videos/{task_id}/content` 获取 `video/mp4` 二进制。视频及对应任务记录约保留 6 小时,请及时下载转存。
|
||||
|
||||
## 404 排查
|
||||
|
||||
Dola-pool 的任务按 API Key 隔离;创建与查询必须使用同一把 Key。任务记录也会在成片保留期结束后清理。Kling Video 插件的轮询路径是 `/v1/videos/generations/{task_id}`,而 Dola-pool 要求 `/v1/videos/{task_id}`,因此两者不能互换。
|
||||
|
||||
错误响应使用 `{ "detail": "错误原因" }`。其他状态码与参数限制以 Dola-pool 接口文档及 `/v1/models` 当前返回为准。
|
||||
|
||||
<!-- YINGCE_MANIFEST_CONTRACT_START -->
|
||||
## Manifest 完整接口定义
|
||||
|
||||
以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。
|
||||
|
||||
```json
|
||||
{
|
||||
"apiVersion": "yingce.plugin/v2",
|
||||
"id": "dola-pool",
|
||||
"name": "Dola-pool Seedance Video",
|
||||
"version": "1.0.0",
|
||||
"author": "Dola-pool / 影策",
|
||||
"description": "Dola-pool Seedance 视频生成任务协议。",
|
||||
"permissions": [
|
||||
"generation.run",
|
||||
"media.read"
|
||||
],
|
||||
"configuration": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "apiKey",
|
||||
"type": "secret",
|
||||
"label": "API Key",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"contributes": {
|
||||
"providers": [
|
||||
{
|
||||
"id": "dola-pool",
|
||||
"label": "Dola-pool Seedance Video",
|
||||
"capabilities": [
|
||||
"video"
|
||||
],
|
||||
"scopes": [
|
||||
"admin.system-channel",
|
||||
"user.custom-channel",
|
||||
"canvas",
|
||||
"creation",
|
||||
"agent"
|
||||
],
|
||||
"baseUrl": "https://dolasd.xyz",
|
||||
"requiresPublicMediaUrls": true,
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"field": "apiKey"
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"mapping": "model",
|
||||
"description": "模型 ID;按 GET /v1/models 返回的模型配置填写。"
|
||||
},
|
||||
{
|
||||
"name": "prompt",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"mapping": "prompt",
|
||||
"description": "视频提示词。"
|
||||
},
|
||||
{
|
||||
"name": "duration",
|
||||
"type": "integer",
|
||||
"required": true,
|
||||
"mapping": "duration",
|
||||
"description": "必须显式传当前开放的时长档位;开放档位见 GET /v1/models。"
|
||||
},
|
||||
{
|
||||
"name": "aspectRatio",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"mapping": "ratio/size",
|
||||
"description": "画幅比例别名或受支持的像素尺寸。"
|
||||
},
|
||||
{
|
||||
"name": "images",
|
||||
"type": "media[]",
|
||||
"required": false,
|
||||
"mapping": "reference_images",
|
||||
"description": "参考图片;通过公网 URL 或 data URL 发送。"
|
||||
},
|
||||
{
|
||||
"name": "providerOptions",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"mapping": "provider-specific fields",
|
||||
"description": "Dola-pool 专属扩展字段。"
|
||||
}
|
||||
],
|
||||
"validations": [
|
||||
{
|
||||
"assert": {
|
||||
"$gt": [
|
||||
{
|
||||
"$ref": "request.duration"
|
||||
},
|
||||
0
|
||||
]
|
||||
},
|
||||
"message": "Dola-pool 视频任务必须显式指定大于 0 的 duration"
|
||||
}
|
||||
],
|
||||
"create": {
|
||||
"method": "POST",
|
||||
"path": "/v1/videos/generations",
|
||||
"contentType": "application/json",
|
||||
"body": {
|
||||
"$merge": [
|
||||
{
|
||||
"model": {
|
||||
"$ref": "request.model"
|
||||
},
|
||||
"prompt": {
|
||||
"$ref": "request.prompt"
|
||||
},
|
||||
"duration": {
|
||||
"$ref": "request.duration"
|
||||
},
|
||||
"ratio": {
|
||||
"$omitEmpty": {
|
||||
"$ref": "request.aspectRatio"
|
||||
}
|
||||
},
|
||||
"size": {
|
||||
"$omitEmpty": {
|
||||
"$ref": "request.aspectRatio"
|
||||
}
|
||||
},
|
||||
"reference_images": {
|
||||
"$omitEmpty": {
|
||||
"$map": {
|
||||
"from": {
|
||||
"$sortByOrder": {
|
||||
"$ref": "request.images"
|
||||
}
|
||||
},
|
||||
"as": "media",
|
||||
"in": {
|
||||
"$ref": "media.value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$coalesce": [
|
||||
{
|
||||
"$ref": "request.providerOptions.dola-pool.body"
|
||||
},
|
||||
{
|
||||
"$ref": "request.providerOptions.dola-pool.extra_body"
|
||||
},
|
||||
{}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"poll": {
|
||||
"method": "GET",
|
||||
"path": "/v1/videos/{{taskId}}",
|
||||
"contentType": "application/json"
|
||||
},
|
||||
"result": {
|
||||
"method": "GET",
|
||||
"path": "/v1/videos/{{taskId}}/content",
|
||||
"headers": {
|
||||
"Accept": "video/mp4"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"taskId": {
|
||||
"$coalesce": [
|
||||
{
|
||||
"$ref": "response.id"
|
||||
},
|
||||
{
|
||||
"$ref": "response.task_id"
|
||||
},
|
||||
{
|
||||
"$ref": "response.taskId"
|
||||
},
|
||||
{
|
||||
"$ref": "response.data.id"
|
||||
},
|
||||
{
|
||||
"$ref": "taskId"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$coalesce": [
|
||||
{
|
||||
"$ref": "response.status"
|
||||
},
|
||||
{
|
||||
"$ref": "response.state"
|
||||
},
|
||||
{
|
||||
"$ref": "response.data.status"
|
||||
},
|
||||
"pending"
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"$coalesce": [
|
||||
{
|
||||
"$ref": "response.error"
|
||||
},
|
||||
{
|
||||
"$ref": "response.detail"
|
||||
},
|
||||
{
|
||||
"$ref": "response.message"
|
||||
},
|
||||
{
|
||||
"$ref": "response.fail_reason"
|
||||
}
|
||||
]
|
||||
},
|
||||
"videos": {
|
||||
"$coalesce": [
|
||||
{
|
||||
"$ref": "response.video_url"
|
||||
},
|
||||
{
|
||||
"$ref": "response.content.video_url"
|
||||
},
|
||||
{
|
||||
"$ref": "response.videoUrl"
|
||||
},
|
||||
{
|
||||
"$ref": "response.result_url"
|
||||
},
|
||||
{
|
||||
"$ref": "response.url"
|
||||
},
|
||||
{
|
||||
"$ref": "response.data.video_url"
|
||||
},
|
||||
{
|
||||
"$ref": "response.output.url"
|
||||
}
|
||||
]
|
||||
},
|
||||
"errorPaths": [
|
||||
"detail",
|
||||
"error.code"
|
||||
],
|
||||
"messagePaths": [
|
||||
"error",
|
||||
"detail",
|
||||
"message"
|
||||
],
|
||||
"resultEphemeral": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"documentation": "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>"
|
||||
}
|
||||
```
|
||||
<!-- YINGCE_MANIFEST_CONTRACT_END -->
|
||||
258
plugin-packages/dola-pool/manifest.json
Normal file
258
plugin-packages/dola-pool/manifest.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user