From fa053529afd8432fbb1a4d00e62a8935117094a8 Mon Sep 17 00:00:00 2001 From: Qian <39391600+qianfree@users.noreply.github.com> Date: Sat, 18 Jul 2026 00:26:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BE=E7=89=87=E8=AF=B7=E6=B1=82=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E5=BC=82=E6=AD=A5=E5=A4=84=E7=90=86=20(#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix:未导入unicode包 * feat:图片异步转换 * feat:添加R2支持 * feat:新增文件管理 * fix:修复错误信息双写导致响应异常 * chore:渠道列表中添加快捷启用停止按钮 * fix:修复模型异常终端导致报错的问题 * improve:优化无可用渠道的日志打印,减少日志污染 * fix:在线体验出错缺少提示信息 * fix:修复日志库问题,只有请求日志保存到独立的库中 * feat:新增阿里图片模型说明文档 * fix:阿里千问图片生成 * fix:修复图片异步控制无效问题 * improve:优化文件展示,支持缩略图查看 * feat:实时监控中添加图片异步转换池状态信息 * improve:优化在线体验页面布局 * fix:修复异步任务状态显示异常问题 * feat:图片异步任务停止时等待任务完成后再退出程序 * chore:调整存储路径 --------- Co-authored-by: qian Co-authored-by: chengqian --- README.md | 36 + api/admin/admin.go | 5 + api/admin/v1/file.go | 105 ++ api/tenant/v1/models.go | 3 + docs/audit-db-schema.sql | 89 + docs/reference/goframe-conventions.md | 20 + docs/协议文档/阿里系列/图像-Z-Image.md | 94 + .../阿里系列/图像-万相-图片生成与编辑2.7.md | 1546 +++++++++++++++++ docs/协议文档/阿里系列/图像-万相-局部重绘.md | 577 ++++++ docs/协议文档/阿里系列/图像-万相-文生图V2.md | 1378 +++++++++++++++ docs/协议文档/阿里系列/图像-万相-涂鸦作画.md | 545 ++++++ docs/协议文档/阿里系列/图像-千问-图像编辑.md | 581 +++++++ docs/协议文档/阿里系列/图像-千问-图像翻译.md | 174 ++ docs/协议文档/阿里系列/图像-千问-文生图.md | 969 +++++++++++ go.mod | 2 +- internal/cmd/cmd.go | 13 +- internal/controller/admin/admin_v1_file.go | 28 + internal/handler/relay/sync_image.go | 232 +++ internal/handler/relay/sync_image_test.go | 84 + internal/handler/relay/task.go | 34 +- internal/logic/admin/audit.go | 18 +- internal/logic/admin/data_governance.go | 144 +- internal/logic/admin/file.go | 212 +++ internal/logic/admin/permission.go | 7 + internal/logic/common/audit_db.go | 11 +- internal/logic/common/settings_registry.go | 12 +- internal/logic/common/storage.go | 67 +- internal/logic/common/storage_cos.go | 18 + internal/logic/common/storage_factory.go | 2 +- internal/logic/common/storage_oss.go | 14 + internal/logic/common/storage_s3.go | 15 +- internal/logic/common/storage_test.go | 54 + internal/logic/common/tenant_model.go | 63 + internal/logic/docs/openapi.go | 47 + internal/logic/monitor/request_tracker.go | 2 + internal/logic/monitor/sync_image_pool.go | 35 + internal/logic/relay/provider.go | 59 +- internal/logic/task/async_polling.go | 71 +- internal/logic/task/async_polling_test.go | 35 + internal/logic/task/mem_writer.go | 48 + internal/logic/task/sync_image_worker.go | 762 ++++++++ internal/logic/task/sync_image_worker_test.go | 129 ++ internal/logic/tenant/audit_config.go | 4 +- internal/logic/tenant/model.go | 21 + internal/middleware/rbac.go | 7 + internal/service/admin.go | 10 + internal/service/tenant.go | 1 + relay/channel/ali/adaptor.go | 15 + relay/channel/ali/image_multimodal.go | 169 ++ relay/channel/gemini/adaptor.go | 9 +- relay/channel/gemini/response.go | 41 +- relay/channel/gemini/response_test.go | 41 + relay/channel/openai/adaptor.go | 12 +- relay/channel/openai/audio.go | 8 +- relay/channel/openai/claude_response.go | 8 +- relay/channel/openai/completion.go | 4 +- relay/channel/openai/gemini_response.go | 8 +- relay/channel/openai/rerank.go | 4 +- relay/common/provider.go | 1 + relay/constant/errors.go | 48 + relay/constant/task.go | 49 + relay/handler/chat_handler.go | 12 +- relay/handler/claude_handler.go | 12 +- relay/handler/error_writer_test.go | 67 + relay/handler/gemini_handler.go | 12 +- relay/handler/passthrough.go | 8 + relay/handler/relay_handler.go | 21 +- relay/taskchannel/ali/adaptor.go | 10 +- test/webhook_receiver/main.go | 168 ++ web/admin/src/router/admin.ts | 6 + web/admin/src/views/AdminLayout.vue | 1 + web/admin/src/views/ai/ChannelsPage.vue | 35 +- .../src/views/monitor/RealtimeMonitorPage.vue | 139 +- web/admin/src/views/system/FilesPage.vue | 544 ++++++ .../src/views/system/settings/ChannelTab.vue | 21 + .../src/views/system/settings/StorageTab.vue | 16 +- web/tenant/src/components/common/Icon.vue | 1 + web/tenant/src/views/service/DocsPage.vue | 46 + .../src/views/service/PlaygroundPage.vue | 2 + .../src/views/service/playground/AudioTab.vue | 17 +- .../src/views/service/playground/ChatTab.vue | 8 +- .../views/service/playground/EmbeddingTab.vue | 17 +- .../src/views/service/playground/ImageTab.vue | 316 +++- .../service/playground/ParamRefModal.vue | 20 +- .../views/service/playground/RerankTab.vue | 17 +- .../src/views/service/playground/VideoTab.vue | 4 +- 86 files changed, 10137 insertions(+), 183 deletions(-) create mode 100644 api/admin/v1/file.go create mode 100644 docs/audit-db-schema.sql create mode 100644 docs/协议文档/阿里系列/图像-Z-Image.md create mode 100644 docs/协议文档/阿里系列/图像-万相-图片生成与编辑2.7.md create mode 100644 docs/协议文档/阿里系列/图像-万相-局部重绘.md create mode 100644 docs/协议文档/阿里系列/图像-万相-文生图V2.md create mode 100644 docs/协议文档/阿里系列/图像-万相-涂鸦作画.md create mode 100644 docs/协议文档/阿里系列/图像-千问-图像编辑.md create mode 100644 docs/协议文档/阿里系列/图像-千问-图像翻译.md create mode 100644 docs/协议文档/阿里系列/图像-千问-文生图.md create mode 100644 internal/controller/admin/admin_v1_file.go create mode 100644 internal/handler/relay/sync_image.go create mode 100644 internal/handler/relay/sync_image_test.go create mode 100644 internal/logic/admin/file.go create mode 100644 internal/logic/common/storage_test.go create mode 100644 internal/logic/monitor/sync_image_pool.go create mode 100644 internal/logic/task/async_polling_test.go create mode 100644 internal/logic/task/mem_writer.go create mode 100644 internal/logic/task/sync_image_worker.go create mode 100644 internal/logic/task/sync_image_worker_test.go create mode 100644 relay/channel/ali/image_multimodal.go create mode 100644 relay/channel/gemini/response_test.go create mode 100644 relay/handler/error_writer_test.go create mode 100644 test/webhook_receiver/main.go create mode 100644 web/admin/src/views/system/FilesPage.vue diff --git a/README.md b/README.md index 9237146..b71e149 100644 --- a/README.md +++ b/README.md @@ -333,6 +333,42 @@ docker build -t team-api:v1.0.0 --build-arg VERSION=v1.0.0 -f manifest/docker/Do | `Dockerfile` | 多阶段构建(bun 前端 → Go 编译 → Alpine 运行) | | `docker-compose.yaml` | 服务编排(PostgreSQL + Redis + App) | +### 审计日志独立数据库(可选) + +大模型请求审计日志(`aud_request_logs`)体量大、写入频繁,可将其存到独立的 PostgreSQL 实例,与主业务库物理隔离,避免影响主库响应速度。其余审核表(操作日志、登录历史、敏感数据访问、内容过滤)数据量小,留在主库即可。 + +**1. 在独立库中建表:** + +```bash +# 连接审计数据库执行建表脚本 +psql -h host -p port -U user -d team-api-audit -f docs/audit-db-schema.sql +``` + +**2. 配置 `manifest/config/config.yaml`:** + +取消 `database.audit` 分组的注释并填写连接信息: + +```yaml +database: + audit: + type: "pgsql" + link: "pgsql:user:password@tcp(127.0.0.1:5432)/team-api-audit?sslmode=disable" + timezone: "Asia/Shanghai" + maxIdle: 10 + maxOpen: 50 +``` + +**3. 重启服务:** + +```bash +make run +``` + +**工作原理:** + +- 仅 `aud_request_logs`(大模型请求审计)走独立库,其余审计表仍在主库 +- 配置了 `database.audit.link` 时,`aud_request_logs` 自动写入独立库;未配置时一切照常写入主库 + ## 开发指南 ### 代码生成 diff --git a/api/admin/admin.go b/api/admin/admin.go index e661019..66f82e1 100644 --- a/api/admin/admin.go +++ b/api/admin/admin.go @@ -105,6 +105,11 @@ type IAdminV1 interface { FeedbackReply(ctx context.Context, req *v1.FeedbackReplyReq) (res *v1.FeedbackReplyRes, err error) FeedbackUpdateStatus(ctx context.Context, req *v1.FeedbackUpdateStatusReq) (res *v1.FeedbackUpdateStatusRes, err error) FeedbackStats(ctx context.Context, req *v1.FeedbackStatsReq) (res *v1.FeedbackStatsRes, err error) + FileList(ctx context.Context, req *v1.FileListReq) (res *v1.FileListRes, err error) + FileStats(ctx context.Context, req *v1.FileStatsReq) (res *v1.FileStatsRes, err error) + FileDownload(ctx context.Context, req *v1.FileDownloadReq) (res *v1.FileDownloadRes, err error) + FileDelete(ctx context.Context, req *v1.FileDeleteReq) (res *v1.FileDeleteRes, err error) + FileCleanup(ctx context.Context, req *v1.FileCleanupReq) (res *v1.FileCleanupRes, err error) HelpCategoryCreate(ctx context.Context, req *v1.HelpCategoryCreateReq) (res *v1.HelpCategoryCreateRes, err error) HelpCategoryUpdate(ctx context.Context, req *v1.HelpCategoryUpdateReq) (res *v1.HelpCategoryUpdateRes, err error) HelpCategoryDelete(ctx context.Context, req *v1.HelpCategoryDeleteReq) (res *v1.HelpCategoryDeleteRes, err error) diff --git a/api/admin/v1/file.go b/api/admin/v1/file.go new file mode 100644 index 0000000..cc162fd --- /dev/null +++ b/api/admin/v1/file.go @@ -0,0 +1,105 @@ +package v1 + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// === 文件存储管理(管理后台) === + +// FileListReq 文件列表(分页 + 筛选)。 +type FileListReq struct { + g.Meta `path:"/files" method:"get" mime:"json" tags:"管理后台-文件管理" summary:"文件列表"` + Page int `json:"page" in:"query" d:"1"` + PageSize int `json:"page_size" in:"query" d:"20"` + TenantId int64 `json:"tenant_id" in:"query" dc:"按租户筛选"` + UserId int64 `json:"user_id" in:"query" dc:"按上传者筛选"` + Provider string `json:"provider" in:"query" dc:"存储供应商 s3/minio/r2/oss/cos"` + Category string `json:"category" in:"query" v:"in:|image|export|other" dc:"分类:image/export/other"` + Keyword string `json:"keyword" in:"query" dc:"按原始文件名 / 存储路径模糊匹配"` + StartDate string `json:"start_date" in:"query" dc:"创建时间起 YYYY-MM-DD"` + EndDate string `json:"end_date" in:"query" dc:"创建时间止 YYYY-MM-DD"` +} + +type FileItem struct { + Id int64 `json:"id"` + TenantId int64 `json:"tenant_id"` + UserId int64 `json:"user_id"` + OriginalName string `json:"original_name"` + MimeType string `json:"mime_type"` + Size int64 `json:"size"` + StorageProvider string `json:"storage_provider"` + StoragePath string `json:"storage_path"` + Category string `json:"category"` // image | export | other + VirusScanStatus string `json:"virus_scan_status"` + CreatedAt *gtime.Time `json:"created_at"` +} + +type FileListRes struct { + List []*FileItem `json:"list"` + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} + +// FileStatsReq 存储占用统计(用于 KPI 卡片)。 +type FileStatsReq struct { + g.Meta `path:"/files/stats" method:"get" mime:"json" tags:"管理后台-文件管理" summary:"文件存储统计"` + TopN int `json:"top_n" in:"query" d:"10" v:"min:1|max:50" dc:"Top 租户数量"` +} + +type FileProviderStat struct { + Provider string `json:"provider"` + Count int64 `json:"count"` + Bytes int64 `json:"bytes"` +} + +type FileCategoryStat struct { + Category string `json:"category"` + Count int64 `json:"count"` + Bytes int64 `json:"bytes"` +} + +type FileTenantStat struct { + TenantId int64 `json:"tenant_id"` + Count int64 `json:"count"` + Bytes int64 `json:"bytes"` +} + +type FileStatsRes struct { + TotalCount int64 `json:"total_count"` + TotalBytes int64 `json:"total_bytes"` + ByProvider []FileProviderStat `json:"by_provider"` + ByCategory []FileCategoryStat `json:"by_category"` + TopTenants []FileTenantStat `json:"top_tenants"` +} + +// FileDownloadReq 生成临时预览/下载链接。variant=thumb 返回缩略图(仅图片,OSS/COS 服务端裁剪,其余供应商回退原图),空/original 返回原图。 +type FileDownloadReq struct { + g.Meta `path:"/files/{id}/download" method:"get" mime:"json" tags:"管理后台-文件管理" summary:"生成文件预览/下载链接"` + Id int64 `json:"id" in:"path" v:"required|min:1"` + Variant string `json:"variant" in:"query" v:"in:thumb,original" dc:"thumb=缩略图预览,空/original=原图"` + Width int `json:"width" in:"query" d:"400" v:"min:0|max:2048" dc:"缩略图宽度像素,仅 variant=thumb 生效"` +} + +type FileDownloadRes struct { + Url string `json:"url"` +} + +// FileDeleteReq 删除单个文件(对象 + 记录)。 +type FileDeleteReq struct { + g.Meta `path:"/files/{id}" method:"delete" mime:"json" tags:"管理后台-文件管理" summary:"删除文件"` + Id int64 `json:"id" in:"path" v:"required|min:1"` +} + +type FileDeleteRes struct{} + +// FileCleanupReq 手动触发一次保留期清理(导出 + 图片)。 +type FileCleanupReq struct { + g.Meta `path:"/files/cleanup" method:"post" mime:"json" tags:"管理后台-文件管理" summary:"手动清理过期文件"` +} + +type FileCleanupRes struct { + ExportsDeleted int `json:"exports_deleted"` + ImagesDeleted int `json:"images_deleted"` +} diff --git a/api/tenant/v1/models.go b/api/tenant/v1/models.go index 2d0e175..40b900e 100644 --- a/api/tenant/v1/models.go +++ b/api/tenant/v1/models.go @@ -41,4 +41,7 @@ type TenantAvailableModelItem struct { CacheReadPrice *float64 `json:"cache_read_price"` CacheCreationPrice *float64 `json:"cache_creation_price"` PricingTiers []PricingTierItem `json:"pricing_tiers"` + // AsyncImage 图片模型是否必须走异步端点(提交 /v1/images/generations/async 后轮询取图)。 + // 仅图片分类模型有意义;DashScope 等异步图片 provider 为 true。 + AsyncImage bool `json:"async_image"` } diff --git a/docs/audit-db-schema.sql b/docs/audit-db-schema.sql new file mode 100644 index 0000000..fc1aa4f --- /dev/null +++ b/docs/audit-db-schema.sql @@ -0,0 +1,89 @@ +-- 审计日志独立数据库建表脚本 +-- +-- 用途:将大模型请求审计日志(aud_request_logs)存储到独立的 PostgreSQL 实例, +-- 与主业务库物理隔离,避免审计写入影响主库响应速度。 +-- +-- 使用方法: +-- 1. 在 PostgreSQL 中创建独立数据库(如 team-api-audit) +-- 2. 连接该数据库执行本脚本: +-- psql -h host -p port -U user -d team-api-audit -f docs/audit-db-schema.sql +-- 3. 在 config.yaml 中配置 database.audit 连接信息 +-- 4. 重启服务 +-- +-- 注意:仅 aud_request_logs 表走独立库,其余审计表(aud_operation_logs、 +-- aud_login_history、aud_sensitive_access_logs、aud_content_filter_logs) +-- 仍留在主库,无需迁移。 + +CREATE TABLE IF NOT EXISTS aud_request_logs ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + api_key_id BIGINT, + request_id VARCHAR(64) NOT NULL, + method VARCHAR(10) NOT NULL, + path VARCHAR(500) NOT NULL, + query_params TEXT, + status_code INTEGER, + client_ip VARCHAR(45), + user_agent TEXT, + request_body TEXT, + response_body TEXT, + latency_ms INTEGER, + audit_level VARCHAR(20) DEFAULT 'full' NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, + tenant_request_body TEXT, + tenant_response_body TEXT, + tenant_audit_level VARCHAR(20) DEFAULT 'full' NOT NULL, + project_id BIGINT, + first_token_ms INTEGER, + request_headers JSONB, + response_headers JSONB, + forwarding_trace JSONB, + task_id VARCHAR(64), + task_status VARCHAR(20), + task_result TEXT, + task_upstream_headers JSONB, + task_completed_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_aud_request_logs_created_brin + ON aud_request_logs USING brin (created_at); +CREATE INDEX IF NOT EXISTS idx_aud_request_logs_tenant + ON aud_request_logs USING btree (tenant_id, created_at); +CREATE INDEX IF NOT EXISTS idx_aud_request_logs_project + ON aud_request_logs USING btree (project_id) WHERE project_id IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_aud_request_logs_task_id + ON aud_request_logs (task_id) WHERE task_id IS NOT NULL; + +COMMENT ON TABLE aud_request_logs IS '请求审计日志(记录所有 API 代理请求)'; +COMMENT ON COLUMN aud_request_logs.id IS '主键ID'; +COMMENT ON COLUMN aud_request_logs.tenant_id IS '租户ID'; +COMMENT ON COLUMN aud_request_logs.user_id IS '用户ID'; +COMMENT ON COLUMN aud_request_logs.api_key_id IS '使用的 API Key ID'; +COMMENT ON COLUMN aud_request_logs.request_id IS '请求唯一ID(关联全链路追踪)'; +COMMENT ON COLUMN aud_request_logs.method IS 'HTTP 方法(GET/POST/PUT/DELETE)'; +COMMENT ON COLUMN aud_request_logs.path IS '请求路径'; +COMMENT ON COLUMN aud_request_logs.query_params IS '查询参数(URL Query String)'; +COMMENT ON COLUMN aud_request_logs.status_code IS 'HTTP 响应状态码'; +COMMENT ON COLUMN aud_request_logs.client_ip IS '客户端 IP'; +COMMENT ON COLUMN aud_request_logs.user_agent IS '客户端 User-Agent'; +COMMENT ON COLUMN aud_request_logs.request_body IS '请求体(脱敏后存储)'; +COMMENT ON COLUMN aud_request_logs.response_body IS '响应体(截断后存储)'; +COMMENT ON COLUMN aud_request_logs.latency_ms IS '请求延迟(毫秒)'; +COMMENT ON COLUMN aud_request_logs.audit_level IS '审计级别:full(完整记录)/ full_text(全量文本)/ masked(脱敏记录)/ question_only(仅提问)/ none(不记录)'; +COMMENT ON COLUMN aud_request_logs.created_at IS '创建时间'; +COMMENT ON COLUMN aud_request_logs.updated_at IS '更新时间'; +COMMENT ON COLUMN aud_request_logs.tenant_request_body IS '租户级请求体(按租户审计级别处理)'; +COMMENT ON COLUMN aud_request_logs.tenant_response_body IS '租户级响应体(按租户审计级别处理)'; +COMMENT ON COLUMN aud_request_logs.tenant_audit_level IS '租户审计级别:full / full_text / masked / question_only / none'; +COMMENT ON COLUMN aud_request_logs.project_id IS '关联项目ID(通过API Key关联,NULL表示个人密钥无项目)'; +COMMENT ON COLUMN aud_request_logs.first_token_ms IS '首个 Token 出现的用时(毫秒),仅流式请求有值'; +COMMENT ON COLUMN aud_request_logs.request_headers IS '请求头信息(仅审计级别为 full 时记录,管理后台调试用)'; +COMMENT ON COLUMN aud_request_logs.response_headers IS '响应头信息(仅审计级别为 full 时记录,管理后台调试用)'; +COMMENT ON COLUMN aud_request_logs.forwarding_trace IS '请求转发路径追踪(仅管理员可见)'; +COMMENT ON COLUMN aud_request_logs.task_id IS '异步任务公开ID(task_xxxxx),关联 tsk_model_tasks.public_task_id'; +COMMENT ON COLUMN aud_request_logs.task_status IS '异步任务终态:SUCCESS / FAILURE'; +COMMENT ON COLUMN aud_request_logs.task_result IS '异步任务完成时上游返回的原始响应体'; +COMMENT ON COLUMN aud_request_logs.task_upstream_headers IS '异步任务完成时上游返回的响应头(仅审计级别为 full 时记录)'; +COMMENT ON COLUMN aud_request_logs.task_completed_at IS '异步任务达到终态的时间'; diff --git a/docs/reference/goframe-conventions.md b/docs/reference/goframe-conventions.md index 6e247fd..4b542b3 100644 --- a/docs/reference/goframe-conventions.md +++ b/docs/reference/goframe-conventions.md @@ -531,3 +531,23 @@ if err != nil { - 查询多行记录 → `Scan(&[]struct{})` - 查询单个标量值(`COUNT`、`SUM`、单个字段)→ `Value()` 返回 `*gvar.Var`,用 `.String()/.Int64()/.Bool()` 取值,**不要用 `Scan(&scalar)` 或 `Scan(&scalarPtr)`** +### 2026-07-16:CAS 更新用「已改写的目标状态」当 WHERE 谓词导致超时兜底网永久失效 + +**问题**:异步任务超时兜底 `handleTimedOutTasks`(`internal/logic/task/async_polling.go`)把任务标记失败时,先执行 `t.Status = "FAILURE"`,随后又把 `t.Status` 传给 `UpdateTaskCAS(ctx, t, t.Status)` 作为 CAS 的 oldStatus。`GetTimedOutTasks` 只返回非终态行(`status NOT IN ('SUCCESS','FAILURE')`),于是 CAS 生成的 SQL 是 `WHERE id=? AND status='FAILURE' SET status='FAILURE'`,对任何非终态行匹配 0 行 → `RowsAffected()==0` → 返回 CAS conflict → `continue`。结果:**超时任务从未被真正 FAILURE、预扣永不退款(资损)、active 计数永不递减**,且影响所有异步平台(sora/kling/suno/mj/ali/gemini/sync_image)。 + +**原因**:GoFrame 的「CAS 更新」惯用法 `Where("id", id).Where("status", oldStatus).Update(...)` + `RowsAffected()` 依赖 oldStatus 是**变更前**的真实值。此处在计算 oldStatus 之前就地改写了同一个 `t.Status` 字段,使谓词退化为「新值 == 旧值」,永不成立。 + +**修复方式**:把「计算 oldStatus + 改写为 FAILURE」抽成纯函数 `buildTimeoutFailure(t, now) (oldStatus string)`,在覆盖前捕获真实状态返回,供 CAS 使用;并补 DB-free 回归单测 `TestBuildTimeoutFailure_UsesOriginalStatusAsCASPredicate` 锁定「oldStatus 必须是覆盖前状态、绝不为 FAILURE」。 + +**正确做法**:任何 CAS 式条件更新,**先快照旧值再改写**,用快照做 WHERE 谓词: +```go +// 错误 — 先改写再拿改写后的值当谓词,CAS 永不匹配 +t.Status = "FAILURE" +dao.Xxx.Ctx(ctx).Where("id", t.ID).Where("status", t.Status).Update(...) // WHERE status='FAILURE' + +// 正确 — 先快照旧状态 +oldStatus := t.Status +t.Status = "FAILURE" +dao.Xxx.Ctx(ctx).Where("id", t.ID).Where("status", oldStatus).Update(...) // WHERE status='IN_PROGRESS' +``` + diff --git a/docs/协议文档/阿里系列/图像-Z-Image.md b/docs/协议文档/阿里系列/图像-Z-Image.md new file mode 100644 index 0000000..135f54d --- /dev/null +++ b/docs/协议文档/阿里系列/图像-Z-Image.md @@ -0,0 +1,94 @@ +Z-Image 是一款轻量级文生图模型,可快速生成图像,支持中英文字渲染,并灵活适配多种分辨率与宽高比例。 + +**快速入口**:在线体验([北京](https://bailian.console.aliyun.com/cn-beijing/?tab=model#/efm/model_experience_center/vision?currentTab=imageGenerate&modelId=z-image-turbo) | [新加坡](https://modelstudio.console.aliyun.com/ap-southeast-1?tab=dashboard#/efm/model_experience_center/vision?currentTab=imageGenerate&modelId=z-image-turbo)) | [技术博客](https://tongyi-mai.github.io/Z-Image-blog/) + +## **效果展示** + +| **输入提示词** | **输出图像** | +| --- | --- | +| film grain, analog film texture, soft film lighting, Kodak Portra 400 style, cinematic grainy texture, photorealistic details, subtle noise, (film grain:1.2)。采用近景特写镜头拍摄的东亚年轻女性,呈现户外雪地场景。她体型纤瘦,呈站立姿势,身体微微向右侧倾斜,头部抬起看向画面上方,姿态自然放松。她的面部是典型东亚长相,肤色白皙,脸颊带有自然的红润感,五官清秀:眼睛是深棕色,眼型偏圆,眼神略带惊讶地望向上方,眼白部分可见;眉毛是深黑色,形状自然弯长;鼻子小巧挺直,嘴唇涂有红色口红,唇瓣微张,表情带着轻微的惊讶或好奇。她的头发是深黑色长直发,发丝被风吹得略显凌乱,部分垂在脸颊两侧,头顶佩戴一顶深灰色的头盔,头盔边缘露出少量发丝。服装是蓝白拼接的厚重外套,外套材质看起来是毛绒与布料结合,显得温暖厚实,适合雪地环境。背景是被白雪覆盖的户外场景,远处可见模糊的树木轮廓,天空是明亮的浅蓝色,带有少量白云,光线是强烈的自然日光,照亮人物面部与头发,形成清晰的光影,色调以蓝、白、黑为主,整体风格清新自然。画面顶部有黑色提示框,内有“**Press esc to exit full screen**”的白色文字。镜头的近景视角放大了人物的表情与细节,营造出户外雪地的真实氛围。 | ![image](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/6915736671/p1036890.png) | + +## 模型概览 + +| **模型名称** | **模型简介** | **输出图像规格** | +| --- | --- | --- | +| z-image-turbo | 轻量模型,快速生图 | 图像分辨率:总像素在\\[512\\*512, 2048\\*2048\\]之间,推荐分辨率请参见[size参数设置](#46b942a420o4e) 图像格式:png 图像张数:固定1张 | + +**说明** + +调用前,请查阅各地域支持的模型列表。 + +## **前提条件** + +您需要[获取API Key](https://help.aliyun.com/zh/model-studio/get-api-key),并[配置API Key到环境变量](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables)。 + +**重要** + +阿里云百炼为华北2(北京)、新加坡地域推出了业务空间专属域名,**能够为推理请求提供卓越的性能和更高的稳定性**,建议迁移至新域名: + +- 华北2(北京)地域:从 `https://dashscope.aliyuncs.com` 迁移至 `https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com` + +- 新加坡地域:从 `https://dashscope-intl.aliyuncs.com` 迁移至 `https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com` + + +其中 `{WorkspaceId}` 为您的业务空间 ID,可在阿里云百炼控制台的**业务空间详情**页面查看。现有域名仍可正常使用。 + +## **HTTP同步调用** + +**北京地域**:`POST https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` + +**新加坡地域**:`POST https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` + +调用时请将`{WorkspaceId}`替换为真实的[业务空间ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +| #### 请求参数 | ## **文生图** 以下示例直接返回图片,响应速度较快。 若想开启“智能思考”能力,请设置`prompt_extend=true` 。开启后,系统将在返回图片的同时,返回优化后的提示词及其推理过程,但会增加响应时间。 ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \\ --header 'Content-Type: application/json' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --data '{ "model": "z-image-turbo", "input": { "messages": [ { "role": "user", "content": [ { "text": "film grain, analog film texture, soft film lighting, Kodak Portra 400 style, cinematic grainy texture, photorealistic details, subtle noise, (film grain:1.2)。采用近景特写镜头拍摄的东亚年轻女性,呈现户外雪地场景。她体型纤瘦,呈站立姿势,身体微微向右侧倾斜,头部抬起看向画面上方,姿态自然放松。她的面部是典型东亚长相,肤色白皙,脸颊带有自然的红润感,五官清秀:眼睛是深棕色,眼型偏圆,眼神略带惊讶地望向上方,眼白部分可见;眉毛是深黑色,形状自然弯长;鼻子小巧挺直,嘴唇涂有红色口红,唇瓣微张,表情带着轻微的惊讶或好奇。她的头发是深黑色长直发,发丝被风吹得略显凌乱,部分垂在脸颊两侧,头顶佩戴一顶深灰色的头盔,头盔边缘露出少量发丝。服装是蓝白拼接的厚重外套,外套材质看起来是毛绒与布料结合,显得温暖厚实,适合雪地环境。背景是被白雪覆盖的户外场景,远处可见模糊的树木轮廓,天空是明亮的浅蓝色,带有少量白云,光线是强烈的自然日光,照亮人物面部与头发,形成清晰的光影,色调以蓝、白、黑为主,整体风格清新自然。画面顶部有黑色提示框,内有“Press esc to exit full screen”的白色文字。镜头的近景视角放大了人物的表情与细节,营造出户外雪地的真实氛围。" } ] } ] }, "parameters": { "prompt_extend": false, "size": "1120*1440" } }' ``` | +| --- | --- | +| ##### 请求头(Headers) | +| **Content-Type** `*string*` **(必选)** 请求内容类型。此参数必须设置为`application/json`。 | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| ##### 请求体(Request Body) | +| **model** `*string*` **(必选)** 模型名称。必须为:z-image-turbo。 | +| **input** `*object*` **(必选)** 输入的基本信息。 **属性** **messages** `*array*` **(必选)** 请求内容数组。当前**仅支持单轮对话**,即传入一组role、content参数,不支持多轮对话。 **属性** **role** `*string*` **(必选)** 消息的角色。此参数必须设置为`user`。 **content** `*array*` **(必选)** 消息内容数组。必须包含且仅包含 **1 个 text 对象**。 **属性** **text** `*string*` **(必选)** 正向提示词用于描述期望生成的图像内容、风格和构图。 支持中英文,长度不超过800个字符,每个汉字、字母、数字或符号计为一个字符,超过部分会自动截断。 示例值:一只坐着的橘黄色的猫,表情愉悦,活泼可爱,逼真准确。 **注意**:仅支持传入一个text,不传或传入多个将报错。 | +| **parameters** `*object*` (可选) 图像处理参数。 **属性** **size** `*string*` (可选) 输出图像的分辨率,格式为`**宽*高**`。 - 默认值:`1024*1536`。 - 总像素范围限制:总像素在 \\[512\\*512, 2048\\*2048\\]之间。 - 推荐分辨率范围:总像素在 \\[1024\\*1024, 1536\\*1536\\]之间,出图效果更佳。 示例值:1024\\*1536。 **总像素为1024\\*1024的推荐分辨率:** - 1:1: 1024\\*1024 - 2:3: 832\\*1248 - 3:2: 1248\\*832 - 3:4: 864\\*1152 - 4:3: 1152\\*864 - 7:9: 896\\*1152 - 9:7: 1152\\*896 - 9:16: 720\\*1280 - 9:21: 576\\*1344 - 16:9: 1280\\*720 - 21:9:1344\\*576 **总像素为1280\\*1280的推荐分辨率:** - 1:1: 1280\\*1280 - 2:3: 1024\\*1536 - 3:2: 1536\\*1024 - 3:4: 1104\\*1472 - 4:3: 1472\\*1104 - 7:9: 1120\\*1440 - 9:7: 1440\\*1120 - 9:16: 864\\*1536 - 9:21: 720\\*1680 - 16:9: 1536\\*864 - 21:9: 1680\\*720 **总像素为1536\\*1536的推荐分辨率:** - 1:1:1536\\*1536 - 2:3: 1248\\*1872 - 3:2: 1872\\*1248 - 3:4: 1296\\*1728 - 4:3: 1728\\*1296 - 7:9: 1344\\*1728 - 9:7: 1728\\*1344 - 9:16: 1152\\*2048 - 9:21: 864\\*2016 - 16:9: 2048\\*1152 - 21:9: 2016\\*864 **prompt\\_extend** `*bool*` (可选) **重要** prompt\\_extend直接影响费用。设为 `true` 时价格高于 `false`,具体见参见[模型价格](https://help.aliyun.com/zh/model-studio/model-pricing#6713612f55h4l)。 是否启用智能提示词(text)改写。开启后,将使用大模型优化提示词,并输出思考过程。 - false:默认值,关闭智能改写。输出图像和原始文本提示词。 - true:开启智能改写。输出图像、优化后的文本提示词、思考过程。 **seed** `*integer*` (可选) 随机数种子,取值范围`[0,2147483647]`。 使用相同的`seed`参数值可使生成内容保持相对稳定。若不提供,算法将自动使用随机数种子。 **注意**:模型生成过程具有概率性,即使使用相同的`seed`,也不能保证每次生成结果完全一致。 | + +| #### 响应参数 | ## 任务执行成功 任务数据(如任务状态、图像URL等)仅保留24小时,超时后会被自动清除。请您务必及时保存生成的图像。 ``` { "output": { "choices": [ { "finish_reason": "stop", "message": { "content": [ { "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxx.png?Expires=xxx" }, { "text": "film grain, analog film texture, soft film lighting, Kodak Portra 400 style, cinematic grainy texture, photorealistic details, subtle noise, (film grain:1.2)。采用近景特写镜头拍摄的东亚年轻女性,呈现户外雪地场景。她体型纤瘦,呈站立姿势,身体微微向右侧倾斜,头部抬起看向画面上方,姿态自然放松。她的面部是典型东亚长相,肤色白皙,脸颊带有自然的红润感,五官清秀:眼睛是深棕色,眼型偏圆,眼神略带惊讶地望向上方,眼白部分可见;眉毛是深黑色,形状自然弯长;鼻子小巧挺直,嘴唇涂有红色口红,唇瓣微张,表情带着轻微的惊讶或好奇。她的头发是深黑色长直发,发丝被风吹得略显凌乱,部分垂在脸颊两侧,头顶佩戴一顶深灰色的头盔,头盔边缘露出少量发丝。服装是蓝白拼接的厚重外套,外套材质看起来是毛绒与布料结合,显得温暖厚实,适合雪地环境。背景是被白雪覆盖的户外场景,远处可见模糊的树木轮廓,天空是明亮的浅蓝色,带有少量白云,光线是强烈的自然日光,照亮人物面部与头发,形成清晰的光影,色调以蓝、白、黑为主,整体风格清新自然。画面顶部有黑色提示框,内有“Press esc to exit full screen”的白色文字。镜头的近景视角放大了人物的表情与细节,营造出户外雪地的真实氛围。" } ], "reasoning_content": "", "role": "assistant" } } ] }, "usage": { "height": 1440, "image_count": 1, "input_tokens": 0, "output_tokens": 0, "total_tokens": 0, "width": 1120 }, "request_id": "8a0809b4-a796-47f4-a095-394b02b62xxx" } ``` ## 任务执行异常 如果因为某种原因导致任务执行失败,将返回相关信息,可以通过code和message字段明确指示错误原因。请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "request_id": "a4d78a5f-655f-9639-8437-xxxxxx", "code": "InvalidParameter", "message": "num_images_per_prompt must be 1" } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **choices** `*array*` 模型生成的输出内容。此数组仅包含1个元素。 **属性** **finish\\_reason** `*string*` 任务停止原因,正常完成时为 `stop`。 **message** `*object*` 模型返回的消息。 **属性** **role** `*string*` 消息的角色,固定为`assistant`。 **content** `*array*` **属性** **image** `*string*` 生成图像的 URL,图像格式为PNG。**链接有效期为24小时**,请及时下载并保存图像。 **text** `*string*` - 当prompt\\_extend=false时,为输入的提示词。 - 当prompt\\_extend=true时,为改写后的提示词。 **reasoning\\_content** `*string*` 模型的思考过程,仅在prompt\\_extend=true时返回思考文本。 | +| **usage** `*object*` 输出信息统计。只对成功的结果计数。 **属性** **width** `*integer*` 生成图像的宽度(像素)。 **height** `*integer*` 生成图像的高度(像素)。 **image\\_count** `*integer*` 生成图像的数量,固定为1。 **input\\_tokens** `*integer*` 输入token数量,prompt\\_extend=false时固定为0。 **output\\_tokens** `*integer*` 输出token数量,prompt\\_extend=false时固定为0。 **output\\_tokens\\_details** `*object*` 输出 token 详情,仅当prompt\\_extend=true时返回。 **属性** **reasoning\\_tokens** `*integer*` 推理思考使用的 token 数量。 **total\\_tokens** `*integer*` 总token数量,prompt\\_extend=false时固定为0。 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | +| **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | | +| **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | | + +## **使用限制** + +- 图像`url`均只保留 24 小时,请及时下载。 + +- **内容审核**:输入的 `prompt` 和输出的图像均会经过内容安全审核,包含违规内容的请求将报错“IPInfringementSuspect”或“DataInspectionFailed”,具体参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 + + +## **计费与限流** + +- 模型免费额度和计费单价请参见[模型价格](https://help.aliyun.com/zh/model-studio/model-pricing#6713612f55h4l)。 + +- 模型限流请参见[Z-Image](https://help.aliyun.com/zh/model-studio/rate-limit#e17ad85fd8wwl)。 + +- 根据是否开启智能按成功生成的 **图像张数** 计费。模型调用失败或处理错误不产生任何费用,也不消耗[免费额度](https://help.aliyun.com/zh/model-studio/new-free-quota)。 + + +## **错误码** + +如果模型调用失败并返回报错信息,请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 + +## **常见问题** + +### **Q: 如何查看模型调用量?** + +A: 模型调用完一小时后,请在[**模型监控**(北京)](https://bailian.console.aliyun.com/cn-beijing/?tab=model#/model-telemetry)或[**模型监控**(新加坡)](https://modelstudio.console.aliyun.com/ap-southeast-1?tab=dashboard#/model-telemetry) 页面,查看模型的调用次数、成功率等指标。详情请参见[账单查询与成本管理](https://help.aliyun.com/zh/model-studio/bill-query-and-cost-management)。 + +### **Q:如何获取图像存储的访问域名白名单?** + +A: 模型生成的图像存储于阿里云OSS,API将返回一个临时的公网URL。**若需要对该下载地址进行防火墙白名单配置**,请注意:由于底层存储会根据业务情况进行动态变更,为避免过期信息影响访问,文档不提供固定的OSS域名白名单。如有安全管控需求,请联系客户经理获取最新OSS域名列表。 + +.tab-item { font-size: 13px !important; /\* 你可以根据需要调整字体大小 \*/ padding: 0px 7px !important; } /\*表格图片设置为块元素(独占一行),居中展示,鼠标放在图片上可以点击查看原图\*/ .unionContainer .markdown-body .image.break { margin: 0px; display: inline-block; vertical-align: middle } + +/\* 调整 table 宽度 \*/ .aliyun-docs-content table.medium-width { max-width: 1018px; width: 100%; } .aliyun-docs-content table.table-no-border tr td:first-child { padding-left: 0; } .aliyun-docs-content table.table-no-border tr td:last-child { padding-right: 0; } /\* 支持吸顶 \*/ div:has(.aliyun-docs-content), .aliyun-docs-content .markdown-body { overflow: visible; } .stick-top { position: sticky; top: 46px; } /\*\*代码块字体\*\*/ /\* 减少表格中的代码块 margin,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body table .help-code-block { margin: 0 !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre { font-size: 12px !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre code { font-size: 12px !important; } /\*\* API Reference 表格 \*\*/ .aliyun-docs-content table.api-reference tr td:first-child { margin: 0px; border-bottom: 1px solid #d8d8d8; } .aliyun-docs-content table.api-reference tr:last-child td:first-child { border-bottom: none; } .aliyun-docs-content table.api-reference p { color: #6e6e80; } .aliyun-docs-content table.api-reference b, i { color: #181818; } .aliyun-docs-content table.api-reference .collapse { border: none; margin-top: 4px; margin-bottom: 4px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse .expandable-title i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse.expanded .expandable-content { padding: 10px 14px 10px 14px !important; margin: 0; border: 1px solid #e9e9e9; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .collapse .expandable-title b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .tabbed-content-box { border: none; } .aliyun-docs-content table.api-reference .tabbed-content-box section { padding: 8px 0 !important; } .aliyun-docs-content table.api-reference .tabbed-content-box.mini .tab-box { /\* position: absolute; left: 40px; right: 0; \*/ } .aliyun-docs-content .margin-top-33 { margin-top: 33px !important; } .aliyun-docs-content .two-codeblocks pre { max-height: calc(50vh - 136px) !important; height: auto; } .expandable-content section { border-bottom: 1px solid #e9e9e9; padding-top: 6px; padding-bottom: 4px; } .expandable-content section:last-child { border-bottom: none; } .expandable-content section:first-child { padding-top: 0; } \ No newline at end of file diff --git a/docs/协议文档/阿里系列/图像-万相-图片生成与编辑2.7.md b/docs/协议文档/阿里系列/图像-万相-图片生成与编辑2.7.md new file mode 100644 index 0000000..271e953 --- /dev/null +++ b/docs/协议文档/阿里系列/图像-万相-图片生成与编辑2.7.md @@ -0,0 +1,1546 @@ +万相2.7图像生成与编辑模型支持文生图、文生组图、图生组图、图像编辑和多图参考生成。 + +## 模型概览 + +| **模型名称** | **模型简介** | **输出图像规格** | +| --- | --- | --- | +| wan2.7-image-pro | 万相2.7 image专业版,文生图(非组图生成)支持4K高清输出 | 图片格式:PNG。 图像分辨率和尺寸请参见[size参数](https://help.aliyun.com/zh/model-studio/wan-image-generation-api-reference#wan27-param-size-section)。 | +| wan2.7-image | 万相2.7 image,生成速度更快 | + +**说明** + +调用前,请查阅各地域支持的[模型列表与价格](https://help.aliyun.com/zh/model-studio/model-pricing#e2540d71a2utl)。 + +## 前提条件 + +您需要已[获取API Key](https://help.aliyun.com/zh/model-studio/get-api-key)并[配置API Key到环境变量](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables)。 + +**重要** + +华北2(北京)和新加坡地域拥有独立的 **API Key** 与**请求地址**,不可混用,跨地域调用将导致鉴权失败或服务报错。 + +**重要** + +阿里云百炼为华北2(北京)、新加坡地域推出了业务空间专属域名,**能够为推理请求提供卓越的性能和更高的稳定性**,建议迁移至新域名: + +- 华北2(北京)地域:从 `https://dashscope.aliyuncs.com` 迁移至 `https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com` + +- 新加坡地域:从 `https://dashscope-intl.aliyuncs.com` 迁移至 `https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com` + + +其中 `{WorkspaceId}` 为您的业务空间 ID,可在阿里云百炼控制台的**业务空间详情**页面查看。现有域名仍可正常使用。 + +## **HTTP同步调用** + +一次请求即可获得结果,流程简单,推荐大多数场景使用。 + +## **北京** + +`POST https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` + +调用时请将`{WorkspaceId}`替换为真实的[业务空间ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +## **新加坡** + +`POST https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` + +调用时请将`{WorkspaceId}`替换为真实的[业务空间ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +| #### 请求参数 | ## 文生图 > wan2.7-image-pro仅文生图场景支持4K分辨率,图像编辑和组图生成支持最高2K分辨率 ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \\ --header 'Content-Type: application/json' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --data '{ "model": "wan2.7-image-pro", "input": { "messages": [ { "role": "user", "content": [ {"text": "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵"} ] } ] }, "parameters": { "size": "2K", "n": 1, "watermark": false, "thinking_mode": true } }' ``` ## **图像编辑** ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \\ --header 'Content-Type: application/json' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --data '{ "model": "wan2.7-image-pro", "input": { "messages": [ { "role": "user", "content": [ {"image": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251229/pjeqdf/car.webp"}, {"image": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251229/xsunlm/paint.webp"}, {"text": "把图2的涂鸦喷绘在图1的汽车上"} ] } ] }, "parameters": { "size": "2K", "n": 1, "watermark": false } }' ``` ## **交互式编辑** ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \\ --header 'Content-Type: application/json' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --data '{ "model": "wan2.7-image-pro", "input": { "messages": [ { "role": "user", "content": [ {"image": "https://img.alicdn.com/imgextra/i3/O1CN0157XGE51l6iL9441yX_!!6000000004770-49-tps-1104-1472.webp"}, {"image": "https://img.alicdn.com/imgextra/i3/O1CN01SfG4J41UYn9WNt4X1_!!6000000002530-49-tps-1696-960.webp"}, {"text": "把图1的闹钟放在图2的框选的位置,保持场景和光线融合自然"} ] } ] }, "parameters": { "bbox_list": [[],[[989, 515, 1138, 681]]], "size": "2K", "n": 1, "watermark": false } }' ``` ## **组图生成** > wan2.7-image-pro组图生成仅支持最高2K分辨率。 ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \\ --header 'Content-Type: application/json' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --data '{ "model": "wan2.7-image-pro", "input": { "messages": [ { "role": "user", "content": [ {"text": "电影感组图,记录同一只流浪橘猫,特征必须前后一致。第一张:春天,橘猫穿梭在盛开的樱花树下;第二张:夏天,橘猫在老街的树荫下乘凉避暑;第三张:秋天,橘猫踩在满地的金色落叶上;第四张:冬天,橘猫在雪地上走留下足迹。"} ] } ] }, "parameters": { "enable_sequential": true, "n": 4, "size": "2K" } }' ``` | +| --- | --- | +| ##### 请求头(Headers) | +| **Content-Type** `*string*` **(必选)** 请求内容类型。此参数必须设置为`application/json`。 | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| ##### 请求体(Request Body) | +| **model** `*string*` **(必选)** 模型名称。可选值:`wan2.7-image-pro`、`wan2.7-image`。 | +| **input** `*object*` **(必选)** 输入的基本信息。 **属性** **messages** `*array*` **(必选)** 请求内容数组。当前**仅支持单轮对话**,即传入一组role、content参数,不支持多轮对话。 **属性** **role** `*string*` **(必选)** 消息的角色。此参数固定设置为`user`。 **content** `*array*` **(必选)** 消息内容数组。 **属性** **text** `*string*` 用户输入提示词。支持中英文,长度不超过5000个字符,每个汉字、字母、数字或符号计为一个字符,超过部分会自动截断。 **image** `*string*` 输入图像的URL或Base64编码字符串。 图像限制: - 图像格式:JPEG、JPG、PNG(不支持透明通道)、BMP、WEBP。 - 图像分辨率:图像的宽高范围均为\\[240, 8000\\]像素,宽高比范围\\[1:8, 8:1\\]。 - 文件大小:不超过20MB。 图像数量限制: - 可传入0-9张图片。 - 当输入多张图像时,需在`content`数组中传入多个`image`对象,并按照数组顺序定义图像顺序。 支持的输入格式: 1. 使用公网可访问URL - 支持HTTP或HTTPS协议。 - 示例值:`http://wanx.alicdn.com/material/xxx.jpeg`。 2. 传入 Base64 编码图像后的字符串 - 格式:data:{MIME\\_type};base64,{base64\\_data} - 示例:data:image/jpeg;base64,GDU7MtCZzEbTbmRZ...(仅示意,实际需传入完整字符串) - Base64 编码规范请参见[图像传入方式](https://help.aliyun.com/zh/model-studio/wan-image-edit#8db0e2215frua)。 | +| **parameters** `*object*` (可选) 模型参数配置。 **属性** **bbox\\_list** `*array[array[array[integer]]]*` (可选) 交互式编辑框选区域。 - 对应关系:列表长度必须与输入图片数量一致。若某张图片无需编辑,请在对应位置传入空列表 `[]`。 - 坐标格式:`[x1, y1, x2, y2]`(左上角 x, 左上角 y, 右下角 x, 右下角 y),使用原图绝对像素坐标,左上角坐标为(0,0)。 - 限制条件:单张图片最多支持 2 个边界框。 示例:输入 3 张图片,其中第 2 张无框选,第 1 张有两个框选: ``` [ [[0, 0, 12, 12], [25, 25, 100, 100]], # 图 1 (2个框) [], # 图 2 (无框) [[10, 10, 50, 50]] # 图 3 (1个框) ] ``` **enable\\_sequential** `*boolean*` (可选) 控制生图模式: - false:默认值。 - true :启用组图输出模式。 **size** `*string*` (可选) 关于输出图片分辨率参数,支持以下两种方式,不可混用: **模型:wan2.7-image-pro** - **方式一:指定输出图片的分辨率(推荐)** - 支持 1K、2K(默认)、4K 三种规格 - **适用范围**: - 文生图(无图片输入,非组图生成):支持1K、2K、4K。 - 其他场景:支持1K、2K。 - **各规格总像素**:1K:1024\\*1024、2K:2048\\*2048、4K:4096\\*4096 - **图像比例**: - 当有图片输入时:输出宽高比与输入图像(多图输入时为最后一张)一致,并缩放到选定分辨率。 - 当没有图片输入时:输出为正方形。 - **方式二:指定生成图像的宽高像素值** - 文生图:总像素在 \\[768\\*768, 4096\\*4096\\] 之间,宽高比范围为 \\[1:8, 8:1\\]。 - 其他场景:总像素在 \\[768\\*768, 2048\\*2048\\] 之间,宽高比范围为 \\[1:8, 8:1\\]。 **模型:wan2.7-image** - **方式一:指定输出图片的分辨率(推荐)** - 支持1K、2K(默认)两种规格,不支持4K。 - **方式二:指定生成图像的宽高像素值** - 所有场景下,总像素在 \\[768\\*768, 2048\\*2048\\] 之间,宽高比范围为 \\[1:8, 8:1\\]。 > 输出图片的像素值可能和指定像素值存在微小差异。 **n** `*integer*` (可选) **重要** n直接影响费用。费用 = 单价 × 成功生成的图片张数,请在调用前确认[模型价格](https://help.aliyun.com/zh/model-studio/model-pricing#e2540d71a2utl)。 - 关闭组图模式时,该数值代表生成图像数量,取值范围 1-4,**默认为 1**; - 开启组图模式时,该数值代表最大生成图像数量,取值范围 1-12,**默认为 12**。实际数量由模型决定且不超过 n。 **thinking\\_mode** `*boolean*` (可选) 是否开启思考模式,默认为`true`(开启)。仅在关闭组图模式且无图片输入时生效。开启时,模型将增强推理能力以提升出图质量,但会增加生成耗时。 **color\\_palette** `*array*` (可选) 自定义颜色主题,一个包含颜色(hex)和占比(ratio)的对象数组,需要包含 3 至 10 种颜色,推荐设置为 8 种。 仅当关闭组图模式(`enable_sequential=false`)时可用。 **属性** **hex** `*string*` **(必选)** 十六进制(HEX)格式的色值。 **ratio** `*string*` **(必选)** 颜色所占的百分比,需精确到小数点后两位(如`"25.00%"`)。所有 ratio 值相加**总和必须为 100.00%**。 **点击查看输入示例** ``` "color_palette": [ { "hex": "#C2D1E6", "ratio": "23.51%" }, { "hex": "#CDD8E9", "ratio": "20.13%" }, { "hex": "#B5C8DB", "ratio": "15.88%" }, { "hex": "#C0B5B4", "ratio": "13.27%" }, { "hex": "#DAE0EC", "ratio": "10.11%" }, { "hex": "#636574", "ratio": "8.93%" }, { "hex": "#CACAD2", "ratio": "5.55%" }, { "hex": "#CBD4E4", "ratio": "2.62%" } ] ``` **watermark** `*bool*` (可选) 是否添加水印标识,水印位于图片右下角,文案固定为“AI生成”。 - false:默认值,不添加水印。 - true:添加水印。 **seed** `*integer*` (可选) 随机数种子,取值范围`[0,2147483647]`。 使用相同的`seed`参数值可使生成内容保持相对稳定。若不提供,算法将自动使用随机数种子。 **注意**:模型生成过程具有概率性,即使使用相同的`seed`,也不能保证每次生成结果完全一致。 | + +| #### 响应参数 | ## 任务执行成功 任务数据(如任务状态、图像URL等)仅保留24小时,超时后会被自动清除。请您务必及时保存生成的图像。 ``` { "output": { "choices": [ { "finish_reason": "stop", "message": { "content": [ { "image": "https://dashscope-xxx.oss-xxx.aliyuncs.com/xxx.png?Expires=xxx", "type": "image" } ], "role": "assistant" } } ], "finished": true }, "usage": { "image_count": 1, "input_tokens": 10867, "output_tokens": 2, "size": "1488*704", "total_tokens": 10869 }, "request_id": "71dfc3c6-f796-9972-97e4-bc4efc4faxxx" } ``` ## 任务执行异常 如果因为某种原因导致任务执行失败,将返回相关信息,可以通过code和message字段明确指示错误原因。请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "request_id": "a4d78a5f-655f-9639-8437-xxxxxx", "code": "InvalidParameter", "message": "num_images_per_prompt must be 1" } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **choices** `*array*` 模型生成的输出内容。 **属性** **finish\\_reason** `*string*` 任务停止原因。自然停止时为`stop`。 **message** `*object*` 模型返回的消息。 **属性** **role** `*string*` 消息的角色,固定为`assistant`。 **content** `*array*` **属性** **type** `*string*` 输出的类型,固定为image。 **image** `*string*` 生成图像的 URL,图像格式为PNG。 **链接有效期为24小时**,请及时下载并保存图像。 **finished** `*boolean*` 任务是否结束。 - true:已结束。 - false:未结束。 | +| **usage** `*object*` 输出信息统计。只对成功的结果计数。 **属性** **image\\_count** `*integer*` 生成图像的张数。 **size** `*string*` 生成的图像分辨率。示例值:1376\\*768。 **input\\_tokens** `*integer*` 输入token数量(不计费)。按图片张数计费。 **output\\_tokens** `*integer*` 输出token数量(不计费)。按图片张数计费。 **total\\_tokens** `*integer*` 总token数量(不计费)。按图片张数计费。 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | +| **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | | +| **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | | + +## **HTTP异步调用** + +适用于耗时较长的任务,支持查询任务状态和结果。 + +### 步骤1:创建任务获取任务ID + +## **北京** + +`POST https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/image-generation/generation` + +调用时请将`{WorkspaceId}`替换为真实的[业务空间ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +## **新加坡** + +`POST https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/services/aigc/image-generation/generation` + +调用时请将`{WorkspaceId}`替换为真实的[业务空间ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +| #### 请求参数 | ## **文生图** > wan2.7-image-pro仅文生图场景支持4K分辨率,图像编辑和组图生成支持最高2K分辨率 ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/image-generation/generation' \\ --header 'Content-Type: application/json' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --header "X-DashScope-Async: enable" \\ --data '{ "model": "wan2.7-image-pro", "input": { "messages": [ { "role": "user", "content": [ {"text": "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵"} ] } ] }, "parameters": { "size": "2K", "n": 1, "watermark": false, "thinking_mode": true } }' ``` ## **图像编辑** ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/image-generation/generation' \\ --header 'Content-Type: application/json' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --header "X-DashScope-Async: enable" \\ --data '{ "model": "wan2.7-image-pro", "input": { "messages": [ { "role": "user", "content": [ {"image": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251229/pjeqdf/car.webp"}, {"image": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251229/xsunlm/paint.webp"}, {"text": "把图2的涂鸦喷绘在图1的汽车上"} ] } ] }, "parameters": { "size": "2K", "n": 1, "watermark": false } }' ``` ## **交互式编辑** ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/image-generation/generation' \\ --header 'Content-Type: application/json' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --header "X-DashScope-Async: enable" \\ --data '{ "model": "wan2.7-image-pro", "input": { "messages": [ { "role": "user", "content": [ {"image": "https://img.alicdn.com/imgextra/i3/O1CN0157XGE51l6iL9441yX_!!6000000004770-49-tps-1104-1472.webp"}, {"image": "https://img.alicdn.com/imgextra/i3/O1CN01SfG4J41UYn9WNt4X1_!!6000000002530-49-tps-1696-960.webp"}, {"text": "把图1的闹钟放在图2的框选的位置,保持场景和光线融合自然"} ] } ] }, "parameters": { "bbox_list": [[],[[989, 515, 1138, 681]]], "size": "2K", "n": 1, "watermark": false } }' ``` ## **组图生成** > wan2.7-image-pro组图生成仅支持最高2K分辨率。 ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/image-generation/generation' \\ --header 'Content-Type: application/json' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --header "X-DashScope-Async: enable" \\ --data '{ "model": "wan2.7-image-pro", "input": { "messages": [ { "role": "user", "content": [ {"text": "电影感组图,记录同一只流浪橘猫,特征必须前后一致。第一张:春天,橘猫穿梭在盛开的樱花树下;第二张:夏天,橘猫在老街的树荫下乘凉避暑;第三张:秋天,橘猫踩在满地的金色落叶上;第四张:冬天,橘猫在雪地上走留下足迹。"} ] } ] }, "parameters": { "enable_sequential": true, "n": 4, "size": "2K" } }' ``` | +| --- | --- | +| ##### 请求头(Headers) | +| **Content-Type** `*string*` **(必选)** 请求内容类型。此参数必须设置为`application/json`。 | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| **X-DashScope-Async** `*string*` **(必选)** 异步处理配置参数。HTTP请求只支持异步,**必须设置为**`**enable**`。 **重要** 缺少此请求头将报错:“current user api does not support synchronous calls”。 | +| ##### 请求体(Request Body) | +| **model** `*string*` **(必选)** 模型名称。可选值:`wan2.7-image-pro`、`wan2.7-image`。 | +| **input** `*object*` **(必选)** 输入的基本信息。 **属性** **messages** `*array*` **(必选)** 请求内容数组。当前**仅支持单轮对话**,即传入一组role、content参数,不支持多轮对话。 **属性** **role** `*string*` **(必选)** 消息的角色。此参数固定设置为`user`。 **content** `*array*` **(必选)** 消息内容数组。 **属性** **text** `*string*` 用户输入提示词。支持中英文,长度不超过5000个字符,每个汉字、字母、数字或符号计为一个字符,超过部分会自动截断。 **image** `*string*` 输入图像的URL或Base64编码字符串。 图像限制: - 图像格式:JPEG、JPG、PNG(不支持透明通道)、BMP、WEBP。 - 图像分辨率:图像的宽高范围均为\\[240, 8000\\]像素,宽高比范围\\[1:8, 8:1\\]。 - 文件大小:不超过20MB。 图像数量限制: - 可传入0-9张图片。 - 当输入多张图像时,需在`content`数组中传入多个`image`对象,并按照数组顺序定义图像顺序。 支持的输入格式: 1. 使用公网可访问URL - 支持HTTP或HTTPS协议。 - 示例值:`http://wanx.alicdn.com/material/xxx.jpeg`。 2. 传入 Base64 编码图像后的字符串 - 格式:data:{MIME\\_type};base64,{base64\\_data} - 示例:data:image/jpeg;base64,GDU7MtCZzEbTbmRZ...(仅示意,实际需传入完整字符串) - Base64 编码规范请参见[图像传入方式](https://help.aliyun.com/zh/model-studio/wan-image-edit#8db0e2215frua)。 | +| **parameters** `*object*` (可选) 模型参数配置。 **属性** **bbox\\_list** `*array[array[array[integer]]]*` (可选) 交互式编辑框选区域。 - 对应关系:列表长度必须与输入图片数量一致。若某张图片无需编辑,请在对应位置传入空列表 `[]`。 - 坐标格式:`[x1, y1, x2, y2]`(左上角 x, 左上角 y, 右下角 x, 右下角 y),使用原图绝对像素坐标,左上角坐标为(0,0)。 - 限制条件:单张图片最多支持 2 个边界框。 示例:输入 3 张图片,其中第 2 张无框选,第 1 张有两个框选: ``` [ [[0, 0, 12, 12], [25, 25, 100, 100]], # 图 1 (2个框) [], # 图 2 (无框) [[10, 10, 50, 50]] # 图 3 (1个框) ] ``` **enable\\_sequential** `*boolean*` (可选) 控制生图模式: - false:默认值。 - true :启用组图输出模式。 **size** `*string*` (可选) 关于输出图片分辨率参数,支持以下两种方式,不可混用: **模型:wan2.7-image-pro** - **方式一:指定输出图片的分辨率(推荐)** - 支持 1K、2K(默认)、4K 三种规格 - **适用范围**: - 文生图(无图片输入,非组图生成):支持1K、2K、4K。 - 其他场景:支持1K、2K。 - **各规格总像素**:1K:1024\\*1024、2K:2048\\*2048、4K:4096\\*4096 - **图像比例**: - 当有图片输入时:输出宽高比与输入图像(多图输入时为最后一张)一致,并缩放到选定分辨率。 - 当没有图片输入时:输出为正方形。 - **方式二:指定生成图像的宽高像素值** - 文生图:总像素在 \\[768\\*768, 4096\\*4096\\] 之间,宽高比范围为 \\[1:8, 8:1\\]。 - 其他场景:总像素在 \\[768\\*768, 2048\\*2048\\] 之间,宽高比范围为 \\[1:8, 8:1\\]。 **模型:wan2.7-image** - **方式一:指定输出图片的分辨率(推荐)** - 支持1K、2K(默认)两种规格,不支持4K。 - **方式二:指定生成图像的宽高像素值** - 所有场景下,总像素在 \\[768\\*768, 2048\\*2048\\] 之间,宽高比范围为 \\[1:8, 8:1\\]。 > 输出图片的像素值可能和指定像素值存在微小差异。 **n** `*integer*` (可选) **重要** n直接影响费用。费用 = 单价 × 成功生成的图片张数,请在调用前确认[模型价格](https://help.aliyun.com/zh/model-studio/model-pricing#e2540d71a2utl)。 - 关闭组图模式时,该数值代表生成图像数量,取值范围 1-4,**默认为 1**; - 开启组图模式时,该数值代表最大生成图像数量,取值范围 1-12,**默认为 12**。实际数量由模型决定且不超过 n。 **thinking\\_mode** `*boolean*` (可选) 是否开启思考模式,默认为`true`(开启)。仅在关闭组图模式且无图片输入时生效。开启时,模型将增强推理能力以提升出图质量,但会增加生成耗时。 **color\\_palette** `*array*` (可选) 自定义颜色主题,一个包含颜色(hex)和占比(ratio)的对象数组,需要包含 3 至 10 种颜色,推荐设置为 8 种。 仅当关闭组图模式(`enable_sequential=false`)时可用。 **属性** **hex** `*string*` **(必选)** 十六进制(HEX)格式的色值。 **ratio** `*string*` **(必选)** 颜色所占的百分比,需精确到小数点后两位(如`"25.00%"`)。所有 ratio 值相加**总和必须为 100.00%**。 **点击查看输入示例** ``` "color_palette": [ { "hex": "#C2D1E6", "ratio": "23.51%" }, { "hex": "#CDD8E9", "ratio": "20.13%" }, { "hex": "#B5C8DB", "ratio": "15.88%" }, { "hex": "#C0B5B4", "ratio": "13.27%" }, { "hex": "#DAE0EC", "ratio": "10.11%" }, { "hex": "#636574", "ratio": "8.93%" }, { "hex": "#CACAD2", "ratio": "5.55%" }, { "hex": "#CBD4E4", "ratio": "2.62%" } ] ``` **watermark** `*bool*` (可选) 是否添加水印标识,水印位于图片右下角,文案固定为“AI生成”。 - false:默认值,不添加水印。 - true:添加水印。 **seed** `*integer*` (可选) 随机数种子,取值范围`[0,2147483647]`。 使用相同的`seed`参数值可使生成内容保持相对稳定。若不提供,算法将自动使用随机数种子。 **注意**:模型生成过程具有概率性,即使使用相同的`seed`,也不能保证每次生成结果完全一致。 | + +| #### 响应参数 | #### 成功响应 请保存 task\\_id,用于查询任务状态与结果。 ``` { "output": { "task_status": "PENDING", "task_id": "0385dc79-5ff8-4d82-bcb6-xxxxxx" }, "request_id": "4909100c-7b5a-9f92-bfe5-xxxxxx" } ``` #### 异常响应 创建任务失败,请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "code": "InvalidApiKey", "message": "No API-key provided.", "request_id": "7438d53d-6eb8-4596-8835-xxxxxx" } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | +| **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | | +| **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | | + +### 步骤2:根据任务ID查询结果 + +## **北京** + +`GET https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/tasks/{task_id}` + +## **新加坡** + +`GET https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/tasks/{task_id}` + +| #### 请求参数 | ## 查询任务结果 将`{task_id}`完整替换为上一步接口返回的`task_id`的值。`task_id`查询有效期为24小时。 ``` curl -X GET https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/tasks/{task_id} \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" ``` | +| --- | --- | +| ##### **请求头(Headers)** | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| ##### **URL路径参数(Path parameters)** | +| **task\\_id** `*string*`**(必选)** 任务ID。 | + +| #### 响应参数 | ## 任务执行成功 任务数据(如任务状态、图像URL等)仅保留24小时,超时后会被自动清除。请您务必及时保存生成的图像。 ``` { "request_id": "810fa5f5-334c-91f3-aaa4-ed89cf0caxxx", "output": { "task_id": "a81ee7cb-014c-473d-b842-76e98311cxxx", "task_status": "SUCCEEDED", "submit_time": "2026-03-26 17:16:01.663", "scheduled_time": "2026-03-26 17:16:01.716", "end_time": "2026-03-26 17:16:22.961", "finished": true, "choices": [ { "finish_reason": "stop", "message": { "role": "assistant", "content": [ { "image": "https://dashscope-xxx.oss-xxx.aliyuncs.com/xxx.png?Expires=xxx", "type": "image" } ] } } ] }, "usage": { "size": "2976*1408", "total_tokens": 11017, "image_count": 1, "output_tokens": 2, "input_tokens": 11015 } } ``` ## 任务执行异常 如果因为某种原因导致任务执行失败,将返回相关信息,可以通过code和message字段明确指示错误原因。请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "request_id": "a4d78a5f-655f-9639-8437-xxxxxx", "code": "InvalidParameter", "message": "num_images_per_prompt must be 1" } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 **轮询过程中的状态流转:** - PENDING(排队中) → RUNNING(处理中)→ SUCCEEDED(成功)/ FAILED(失败)。 - 初次查询状态通常为 PENDING(排队中)或 RUNNING(处理中)。 - 当状态变为 SUCCEEDED 时,响应中将包含生成的图像URL。 - 若状态为 FAILED,请检查错误信息并重试。 **submit\\_time** `*string*` 任务提交时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **scheduled\\_time** `*string*` 任务执行时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **end\\_time** `*string*` 任务完成时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **finished** `*boolean*` 任务是否结束。 - true:已结束。 - false:未结束。 **choices** `*array*` 模型生成的输出内容。 **属性** **finish\\_reason** `*string*` 任务停止原因,自然停止时为`stop`。 **message** `*object*` 模型返回的消息。 **属性** **role** `*string*` 消息的角色,固定为`assistant`。 **content** `*array*` **属性** **type** `*string*` 输出的类型,枚举值为text、image。 **text** `*string*` 生成的文字。 **image** `*string*` 生成图像的 URL,图像格式为PNG。 **链接有效期为24小时**,请及时下载并保存图像。 | +| **usage** `*object*` 输出信息统计。只对成功的结果计数。 **属性** **image\\_count** `*integer*` 生成图像的张数。 **size** `*string*` 生成的图像分辨率。示例值:1376\\*768。 **input\\_tokens** `*integer*` 输入token数量(不计费)。按图片张数计费。 **output\\_tokens** `*integer*` 输出token数量(不计费)。按图片张数计费。 **total\\_tokens** `*integer*` 总token数量(不计费)。按图片张数计费。 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | +| **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | +| **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | + +## **Python SDK调用** + +SDK 参数命名与HTTP接口基本一致。 + +任务可能耗时较长,SDK 已封装HTTP异步调用流程,同时支持同步和异步调用。 + +> 具体耗时受限于排队任务数和服务执行情况,请在获取结果时耐心等待。 + +**重要** + +请确保 DashScope Python SDK版本**不低于 1.25.15**,再运行以下代码。更新请参考[安装SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。 + +各地域的`base_url`和API Key 不通用,以下示例以北京地域为例进行调用: + +### 华北2(北京) + +`https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1` + +调用时请将`WorkspaceId`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +### 新加坡 + +`https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1` + +调用时请将`WorkspaceId`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +### **图像编辑** + +## **同步调用** + +##### **请求示例** + +``` +import os +import base64 +import mimetypes +import urllib.request +import dashscope +from dashscope.aigc.image_generation import ImageGeneration +from dashscope.api_entities.dashscope_response import Message + +# 以下为华北2(北京)地域的URL,调用时请将{WorkspaceId}替换为真实的业务空间ID,各地域的URL不同。 +dashscope.base_http_api_url = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1" + +# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx" +# 各地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +api_key = os.getenv("DASHSCOPE_API_KEY") + +# --- Base64编码函数 --- +# base64编码格式为 data:{MIME_type};base64,{base64_data} +def encode_file(file_path): + mime_type, _ = mimetypes.guess_type(file_path) + if not mime_type or not mime_type.startswith("image/"): + raise ValueError("不支持或无法识别的图像格式") + with open(file_path, "rb") as image_file: + encoded_string = base64.b64encode(image_file.read()).decode("utf-8") + return f"data:{mime_type};base64,{encoded_string}" + +""" +图像输入方式说明: +以下提供了三种图片输入方式,三选一即可 +1. 使用公网URL - 适合已有公开可访问的图片 +2. 使用本地文件 - 适合本地开发测试 +3. 使用Base64编码 - 适合私有图片或需要加密传输的场景 +""" +# 【方式一】使用公网图片 URL +image_1 = "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251229/pjeqdf/car.webp" +image_2 = "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251229/xsunlm/paint.webp" + +# 【方式二】使用本地文件(支持绝对路径和相对路径) +# image_1 = "file:///path/to/your/car.png" +# image_2 = "file:///path/to/your/paint.png" + +# 【方式三】使用Base64编码的图片 +# image_1 = encode_file("/path/to/your/car.png") +# image_2 = encode_file("/path/to/your/paint.png") + +message = Message( + role="user", + content=[ + {"text": "把图2的涂鸦喷绘在图1的汽车上"}, + {"image": image_1}, + {"image": image_2}, + ], +) +print("----sync call, please wait a moment----") +rsp = ImageGeneration.call( + model="wan2.7-image-pro", + api_key=api_key, + messages=[message], + watermark=False, + n=1, + size="2K", # wan2.7-image-pro仅文生图场景支持4K分辨率,图像编辑和组图生成支持最高2K分辨率 +) + +# 提取结果图片URL并保存到本地 +if rsp.status_code == 200: + for i, choice in enumerate(rsp.output.choices): + for j, content in enumerate(choice["message"]["content"]): + if content.get("type") == "image": + image_url = content["image"] + file_name = f"output_{i}_{j}.png" + # 结果URL有效期为24小时,请及时下载 + urllib.request.urlretrieve(image_url, file_name) + print(f"Image saved to {file_name}") +else: + print(f"Failed: status_code={rsp.status_code}, message={rsp.message}") +``` + +##### 响应示例 + +> URL 有效期24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "81d868c6-6ce1-92d8-a90d-d2ee71xxxxxx", + "code": "", + "message": "", + "output": { + "text": null, + "finish_reason": null, + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + } + ] + } + } + ], + "audio": null, + "finished": true + }, + "usage": { + "input_tokens": 18790, + "output_tokens": 2, + "characters": 0, + "image_count": 1, + "size": "2985*1405", + "total_tokens": 18792 + } +} +``` + +## **异步调用** + +##### **请求示例** + +``` +import os +import base64 +import mimetypes +import urllib.request +import dashscope +from dashscope.aigc.image_generation import ImageGeneration +from dashscope.api_entities.dashscope_response import Message +from http import HTTPStatus + +# 以下为华北2(北京)地域的URL,调用时请将{WorkspaceId}替换为真实的业务空间ID,各地域的URL不同。 +dashscope.base_http_api_url = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1" + +# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx" +# 各地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +api_key = os.getenv("DASHSCOPE_API_KEY") + +# --- Base64编码函数 --- +# base64编码格式为 data:{MIME_type};base64,{base64_data} +def encode_file(file_path): + mime_type, _ = mimetypes.guess_type(file_path) + if not mime_type or not mime_type.startswith("image/"): + raise ValueError("不支持或无法识别的图像格式") + with open(file_path, "rb") as image_file: + encoded_string = base64.b64encode(image_file.read()).decode("utf-8") + return f"data:{mime_type};base64,{encoded_string}" + +""" +图像输入方式说明: +以下提供了三种图片输入方式,三选一即可 +1. 使用公网URL - 适合已有公开可访问的图片 +2. 使用本地文件 - 适合本地开发测试 +3. 使用Base64编码 - 适合私有图片或需要加密传输的场景 +""" +# 【方式一】使用公网图片 URL +image_1 = "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251229/pjeqdf/car.webp" +image_2 = "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251229/xsunlm/paint.webp" + +# 【方式二】使用本地文件(支持绝对路径和相对路径) +# image_1 = "file:///path/to/your/car.png" +# image_2 = "file:///path/to/your/paint.png" + +# 【方式三】使用Base64编码的图片 +# image_1 = encode_file("/path/to/your/car.png") +# image_2 = encode_file("/path/to/your/paint.png") + +# 创建异步任务 +def create_async_task(): + print("Creating async task...") + message = Message( + role="user", + content=[ + {"text": "把图2的涂鸦喷绘在图1的汽车上"}, + {"image": image_1}, + {"image": image_2}, + ], + ) + response = ImageGeneration.async_call( + model="wan2.7-image-pro", + api_key=api_key, + messages=[message], + watermark=False, + n=1, + size="2K", # wan2.7-image-pro仅文生图场景支持4K分辨率,图像编辑和组图生成支持最高2K分辨率 + ) + + if response.status_code == 200: + print("Task created successfully:", response) + return response + else: + raise Exception(f"Failed to create task: {response.code} - {response.message}") + +# 等待任务完成 +def wait_for_completion(task_response): + print("Waiting for task completion...") + status = ImageGeneration.wait(task=task_response, api_key=api_key) + + if status.output.task_status == "SUCCEEDED": + print("Task succeeded!") + # 提取结果图片URL并保存到本地 + for i, choice in enumerate(status.output.choices): + for j, content in enumerate(choice["message"]["content"]): + if content.get("type") == "image": + image_url = content["image"] + file_name = f"output_{i}_{j}.png" + # 结果URL有效期为24小时,请及时下载 + urllib.request.urlretrieve(image_url, file_name) + print(f"Image saved to {file_name}") + else: + raise Exception(f"Task failed with status: {status.output.task_status}") + +# 获取异步任务信息 +def fetch_task_status(task): + print("Fetching task status...") + status = ImageGeneration.fetch(task=task, api_key=api_key) + + if status.status_code == HTTPStatus.OK: + print("Task status:", status.output.task_status) + print("Response details:", status) + else: + print(f"Failed to fetch status: {status.code} - {status.message}") + +# 取消异步任务 +def cancel_task(task): + print("Canceling task...") + response = ImageGeneration.cancel(task=task, api_key=api_key) + + if response.status_code == HTTPStatus.OK: + print("Task canceled successfully:", response.output.task_status) + else: + print(f"Failed to cancel task: {response.code} - {response.message}") + +# 主执行流程 +if __name__ == "__main__": + task = create_async_task() + wait_for_completion(task) +``` + +##### 响应示例 + +1、创建任务的响应示例 + +``` +{ + "status_code": 200, + "request_id": "4fb3050f-de57-4a24-84ff-e37ee5xxxxxx", + "code": "", + "message": "", + "output": { + "text": null, + "finish_reason": null, + "choices": null, + "audio": null, + "task_id": "127ec645-118f-4884-955d-0eba8dxxxxxx", + "task_status": "PENDING" + }, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "characters": 0 + } +} +``` + +2、查询任务结果的响应示例 + +> URL 有效期24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "3b99aae5-d26f-9059-8dd0-ee9ca4804xxx", + "code": null, + "message": "", + "output": { + "text": null, + "finish_reason": null, + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + } + ] + } + } + ], + "audio": null, + "task_id": "127ec645-118f-4884-955d-0eba8dxxxxxx", + "task_status": "SUCCEEDED", + "submit_time": "2026-03-31 22:58:47.646", + "scheduled_time": "2026-03-31 22:58:47.683", + "end_time": "2026-03-31 22:58:59.642", + "finished": true + }, + "usage": { + "input_tokens": 18711, + "output_tokens": 2, + "characters": 0, + "size": "2985*1405", + "total_tokens": 18713, + "image_count": 1 + } +} +``` + +### **组图生成** + +## **同步调用** + +##### **请求示例** + +``` +import os +import base64 +import mimetypes +import urllib.request +import dashscope +from dashscope.aigc.image_generation import ImageGeneration +from dashscope.api_entities.dashscope_response import Message + +# 以下为华北2(北京)地域的URL,调用时请将{WorkspaceId}替换为真实的业务空间ID,各地域的URL不同。 +dashscope.base_http_api_url = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1" + +# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx" +# 各地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +api_key = os.getenv("DASHSCOPE_API_KEY") + +# --- Base64编码函数 --- +# base64编码格式为 data:{MIME_type};base64,{base64_data} +def encode_file(file_path): + mime_type, _ = mimetypes.guess_type(file_path) + if not mime_type or not mime_type.startswith("image/"): + raise ValueError("不支持或无法识别的图像格式") + with open(file_path, "rb") as image_file: + encoded_string = base64.b64encode(image_file.read()).decode("utf-8") + return f"data:{mime_type};base64,{encoded_string}" + +""" +图像输入方式说明(图生组图场景): +以下提供了三种图片输入方式,三选一即可 +1. 使用公网URL - 适合已有公开可访问的图片 +2. 使用本地文件 - 适合本地开发测试 +3. 使用Base64编码 - 适合私有图片或需要加密传输的场景 +""" +# 【方式一】使用公网图片 URL +# image_1 = "https://img.alicdn.com/imgextra/i4/O1CN01IM44WN23dq5uY1yla_!!6000000007279-49-tps-1024-1024.webp" + +# 【方式二】使用本地文件(支持绝对路径和相对路径) +# image_1 = "file:///path/to/your/image.png" + +# 【方式三】使用Base64编码的图片 +# image_1 = encode_file("/path/to/your/image.png") + +message = Message( + role="user", + content=[ + { + "text": "电影感组图,记录同一只流浪橘猫,特征必须前后一致。第一张:春天,橘猫穿梭在盛开的樱花树下;第二张:夏天,橘猫在老街的树荫下乘凉避暑;第三张:秋天,橘猫踩在满地的金色落叶上;第四张:冬天,橘猫在雪地上走留下足迹。" + } + # 图生组图场景:取消以下注释并注释掉上方纯文本 + # {"text": "参考图片风格生成四季组图"}, + # {"image": image_1} + ], +) + +print("----sync call, please wait a moment----") +rsp = ImageGeneration.call( + model="wan2.7-image-pro", + api_key=api_key, + messages=[message], + enable_sequential=True, + n=4, + size="2K", # wan2.7-image-pro仅文生图场景支持4K分辨率,图像编辑和组图生成支持最高2K分辨率 +) + +# 提取结果图片URL并保存到本地 +if rsp.status_code == 200: + for i, choice in enumerate(rsp.output.choices): + for j, content in enumerate(choice["message"]["content"]): + if content.get("type") == "image": + image_url = content["image"] + file_name = f"output_{i}_{j}.png" + # 结果URL有效期为24小时,请及时下载 + urllib.request.urlretrieve(image_url, file_name) + print(f"Image saved to {file_name}") +else: + print(f"Failed: status_code={rsp.status_code}, message={rsp.message}") +``` + +##### 响应示例 + +> URL 有效期24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "56e318fd-ed60-99e8-8ca1-cdef25ca4xxx", + "code": "", + "message": "", + "output": { + "text": null, + "finish_reason": null, + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + }, + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + }, + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + }, + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + } + ] + } + } + ], + "audio": null, + "finished": true + }, + "usage": { + "input_tokens": 720, + "output_tokens": 11, + "characters": 0, + "image_count": 4, + "size": "2048*2048", + "total_tokens": 731 + } +} +``` + +## **异步调用** + +##### **请求示例** + +``` +import os +import base64 +import mimetypes +import urllib.request +import dashscope +from dashscope.aigc.image_generation import ImageGeneration +from dashscope.api_entities.dashscope_response import Message + +# 以下为华北2(北京)地域的URL,调用时请将{WorkspaceId}替换为真实的业务空间ID,各地域的URL不同。 +dashscope.base_http_api_url = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1" + +# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx" +# 各地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +api_key = os.getenv("DASHSCOPE_API_KEY") + +# --- Base64编码函数 --- +# base64编码格式为 data:{MIME_type};base64,{base64_data} +def encode_file(file_path): + mime_type, _ = mimetypes.guess_type(file_path) + if not mime_type or not mime_type.startswith("image/"): + raise ValueError("不支持或无法识别的图像格式") + with open(file_path, "rb") as image_file: + encoded_string = base64.b64encode(image_file.read()).decode("utf-8") + return f"data:{mime_type};base64,{encoded_string}" + +""" +图像输入方式说明(图生组图场景): +以下提供了三种图片输入方式,三选一即可 +1. 使用公网URL - 适合已有公开可访问的图片 +2. 使用本地文件 - 适合本地开发测试 +3. 使用Base64编码 - 适合私有图片或需要加密传输的场景 +""" +# 【方式一】使用公网图片 URL +# image_1 = "https://img.alicdn.com/imgextra/i4/O1CN01IM44WN23dq5uY1yla_!!6000000007279-49-tps-1024-1024.webp" + +# 【方式二】使用本地文件(支持绝对路径和相对路径) +# image_1 = "file:///path/to/your/image.png" + +# 【方式三】使用Base64编码的图片 +# image_1 = encode_file("/path/to/your/image.png") + +def main(): + message = Message( + role="user", + content=[ + { + "text": "电影感组图,记录同一只流浪橘猫,特征必须前后一致。第一张:春天,橘猫穿梭在盛开的樱花树下;第二张:夏天,橘猫在老街的树荫下乘凉避暑;第三张:秋天,橘猫踩在满地的金色落叶上;第四张:冬天,橘猫在雪地上走留下足迹。" + } + # 图生组图场景:取消以下注释并注释掉上方纯文本 + # {"text": "参考图片风格生成四季组图"}, + # {"image": image_1} + ], + ) + + # 提交异步任务 + print("提交异步任务...") + response = ImageGeneration.async_call( + model="wan2.7-image-pro", + api_key=api_key, + messages=[message], + enable_sequential=True, + n=4, + size="2K", # wan2.7-image-pro仅文生图场景支持4K分辨率,图像编辑和组图生成支持最高2K分辨率 + ) + + if response.status_code == 200: + print(f"任务提交成功,任务ID: {response.output.task_id}") + + # 等待任务完成 + status = ImageGeneration.wait(task=response, api_key=api_key) + + if status.output.task_status == "SUCCEEDED": + print("任务完成!") + # 提取结果图片URL并保存到本地 + for i, choice in enumerate(status.output.choices): + for j, content in enumerate(choice["message"]["content"]): + if content.get("type") == "image": + image_url = content["image"] + file_name = f"output_{i}_{j}.png" + # 结果URL有效期为24小时,请及时下载 + urllib.request.urlretrieve(image_url, file_name) + print(f"Image saved to {file_name}") + else: + print(f"任务失败,状态: {status.output.task_status}") + else: + print(f"任务创建失败: {response.code} - {response.message}") + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"错误: {e}") +``` + +##### 响应示例 + +1、创建任务的响应示例 + +``` +{ + "status_code": 200, + "request_id": "4fb3050f-de57-4a24-84ff-e37ee5xxxxxx", + "code": "", + "message": "", + "output": { + "text": null, + "finish_reason": null, + "choices": null, + "audio": null, + "task_id": "77093787-a217-4c29-9cd4-ca7b5ac86xxx", + "task_status": "PENDING" + }, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "characters": 0 + } +} +``` + +2、查询任务结果的响应示例 + +> URL 有效期24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "56e318fd-ed60-99e8-8ca1-cdef25ca4xxx", + "code": "", + "message": "", + "output": { + "text": null, + "finish_reason": null, + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + }, + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + }, + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + }, + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + } + ] + } + } + ], + "audio": null, + "task_id": "77093787-a217-4c29-9cd4-ca7b5ac86xxx", + "task_status": "SUCCEEDED", + "submit_time": "2026-03-31 23:04:46.166", + "scheduled_time": "2026-03-31 23:04:46.208", + "end_time": "2026-03-31 23:05:11.664", + "finished": true + }, + "usage": { + "input_tokens": 720, + "output_tokens": 11, + "characters": 0, + "size": "2048*2048", + "total_tokens": 731, + "image_count": 4 + } +} +``` + +## **Java SDK调用** + +SDK 参数命名与HTTP接口基本一致。 + +任务可能耗时较长,SDK 已封装HTTP异步调用流程,同时支持同步和异步调用。 + +**重要** + +请确保 DashScope Java SDK版本不低于 `2.22.13`,否则可能不支持本文所用的部分参数。 + +### 华北2(北京) + +`https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1` + +调用时请将`WorkspaceId`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +### 新加坡 + +`https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1` + +调用时请将`WorkspaceId`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +### **图像编辑** + +## **同步调用** + +##### 请求示例 + +``` +import com.alibaba.dashscope.aigc.imagegeneration.*; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.exception.UploadFileException; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * wan2.7-image-pro 图像编辑 - 同步调用示例 + */ +public class Main { + + static { + // 以下为华北2(北京)地域的URL,调用时请将{WorkspaceId}替换为真实的业务空间ID,各地域的URL不同。 + Constants.baseHttpApiUrl = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"; + } + + // 若没有配置环境变量,请用百炼API Key将下行替换为:apiKey="sk-xxx" + // 各地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + static String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + // --- Base64编码函数 --- + // base64编码格式为 data:{MIME_type};base64,{base64_data} + public static String encodeFile(String filePath) throws IOException { + byte[] fileContent = Files.readAllBytes(Paths.get(filePath)); + String base64String = Base64.getEncoder().encodeToString(fileContent); + String mimeType = Files.probeContentType(Paths.get(filePath)); + return "data:" + mimeType + ";base64," + base64String; + } + + public static void basicCall() throws ApiException, NoApiKeyException, UploadFileException, IOException { + /* + * 图像输入方式说明: + * 以下提供了三种图片输入方式,三选一即可 + * 1. 使用公网URL - 适合已有公开可访问的图片 + * 2. 使用本地文件 - 适合本地开发测试 + * 3. 使用Base64编码 - 适合私有图片或需要加密传输的场景 + */ + // 【方式一】使用公网图片 URL + String image1 = "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251229/pjeqdf/car.webp"; + String image2 = "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251229/xsunlm/paint.webp"; + + // 【方式二】使用本地文件(支持绝对路径和相对路径) + // 格式要求:file:// + 文件路径 + // String image1 = "file:///path/to/your/car.png"; + // String image2 = "file:///path/to/your/paint.png"; + + // 【方式三】使用Base64编码的图片 + // String image1 = encodeFile("/path/to/your/car.png"); + // String image2 = encodeFile("/path/to/your/paint.png"); + + // 构建多图输入消息 + ImageGenerationMessage message = ImageGenerationMessage.builder() + .role("user") + .content(Arrays.asList( + // 支持多图输入,可以提供多张参考图片 + Collections.singletonMap("text", "把图2的涂鸦喷绘在图1的汽车上"), + Collections.singletonMap("image", image1), + Collections.singletonMap("image", image2) + )).build(); + + ImageGenerationParam param = ImageGenerationParam.builder() + .apiKey(apiKey) + .model("wan2.7-image-pro") + .messages(Collections.singletonList(message)) + .n(1) + .size("2K") // wan2.7-image-pro仅文生图场景支持4K分辨率,图像编辑和组图生成支持最高2K分辨率 + .build(); + + ImageGeneration imageGeneration = new ImageGeneration(); + ImageGenerationResult result = null; + try { + System.out.println("---sync call for image editing, please wait a moment----"); + result = imageGeneration.call(param); + } catch (ApiException | NoApiKeyException | UploadFileException e) { + throw new RuntimeException(e.getMessage()); + } + // 提取结果图片URL并保存到本地 + for (int i = 0; i < result.getOutput().getChoices().size(); i++) { + List> contents = result.getOutput().getChoices().get(i) + .getMessage().getContent(); + for (int j = 0; j < contents.size(); j++) { + if ("image".equals(contents.get(j).get("type"))) { + String imageUrl = (String) contents.get(j).get("image"); + String fileName = "output_" + i + "_" + j + ".png"; + // 结果URL有效期为24小时,请及时下载 + try (InputStream in = new URL(imageUrl).openStream()) { + Files.copy(in, Paths.get(fileName), StandardCopyOption.REPLACE_EXISTING); + } + System.out.println("Image saved to " + fileName); + } + } + } + } + + public static void main(String[] args) throws ApiException, NoApiKeyException, UploadFileException, IOException { + basicCall(); + } +} +``` + +##### 响应示例 + +> URL 有效期24小时,请及时下载并保存图像。 + +``` +{ + "requestId": "1bf6173a-e8de-9f75-94d3-5e618f875xxx", + "usage": { + "input_tokens": 18790, + "output_tokens": 2, + "total_tokens": 18792, + "image_count": 1, + "size": "2985*1405" + }, + "output": { + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + } + ] + } + } + ], + "finished": true + }, + "status_code": 200, + "code": "", + "message": "" +} +``` + +## **异步调用** + +##### 请求示例 + +``` +import com.alibaba.dashscope.aigc.imagegeneration.*; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.exception.UploadFileException; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * wan2.7-image-pro 图像编辑 - 异步调用示例 + */ +public class Main { + + static { + // 以下为华北2(北京)地域的URL,调用时请将{WorkspaceId}替换为真实的业务空间ID,各地域的URL不同。 + Constants.baseHttpApiUrl = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"; + } + + // 若没有配置环境变量,请用百炼API Key将下行替换为:apiKey="sk-xxx" + // 各地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + static String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + // --- Base64编码函数 --- + // base64编码格式为 data:{MIME_type};base64,{base64_data} + public static String encodeFile(String filePath) throws IOException { + byte[] fileContent = Files.readAllBytes(Paths.get(filePath)); + String base64String = Base64.getEncoder().encodeToString(fileContent); + String mimeType = Files.probeContentType(Paths.get(filePath)); + return "data:" + mimeType + ";base64," + base64String; + } + + public static void asyncCall() throws ApiException, NoApiKeyException, UploadFileException, IOException { + /* + * 图像输入方式说明: + * 以下提供了三种图片输入方式,三选一即可 + * 1. 使用公网URL - 适合已有公开可访问的图片 + * 2. 使用本地文件 - 适合本地开发测试 + * 3. 使用Base64编码 - 适合私有图片或需要加密传输的场景 + */ + // 【方式一】使用公网图片 URL + String image1 = "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251229/pjeqdf/car.webp"; + String image2 = "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251229/xsunlm/paint.webp"; + + // 【方式二】使用本地文件(支持绝对路径和相对路径) + // 格式要求:file:// + 文件路径 + // String image1 = "file:///path/to/your/car.png"; + // String image2 = "file:///path/to/your/paint.png"; + + // 【方式三】使用Base64编码的图片 + // String image1 = encodeFile("/path/to/your/car.png"); + // String image2 = encodeFile("/path/to/your/paint.png"); + + // 构建多图输入消息 + ImageGenerationMessage message = ImageGenerationMessage.builder() + .role("user") + .content(Arrays.asList( + // 支持多图输入,可以提供多张参考图片 + Collections.singletonMap("text", "把图2的涂鸦喷绘在图1的汽车上"), + Collections.singletonMap("image", image1), + Collections.singletonMap("image", image2) + )).build(); + + ImageGenerationParam param = ImageGenerationParam.builder() + .apiKey(apiKey) + .model("wan2.7-image-pro") + .n(1) + .size("2K") // wan2.7-image-pro仅文生图场景支持4K分辨率,图像编辑和组图生成支持最高2K分辨率 + .messages(Arrays.asList(message)) + .build(); + + ImageGeneration imageGeneration = new ImageGeneration(); + ImageGenerationResult result = null; + try { + System.out.println("---async call for image editing, creating task----"); + result = imageGeneration.asyncCall(param); + } catch (ApiException | NoApiKeyException | UploadFileException e) { + throw new RuntimeException(e.getMessage()); + } + System.out.println("任务创建结果:"); + System.out.println(JsonUtils.toJson(result)); + + String taskId = result.getOutput().getTaskId(); + // 等待任务完成 + waitTask(taskId); + } + + public static void waitTask(String taskId) throws ApiException, NoApiKeyException, IOException { + ImageGeneration imageGeneration = new ImageGeneration(); + System.out.println("\n---waiting for task completion----"); + ImageGenerationResult result = imageGeneration.wait(taskId, apiKey); + // 提取结果图片URL并保存到本地 + for (int i = 0; i < result.getOutput().getChoices().size(); i++) { + List> contents = result.getOutput().getChoices().get(i) + .getMessage().getContent(); + for (int j = 0; j < contents.size(); j++) { + if ("image".equals(contents.get(j).get("type"))) { + String imageUrl = (String) contents.get(j).get("image"); + String fileName = "output_" + i + "_" + j + ".png"; + // 结果URL有效期为24小时,请及时下载 + try (InputStream in = new URL(imageUrl).openStream()) { + Files.copy(in, Paths.get(fileName), StandardCopyOption.REPLACE_EXISTING); + } + System.out.println("Image saved to " + fileName); + } + } + } + } + + public static void main(String[] args) throws ApiException, NoApiKeyException, UploadFileException, IOException { + asyncCall(); + } +} +``` + +##### 响应示例 + +1、创建任务的响应示例 + +``` +{ + "requestId": "ccf4b2f4-bf30-9e13-9461-3a28c6a7bxxx", + "output": { + "task_id": "8811b4a4-00ac-4aa2-a2fd-017d3b90cxxx", + "task_status": "PENDING" + }, + "status_code": 200, + "code": "", + "message": "" +} +``` + +2、查询任务结果的响应示例 + +> URL 有效期24小时,请及时下载并保存图像。 + +``` +{ + "requestId": "60a08540-f1c1-9e76-8cd3-d5949db8cxxx", + "usage": { + "input_tokens": 18711, + "output_tokens": 2, + "total_tokens": 18713, + "image_count": 1, + "size": "2985*1405" + }, + "output": { + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + } + ] + } + } + ], + "task_id": "8811b4a4-00ac-4aa2-a2fd-017d3b90cxxx", + "task_status": "SUCCEEDED", + "finished": true, + "submit_time": "2026-03-31 19:57:58.840", + "scheduled_time": "2026-03-31 19:57:58.877", + "end_time": "2026-03-31 19:58:11.563" + }, + "status_code": 200, + "code": "", + "message": "" +} +``` + +### **组图生成** + +## **同步调用** + +##### 请求示例 + +``` +import com.alibaba.dashscope.aigc.imagegeneration.*; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.exception.UploadFileException; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * wan2.7-image-pro 组图生成 - 同步调用示例(北京地域) + */ +public class Main { + + static { + // 以下为华北2(北京)地域的URL,调用时请将{WorkspaceId}替换为真实的业务空间ID,各地域的URL不同。 + Constants.baseHttpApiUrl = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"; + } + + // 若没有配置环境变量,请用百炼API Key将下行替换为:apiKey="sk-xxx" + // 各地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + static String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + // --- Base64编码函数 --- + // base64编码格式为 data:{MIME_type};base64,{base64_data} + public static String encodeFile(String filePath) throws IOException { + byte[] fileContent = Files.readAllBytes(Paths.get(filePath)); + String base64String = Base64.getEncoder().encodeToString(fileContent); + String mimeType = Files.probeContentType(Paths.get(filePath)); + return "data:" + mimeType + ";base64," + base64String; + } + + public static void basicCall() throws ApiException, NoApiKeyException, UploadFileException, IOException { + /* + * 图像输入方式说明(图生组图场景): + * 以下提供了三种图片输入方式,三选一即可 + * 1. 使用公网URL - 适合已有公开可访问的图片 + * 2. 使用本地文件 - 适合本地开发测试 + * 3. 使用Base64编码 - 适合私有图片或需要加密传输的场景 + */ + // 【方式一】使用公网图片 URL + // String image1 = "https://img.alicdn.com/imgextra/i4/O1CN01IM44WN23dq5uY1yla_!!6000000007279-49-tps-1024-1024.webp"; + + // 【方式二】使用本地文件(支持绝对路径和相对路径) + // 格式要求:file:// + 文件路径 + // String image1 = "file:///path/to/your/image.png"; + + // 【方式三】使用Base64编码的图片 + // String image1 = encodeFile("/path/to/your/image.png"); + + // 构建文本输入消息(支持文生组图以及图生组图,此处以文生组图为例) + ImageGenerationMessage message = ImageGenerationMessage.builder() + .role("user") + .content(Collections.singletonList( + Collections.singletonMap("text", "电影感组图,记录同一只流浪橘猫,特征必须前后一致。第一张:春天,橘猫穿梭在盛开的樱花树下;第二张:夏天,橘猫在老街的树荫下乘凉避暑;第三张:秋天,橘猫踩在满地的金色落叶上;第四张:冬天,橘猫在雪地上走留下足迹。") + )).build(); + // 图生组图场景:取消以下注释并注释掉上方纯文本构建 + // ImageGenerationMessage message = ImageGenerationMessage.builder() + // .role("user") + // .content(Arrays.asList( + // Collections.singletonMap("text", "参考图片风格生成四季组图"), + // Collections.singletonMap("image", image1) + // )).build(); + + ImageGenerationParam param = ImageGenerationParam.builder() + .apiKey(apiKey) + .model("wan2.7-image-pro") + .messages(Collections.singletonList(message)) + .enableSequential(true) + .n(4) + .size("2K") // wan2.7-image-pro仅文生图场景支持4K分辨率,图像编辑和组图生成支持最高2K分辨率 + .build(); + + ImageGeneration imageGeneration = new ImageGeneration(); + ImageGenerationResult result = null; + try { + System.out.println("----sync call, please wait a moment----"); + result = imageGeneration.call(param); + } catch (ApiException | NoApiKeyException | UploadFileException e) { + throw new RuntimeException(e.getMessage()); + } + // 提取结果图片URL并保存到本地 + for (int i = 0; i < result.getOutput().getChoices().size(); i++) { + List> contents = result.getOutput().getChoices().get(i) + .getMessage().getContent(); + for (int j = 0; j < contents.size(); j++) { + if ("image".equals(contents.get(j).get("type"))) { + String imageUrl = (String) contents.get(j).get("image"); + String fileName = "output_" + i + "_" + j + ".png"; + // 结果URL有效期为24小时,请及时下载 + try (InputStream in = new URL(imageUrl).openStream()) { + Files.copy(in, Paths.get(fileName), StandardCopyOption.REPLACE_EXISTING); + } + System.out.println("Image saved to " + fileName); + } + } + } + } + + public static void main(String[] args) throws ApiException, NoApiKeyException, UploadFileException, IOException { + basicCall(); + } +} +``` + +##### 响应示例 + +> URL 有效期24小时,请及时下载并保存图像。 + +``` +{ + "requestId": "4678c314-b37a-91c9-a2ae-2d3cd54bbxxx", + "usage": { + "input_tokens": 720, + "output_tokens": 11, + "total_tokens": 731, + "image_count": 4, + "size": "2048*2048" + }, + "output": { + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + }, + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + }, + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + }, + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + } + ] + } + } + ], + "finished": true + }, + "status_code": 200, + "code": "", + "message": "" +} +``` + +## **异步调用** + +##### 请求示例 + +``` +import com.alibaba.dashscope.aigc.imagegeneration.*; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.exception.UploadFileException; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * wan2.7-image-pro 组图生成 - 异步调用示例(北京地域) + */ +public class Main { + + static { + // 以下为华北2(北京)地域的URL,调用时请将{WorkspaceId}替换为真实的业务空间ID,各地域的URL不同。 + Constants.baseHttpApiUrl = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"; + } + + // 若没有配置环境变量,请用百炼API Key将下行替换为:apiKey="sk-xxx" + // 各地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + static String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + // --- Base64编码函数 --- + // base64编码格式为 data:{MIME_type};base64,{base64_data} + public static String encodeFile(String filePath) throws IOException { + byte[] fileContent = Files.readAllBytes(Paths.get(filePath)); + String base64String = Base64.getEncoder().encodeToString(fileContent); + String mimeType = Files.probeContentType(Paths.get(filePath)); + return "data:" + mimeType + ";base64," + base64String; + } + + public static ImageGenerationResult waitTask(String taskId) + throws ApiException, NoApiKeyException { + ImageGeneration imageGeneration = new ImageGeneration(); + return imageGeneration.wait(taskId, apiKey); + } + + public static void asyncCall() throws ApiException, NoApiKeyException, UploadFileException, IOException { + /* + * 图像输入方式说明(图生组图场景): + * 以下提供了三种图片输入方式,三选一即可 + * 1. 使用公网URL - 适合已有公开可访问的图片 + * 2. 使用本地文件 - 适合本地开发测试 + * 3. 使用Base64编码 - 适合私有图片或需要加密传输的场景 + */ + // 【方式一】使用公网图片 URL + // String image1 = "https://img.alicdn.com/imgextra/i4/O1CN01IM44WN23dq5uY1yla_!!6000000007279-49-tps-1024-1024.webp"; + + // 【方式二】使用本地文件(支持绝对路径和相对路径) + // 格式要求:file:// + 文件路径 + // String image1 = "file:///path/to/your/image.png"; + + // 【方式三】使用Base64编码的图片 + // String image1 = encodeFile("/path/to/your/image.png"); + + // 构建文本输入消息(支持文生组图以及图生组图,此处以文生组图为例) + ImageGenerationMessage message = ImageGenerationMessage.builder() + .role("user") + .content(Collections.singletonList( + Collections.singletonMap("text", "电影感组图,记录同一只流浪橘猫,特征必须前后一致。第一张:春天,橘猫穿梭在盛开的樱花树下;第二张:夏天,橘猫在老街的树荫下乘凉避暑;第三张:秋天,橘猫踩在满地的金色落叶上;第四张:冬天,橘猫在雪地上走留下足迹。") + )).build(); + // 图生组图场景:取消以下注释并注释掉上方纯文本构建 + // ImageGenerationMessage message = ImageGenerationMessage.builder() + // .role("user") + // .content(Arrays.asList( + // Collections.singletonMap("text", "参考图片风格生成四季组图"), + // Collections.singletonMap("image", image1) + // )).build(); + + ImageGenerationParam param = ImageGenerationParam.builder() + .apiKey(apiKey) + .model("wan2.7-image-pro") + .messages(Collections.singletonList(message)) + .enableSequential(true) + .n(4) + .size("2K") // wan2.7-image-pro仅文生图场景支持4K分辨率,图像编辑和组图生成支持最高2K分辨率 + .build(); + + ImageGeneration imageGeneration = new ImageGeneration(); + ImageGenerationResult taskResult = null; + try { + System.out.println("----async call, creating task----"); + taskResult = imageGeneration.asyncCall(param); + } catch (ApiException | NoApiKeyException | UploadFileException e) { + throw new RuntimeException(e.getMessage()); + } + System.out.println("Task created: " + JsonUtils.toJson(taskResult)); + + // 等待任务完成 + String taskId = taskResult.getOutput().getTaskId(); + ImageGenerationResult result = waitTask(taskId); + // 提取结果图片URL并保存到本地 + for (int i = 0; i < result.getOutput().getChoices().size(); i++) { + List> contents = result.getOutput().getChoices().get(i) + .getMessage().getContent(); + for (int j = 0; j < contents.size(); j++) { + if ("image".equals(contents.get(j).get("type"))) { + String imageUrl = (String) contents.get(j).get("image"); + String fileName = "output_" + i + "_" + j + ".png"; + // 结果URL有效期为24小时,请及时下载 + try (InputStream in = new URL(imageUrl).openStream()) { + Files.copy(in, Paths.get(fileName), StandardCopyOption.REPLACE_EXISTING); + } + System.out.println("Image saved to " + fileName); + } + } + } + } + + public static void main(String[] args) throws ApiException, NoApiKeyException, UploadFileException, IOException { + asyncCall(); + } +} +``` + +##### 响应示例 + +1、创建任务的响应示例 + +``` +{ + "requestId": "7d026dc1-e8c9-9caa-84ac-e82e2da97xxx", + "output": { + "task_id": "2de18c56-c151-4b80-8105-1d164733exxx", + "task_status": "PENDING" + }, + "status_code": 200, + "code": "", + "message": "" +} +``` + +2、查询任务结果的响应示例 + +> URL 有效期24小时,请及时下载并保存图像。 + +``` +{ + "requestId": "daea7295-4ce0-928a-9a11-4d2bea058xxx", + "usage": { + "input_tokens": 720, + "output_tokens": 11, + "total_tokens": 731, + "image_count": 4, + "size": "2048*2048" + }, + "output": { + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + }, + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + }, + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + }, + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + } + ] + } + } + ], + "task_id": "2de18c56-c151-4b80-8105-1d164733exxx", + "task_status": "SUCCEEDED", + "finished": true, + "submit_time": "2026-03-31 19:49:53.124", + "scheduled_time": "2026-03-31 19:49:53.175", + "end_time": "2026-03-31 19:50:53.160" + }, + "status_code": 200, + "code": "", + "message": "" +} +``` + +## **计费与限流** + +- 模型免费额度和计费单价请参见[模型价格](https://help.aliyun.com/zh/model-studio/model-pricing#e2540d71a2utl)。 + +- 模型限流请参见[万相](https://help.aliyun.com/zh/model-studio/rate-limit#513e0a3df24v7)。 + +- 计费说明:按成功生成的 **图像张数** 计费。模型调用失败或处理错误不产生任何费用,也不消耗[新人免费额度](https://help.aliyun.com/zh/model-studio/new-free-quota)。 + + +## **错误码** + +如果模型调用失败并返回报错信息,请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 + +/\* 调整 table 宽度 \*/ .aliyun-docs-content table.medium-width { max-width: 1018px; width: 100%; } .aliyun-docs-content table.table-no-border tr td:first-child { padding-left: 0; } .aliyun-docs-content table.table-no-border tr td:last-child { padding-right: 0; } /\* 支持吸顶 \*/ div:has(.aliyun-docs-content), .aliyun-docs-content .markdown-body { overflow: visible; } .stick-top { position: sticky; top: 46px; } /\*\*代码块字体\*\*/ /\* 减少表格中的代码块 margin,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body table .help-code-block { margin: 0 !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre { font-size: 12px !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre code { font-size: 12px !important; } /\*\* API Reference 表格 \*\*/ .aliyun-docs-content table.api-reference tr td:first-child { margin: 0px; border-bottom: 1px solid #d8d8d8; } .aliyun-docs-content table.api-reference tr:last-child td:first-child { border-bottom: none; } .aliyun-docs-content table.api-reference p { color: #6e6e80; } .aliyun-docs-content table.api-reference b, i { color: #181818; } .aliyun-docs-content table.api-reference .collapse { border: none; margin-top: 4px; margin-bottom: 4px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse .expandable-title i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse.expanded .expandable-content { padding: 10px 14px 10px 14px !important; margin: 0; border: 1px solid #e9e9e9; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .collapse .expandable-title b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .tabbed-content-box { border: none; } .aliyun-docs-content table.api-reference .tabbed-content-box section { padding: 8px 0 !important; } .aliyun-docs-content table.api-reference .tabbed-content-box.mini .tab-box { /\* position: absolute; left: 40px; right: 0; \*/ } .aliyun-docs-content .margin-top-33 { margin-top: 33px !important; } .aliyun-docs-content .two-codeblocks pre { max-height: calc(50vh - 136px) !important; height: auto; } .expandable-content section { border-bottom: 1px solid #e9e9e9; padding-top: 6px; padding-bottom: 4px; } .expandable-content section:last-child { border-bottom: none; } .expandable-content section:first-child { padding-top: 0; } \ No newline at end of file diff --git a/docs/协议文档/阿里系列/图像-万相-局部重绘.md b/docs/协议文档/阿里系列/图像-万相-局部重绘.md new file mode 100644 index 0000000..db4e185 --- /dev/null +++ b/docs/协议文档/阿里系列/图像-万相-局部重绘.md @@ -0,0 +1,577 @@ +本文介绍万相-图像局部重绘模型的输入输出参数。 + +**相关指南**:[图像局部重绘](https://help.aliyun.com/zh/model-studio/vary-region) + +**重要** + +- 本文档仅适用于华北2(北京)地域,且必须使用该地域的[API Key](https://bailian.console.aliyun.com/?tab=model#/api-key)。 + +- wanx-x-painting 模型当前仅提供**免费体验**,免费额度用完后不可调用且不支持付费,推荐参考[图像编辑-千问](https://help.aliyun.com/zh/model-studio/qwen-image-edit-guide)或[图像编辑-万相2.1](https://help.aliyun.com/zh/model-studio/wanx-image-edit)获取替代方案。 + + +**重要** + +百炼为华北2(北京)地域推出了业务空间专属域名 `https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com`,**能够为推理请求提供卓越的性能和更高的稳定性**,建议从 `https://dashscope.aliyuncs.com` 迁移至新域名。 + +其中 `{WorkspaceId}` 为您的业务空间 ID,可在百炼控制台的**业务空间详情**页面查看。现有域名仍可正常使用。 + +## **模型概览** + +**模型效果示意** + +![image](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/1300704371/p885604.png) + +**模型简介** + +| **模型名称** | **模型简介** | +| --- | --- | +| wanx-x-painting | 万相-图像局部重绘根据用户输入的原始图片、局部区域涂抹图和prompt提示词文字内容,在涂抹区域生成与文字描述相对应的内容,而涂抹区域外的部分则基本保持不变。 | + +**模型说明** + +| **模型名称** | **计费单价** | **限流(主账号与RAM子账号共用)** | | **免费额度**[(查看)](https://help.aliyun.com/zh/model-studio/new-free-quota) | +| --- | --- | --- | --- | --- | +| **任务下发接口QPS限制** | **同时处理中任务数量** | +| wanx-x-painting | 目前仅供免费体验。 > 免费额度用完后不可调用,推荐参考[图像编辑-千问](https://help.aliyun.com/zh/model-studio/qwen-image-edit-guide)或[图像编辑-万相2.1](https://help.aliyun.com/zh/model-studio/wanx-image-edit)获取替代方案。 | 2 | 1 | 500张 | + +更多说明请参见[模型计费及限流说明](#b8457b7223zhp)。 + +## **前提条件** + +图像局部重绘API支持通过HTTP和DashScope SDK进行调用。 + +在调用前,您需要[获取API Key](https://help.aliyun.com/zh/model-studio/get-api-key),再[配置API Key到环境变量](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables)。 + +如需通过SDK进行调用,请[安装DashScope SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。目前,该SDK已支持Python和Java。 + +## **HTTP调用** + +图像模型处理时间较长,为了避免请求超时,HTTP调用仅支持异步获取模型结果。您需要发起两个请求: + +1. **创建任务获取任务ID**:首先发起创建任务请求,该请求会返回任务ID(task\_id)。 + +2. **根据任务ID查询结果**:使用上一步获得的任务ID,查询任务状态及结果。任务成功执行时将返回图像URL,有效期24小时。 + + +**说明** + +创建任务后,该任务将被加入到排队队列,等待调度执行。后续需要调用“根据任务ID查询结果接口”获取任务状态及结果。 + +### **步骤1:创建任务获取任务ID** + +`POST https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/image2image/image-synthesis` + +| #### **请求参数** | ## 图像局部重绘 ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/image2image/image-synthesis' \\ --header 'X-DashScope-Async: enable' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --header 'Content-Type: application/json' \\ --data '{ "model": "wanx-x-painting", "input": { "prompt": "一只狗戴着红色眼镜", "base_image_url": "http://synthesis-source.oss-accelerate.aliyuncs.com/lingji/validation/mask2img/demo/source3.jpg", "mask_image_url": "http://synthesis-source.oss-accelerate.aliyuncs.com/lingji/validation/mask2img/demo/glasses.png" }, "parameters": { "size": "1024*1024", "n": 1 } }' ``` | +| --- | --- | +| ##### **请求头(Headers)** | +| **Content-Type** `*string*` **(必选)** 请求内容类型。此参数必须设置为`application/json`。 | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| **X-DashScope-Async** `*string*` **(必选)** 异步处理配置参数。HTTP请求只支持异步,**必须设置为**`**enable**`。 **重要** 缺少此请求头将报错:“current user api does not support synchronous calls”。 | +| ##### **请求体(Request Body)** | +| **model** `*string*` **(必选)** 模型名称。示例值:wanx-x-painting。 | +| **input** `*object*` **(必选)** 输入的基本信息,如提示词、图像URL等。 **属性** **prompt** `*string*` **(必选)** 正向提示词,用来描述生成图像中期望包含的元素和视觉特点。 支持中英文,长度不超过75个字符,超过部分会自动截断。 **base\\_image\\_url** `*string*` **(必选)** 输入图像URL地址,不支持填写图像Base64数据。 URL 需为公网可访问的地址,并支持 HTTP 或 HTTPS 协议。您也可在此[获取临时公网URL](https://help.aliyun.com/zh/model-studio/get-temporary-file-url)。 图像限制: - 图像格式:JPG、JPEG、PNG、BMP。 - 图像分辨率:大于256×256像素,小于4096×4096像素。 - 图像大小:不超过10MB。 - URL地址中不能包含中文字符。 **mask\\_image\\_url** `*string*` **(必选)** 用户标记涂抹区域的图像URL地址,需要和base\\_image\\_url图像分辨率保持一致。不支持填写图像Base64数据。 URL 需为公网可访问的地址,并支持 HTTP 或 HTTPS 协议。您也可在此[获取临时公网URL](https://help.aliyun.com/zh/model-studio/get-temporary-file-url)。 图像限制: - 图像格式:JPG、JPEG、PNG、BMP。 - 图像分辨率:大于256×256像素,小于4096×4096像素。 - 图像大小:不超过10MB。 - URL地址中不能包含中文字符。 您可以使用PS或者其他图像编辑工具在原图上涂抹获得掩码图像。 | +| **parameters** `*object*` (可选) 图像处理参数。 **属性** **style** `*string*` (可选) 输出图像的风格。目前支持以下风格取值: - : 默认。 - <3d cartoon>:3D卡通。 - :动画。 - : 油画。 - :水彩。 - :素描。 - :中国画。 - :扁平插画。 **size** `*string*` (可选) 输出图像的分辨率。目前支持3种图像分辨率: - 1024\\*1024:默认值。 - 720\\*1280 - 1280\\*720 **n** `*integer*` (可选) 生成图片的数量。取值范围为1~4张,默认为1。 **mask\\_color** `*array*` (可选) RGB颜色数值列表,用于指定掩码图片中表示涂改区域的颜色。默认值为\\[\\]。 当该字段为空,默认对掩码图片进行二值化处理(处理后白色代表涂抹区域)。当该字段非空时,表示RGB颜色数值列表,代表一种或多种颜色。这些RGB颜色所描绘的区域即为涂改区域,如\\[0,0,0\\]和\\[134,134,134\\],而未被指定的颜色则视为背景色。 示例值:\\[\\[0, 0, 0\\], \\[134, 134, 134\\]\\]。 | | + +| #### **响应参数** | #### 成功响应 请保存 task\\_id,用于查询任务状态与结果。 ``` { "output": { "task_status": "PENDING", "task_id": "0385dc79-5ff8-4d82-bcb6-xxxxxx" }, "request_id": "4909100c-7b5a-9f92-bfe5-xxxxxx" } ``` #### 异常响应 创建任务失败,请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "code": "InvalidApiKey", "message": "No API-key provided.", "request_id": "7438d53d-6eb8-4596-8835-xxxxxx" } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 属性 **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | +| **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | +| **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | + +### 步骤2:根据任务ID查询结果 + +`GET https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/tasks/{task_id}` + +| #### 请求参数 | #### 查询任务结果 请将`86ecf553-d340-4e21-xxxxxxxxx`替换为真实的task\\_id。 > 若使用新加坡地域的模型,需将base\\_url替换为https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/tasks/86ecf553-d340-4e21-xxxxxxxxx,其中WorkspaceId需替换为真实的业务空间ID。 ``` curl -X GET https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/tasks/86ecf553-d340-4e21-xxxxxxxxx \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" ``` | +| --- | --- | +| #### **请求头(Headers)** | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| #### **URL路径参数(Path parameters)** | +| **task\\_id** `*string*`**(必选)** 任务ID。 | + +| #### **响应参数** | #### 任务执行成功 任务数据(如任务状态、图像URL等)仅保留24小时,超时后会被自动清除。请您务必及时保存生成的图像。 ``` { "request_id": "85eaba38-0185-99d7-8d16-4d9135238846", "output": { "task_id": "86ecf553-d340-4e21-af6e-a0c6a421c010", "task_status": "SUCCEEDED", "results": [ { "url": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/123/a1.png" }, { "url": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/123/b2.png" } ], "task_metrics": { "TOTAL": 2, "SUCCEEDED": 2, "FAILED": 0 } }, "usage": { "image_count": 2 } } ``` #### 任务执行失败 若任务执行失败,task\\_status将置为 FAILED,并提供错误码和信息。请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "request_id": "e5d70b02-ebd3-98ce-9fe8-759d7d7b107d", "output": { "task_id": "86ecf553-d340-4e21-af6e-a0c6a421c010", "task_status": "FAILED", "code": "InvalidParameter", "message": "The size is not match the allowed size ['1024*1024', '720*1280', '1280*720']", "task_metrics": { "TOTAL": 4, "SUCCEEDED": 0, "FAILED": 4 } } } ``` #### 任务部分失败 模型可以在一次任务中生成多张图片。只要有一张图片生成成功,任务状态将标记为`SUCCEEDED`,并且返回相应的图像URL。对于生成失败的图片,结果中会返回相应的失败原因。同时在usage统计中,只会对成功的结果计数。请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "request_id": "85eaba38-0185-99d7-8d16-4d9135238846", "output": { "task_id": "86ecf553-d340-4e21-af6e-a0c6a421c010", "task_status": "SUCCEEDED", "results": [ { "url": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/123/a1.png" }, { "code": "InternalError.Timeout", "message": "An internal timeout error has occured during execution, please try again later or contact service support." } ], "task_metrics": { "TOTAL": 2, "SUCCEEDED": 1, "FAILED": 1 } }, "usage": { "image_count": 1 } } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 **task\\_metrics** `*object*` 任务结果统计。 **属性** **TOTAL** `*integer*` 总的任务数。 **SUCCEEDED** `*integer*` 任务状态为成功的任务数。 **FAILED** `*integer*` 任务状态为失败的任务数。 **results** `*array of object*` 任务结果列表,包括图像URL、部分任务执行失败报错信息等。 **数据结构** ``` { "results": [ { "url": "" }, { "code": "", "message": "" } ] } ``` **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | +| **usage** `*object*` 输出信息统计。只对成功的结果计数。 **属性** **image\\_count** `*integer*` 模型成功生成图片的数量。计费公式:费用 = 图片数量 × 单价。 | | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | | + +## DashScope SDK调用 + +请先确认已安装最新版DashScope SDK:[安装SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。 + +DashScope SDK目前已支持Python和Java。 + +SDK与HTTP接口的参数名基本一致,参数结构根据不同语言的SDK封装而定。参数说明可参考[HTTP调用](https://help.aliyun.com/zh/model-studio/text-to-image-api-reference#42703589880ts)。 + +由于图像模型处理时间较长,底层服务采用异步方式提供。SDK在上层进行了封装,支持同步、异步两种调用方式。 + +### Python SDK调用 + +## 同步调用 + +##### **请求示例** + +``` +from http import HTTPStatus +from urllib.parse import urlparse, unquote +from pathlib import PurePosixPath +import requests +from dashscope import ImageSynthesis + +# 以下为华北2(北京)地域的配置,调用时请将WorkspaceId替换为真实的业务空间ID,各地域的配置不同。 +dashscope.base_http_api_url = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1" + +prompt = "一只狗戴着红色眼镜" +model = "wanx-x-painting" +task = "image2image" +extra_input = { + "base_image_url": "http://synthesis-source.oss-accelerate.aliyuncs.com/lingji/validation/mask2img/demo/source3.jpg", + "mask_image_url": "http://synthesis-source.oss-accelerate.aliyuncs.com/lingji/validation/mask2img/demo/glasses.png" +} + + +print('----sync call, please wait a moment----') +rsp = ImageSynthesis.call(model=model, + prompt=prompt, + n=1, + size='1024*1024', + task=task, + extra_input=extra_input) +if rsp.status_code == HTTPStatus.OK: + print(rsp) + # save file to current directory + for result in rsp.output.results: + file_name = PurePosixPath(unquote(urlparse(result.url).path)).parts[-1] + with open('./%s' % file_name, 'wb+') as f: + f.write(requests.get(result.url).content) +else: + print('sync_call Failed, status_code: %s, code: %s, message: %s' % + (rsp.status_code, rsp.code, rsp.message)) +``` + +##### **响应示例** + +``` +{ + "status_code": 200, + "request_id": "4126d9dd-e037-9f32-8d56-6d29ab3f9a06", + "code": null, + "message": "", + "output": { + "task_id": "b476bc4e-35c1-4c4e-a4d9-xxxxxxx", + "task_status": "SUCCEEDED", + "results": [{ + "url": "https://dashscope-result-sh.oss-cn-shanghai.aliyuncs.com/xxxx.png" + }], + "submit_time": "2024-11-01 09:50:56.081", + "scheduled_time": "2024-11-01 09:50:56.104", + "end_time": "2024-11-01 09:51:22.740", + "task_metrics": { + "TOTAL": 1, + "SUCCEEDED": 1, + "FAILED": 0 + } + }, + "usage": { + "image_count": 1 + } +} +``` + +## 异步调用 + +##### **请求示例** + +``` +from http import HTTPStatus +from urllib.parse import urlparse, unquote +from pathlib import PurePosixPath +import requests +from dashscope import ImageSynthesis + +# 以下为华北2(北京)地域的配置,调用时请将WorkspaceId替换为真实的业务空间ID,各地域的配置不同。 +dashscope.base_http_api_url = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1" + +prompt = "一只狗戴着红色眼镜" +model = "wanx-x-painting" +task = "image2image" +extra_input = { + "base_image_url": "http://synthesis-source.oss-accelerate.aliyuncs.com/lingji/validation/mask2img/demo/source3.jpg", + "mask_image_url": "http://synthesis-source.oss-accelerate.aliyuncs.com/lingji/validation/mask2img/demo/glasses.png" +} + + +def async_call(): + print('----create task----') + task_info = create_async_task() + print('----wait task done then save image----') + wait_async_task(task_info) + + +# 创建异步任务 +def create_async_task(): + rsp = ImageSynthesis.async_call(model=model, + prompt=prompt, + n=1, + size='1024*1024', + task=task, + extra_input=extra_input) + print(rsp) + if rsp.status_code == HTTPStatus.OK: + print(rsp.output) + else: + print('create_async_task Failed, status_code: %s, code: %s, message: %s' % + (rsp.status_code, rsp.code, rsp.message)) + return rsp + + +# 等待异步任务结束 +def wait_async_task(task): + rsp = ImageSynthesis.wait(task) + print(rsp) + if rsp.status_code == HTTPStatus.OK: + print(rsp.output.task_status) + # save file to current directory + for result in rsp.output.results: + file_name = PurePosixPath(unquote(urlparse(result.url).path)).parts[-1] + with open('./%s' % file_name, 'wb+') as f: + f.write(requests.get(result.url).content) + else: + print('Failed, status_code: %s, code: %s, message: %s' % + (rsp.status_code, rsp.code, rsp.message)) + + +if __name__ == '__main__': + async_call() +``` + +##### **响应示例** + +**1、创建任务的响应示例** + +``` +{ + "status_code": 200, + "request_id": "31b04171-011c-96bd-ac00-f0383b669cc7", + "code": "", + "message": "", + "output": { + "task_id": "4f90cf14-a34e-4eae-xxxxxxxx", + "task_status": "PENDING", + "results": [] + }, + "usage": null +} +``` + +**2、查询任务结果的响应示例** + +``` +{ + "status_code": 200, + "request_id": "d861d3ba-4b29-9491-abad-266ef4fb2f08", + "code": null, + "message": "", + "output": { + "task_id": "4f90cf14-a34e-4eae-xxxxxxxx", + "task_status": "SUCCEEDED", + "results": [{ + "url": "https://dashscope-result-hz.oss-cn-hangzhou.aliyuncs.com/xxxx.png" + }], + "submit_time": "2024-10-31 20:40:35.631", + "scheduled_time": "2024-10-31 20:40:35.684", + "end_time": "2024-10-31 20:41:02.700", + "task_metrics": { + "TOTAL": 1, + "SUCCEEDED": 1, + "FAILED": 0 + } + }, + "usage": { + "image_count": 1 + } +} +``` + +### Java SDK调用 + +## 同步调用 + +##### 请求示例 + +``` +// Copyright (c) Alibaba, Inc. and its affiliates. + +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.utils.JsonUtils; + +import java.util.HashMap; +import com.alibaba.dashscope.utils.Constants; + +public class Main { + static {Constants.baseHttpApiUrl="https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1";} + + public void syncCall() { + String task = "image2image"; + ImageSynthesis imageSynthesis = new ImageSynthesis(task); + ImageSynthesisParam param = genImageSynthesis(); + ImageSynthesisResult result = null; + try { + System.out.println("---sync call, please wait a moment----"); + result = imageSynthesis.call(param); + } catch (ApiException | NoApiKeyException e){ + throw new RuntimeException(e.getMessage()); + } + System.out.println(JsonUtils.toJson(result)); + } + + private ImageSynthesisParam genImageSynthesis(){ + HashMap extraInputMap = new HashMap<>(); + extraInputMap.put("base_image_url", "http://synthesis-source.oss-accelerate.aliyuncs.com/lingji/validation/mask2img/demo/source3.jpg"); + extraInputMap.put("mask_image_url", "http://synthesis-source.oss-accelerate.aliyuncs.com/lingji/validation/mask2img/demo/glasses.png"); + String prompt = "一只狗戴着红色眼镜"; + String model = "wanx-x-painting"; + return ImageSynthesisParam.builder() + .model(model) + .prompt(prompt) + .n(1) + .size("1024*1024") + .extraInputs(extraInputMap) + .build(); + } + + + public static void main(String[] args){ + Main text2Image = new Main(); + text2Image.syncCall(); + } + +} +``` + +##### **响应示例** + +``` +{ + "request_id": "150edcda-05d5-9ffe-8803-84626d1db623", + "output": { + "task_id": "f2098ff0-146e-404c-bb25-xxxxxxxx", + "task_status": "SUCCEEDED", + "results": [{ + "url": "https://dashscope-result-hz.oss-cn-hangzhou.aliyuncs.com/xxxx.png" + }], + "task_metrics": { + "TOTAL": 1, + "SUCCEEDED": 1, + "FAILED": 0 + } + }, + "usage": { + "image_count": 1 + } +} +``` + +## 异步调用 + +##### **请求示例** + +``` +// Copyright (c) Alibaba, Inc. and its affiliates. + +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.utils.JsonUtils; + +import java.util.HashMap; +import com.alibaba.dashscope.utils.Constants; + +public class Main { + static {Constants.baseHttpApiUrl="https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1";} + + public void asyncCall() { + System.out.println("---create task----"); + String taskId = this.createAsyncTask(); + System.out.println("---wait task done then return image url----"); + this.waitAsyncTask(taskId); + } + + + /** + * 创建异步任务 + * @return taskId + */ + public String createAsyncTask() { + String task = "image2image"; + ImageSynthesis imageSynthesis = new ImageSynthesis(task); + ImageSynthesisParam param = genImageSynthesis(); + ImageSynthesisResult result = null; + try { + result = imageSynthesis.asyncCall(param); + } catch (ApiException | NoApiKeyException e){ + throw new RuntimeException(e.getMessage()); + } + String taskId = result.getOutput().getTaskId(); + System.out.println("taskId=" + taskId); + return taskId; + } + + + private ImageSynthesisParam genImageSynthesis(){ + String prompt = "一只狗戴着红色眼镜"; + String model = "wanx-x-painting"; + HashMap extraInputMap = new HashMap<>(); + extraInputMap.put("base_image_url", "http://synthesis-source.oss-accelerate.aliyuncs.com/lingji/validation/mask2img/demo/source3.jpg"); + extraInputMap.put("mask_image_url", "http://synthesis-source.oss-accelerate.aliyuncs.com/lingji/validation/mask2img/demo/glasses.png"); + + return ImageSynthesisParam.builder() + .model(model) + .prompt(prompt) + .n(1) + .size("1024*1024") + .extraInputs(extraInputMap) + .build(); + } + + + /** + * 等待异步任务结束 + * @param taskId 任务id + * */ + public void waitAsyncTask(String taskId) { + ImageSynthesis imageSynthesis = new ImageSynthesis(); + ImageSynthesisResult result = null; + try { + // If you have set the DASHSCOPE_API_KEY in the system environment variable, the apiKey can be null. + result = imageSynthesis.wait(taskId, null); + } catch (ApiException | NoApiKeyException e){ + throw new RuntimeException(e.getMessage()); + } + + System.out.println(JsonUtils.toJson(result.getOutput())); + System.out.println(JsonUtils.toJson(result.getUsage())); + } + + + public static void main(String[] args){ + Main text2Image = new Main(); + text2Image.asyncCall(); + } + +} +``` + +##### **响应示例** + +**1、创建任务的响应示例** + +``` +{ + "request_id": "5dbf9dc5-4f4c-9605-85ea-542f97709ba8", + "output": { + "task_id": "7277e20e-aa01-4709-xxxxxxxx", + "task_status": "PENDING" + } +} +``` + +**2、查询任务结果的响应示例** + +**output字段内容:** + +``` +{ + "task_id": "7277e20e-aa01-4709-xxxxxxxx", + "task_status": "SUCCEEDED", + "results": [{ + "url": "https://dashscope-result-hz.oss-cn-hangzhou.aliyuncs.com/xxxx.png" + }], + "task_metrics": { + "TOTAL": 1, + "SUCCEEDED": 1, + "FAILED": 0 + } +} +``` + +**usage字段内容:** + +``` +{ + "image_count": 1 +} +``` + +## **错误码** + +如果模型调用失败并返回报错信息,请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 + +## 常见问题 + +### 模型计费及限流说明 + +**免费额度** + +- 额度说明:免费额度是指模型成功生成的输出图片数量。输入图片及模型处理失败的情况不占用免费额度。 + +- 领取方式:开通阿里云百炼大模型服务后自动发放,有效期90天。 + +- 使用账号:阿里云主账号与其RAM子账号共享免费额度。 + +- 更多详情请参见[新人免费额度](https://help.aliyun.com/zh/model-studio/new-free-quota)。 + + +**限时免费** + +- 当计费为限时免费时,表示该模型处于公测阶段,免费额度用尽后不可使用。 + + +**计费说明** + +- 当计费有明确单价时,如0.2元/秒,表示该模型已商业化,免费额度用尽或过期后需付费使用。 + +- 计费项:只对模型成功生成的输出图片进行收费,其余情况暂不计费。 + +- 付费方式:由阿里云主账号统一付费。RAM子账号不能独立计量计费,必须由所属的主账号付费。如果您需要查询账单信息,请前往阿里云控制台[账单概览](https://billing-cost.console.aliyun.com/finance/month-bill/account)。 + +- 充值途径:您可以在阿里云控制台[费用与成本](https://billing-cost.console.aliyun.com/home?spm=a2c4g.11186623.0.0.2d543048F4KRQP)页面进行充值。 + +- 模型调用情况:您可以前往阿里云百炼的[模型观测](https://bailian.console.aliyun.com/#/model-telemetry)查看模型调用量及调用次数。 + +- 更多计费问题请参见[计费项](https://help.aliyun.com/zh/model-studio/billing-for-model-studio)。 + + +**限流** + +- 限流说明:阿里云主账号与其RAM子账号共享限流限制。 + + +### **图像局部重绘API通用问题** + +1. **输入图像的掩码图像(mask\_image)有什么限制?** + + - 只能使用图像URL地址,暂不支持填写图像Base64数据。 + + - 需要和输入图像的分辨率保持一致。 + + - 文档中的示例掩码图像为白色背景,实际上,掩码图像并不要求必须为白色背景,可以通过`mask_color`字段指定其他颜色。 + + +2. **如何获得输入图像的掩码图像?** + + 您可以使用PS或者其他图像编辑工具在原图上涂抹获得掩码图像。 + +3. **使用****图像局部重绘****功能时,若涂抹区域外的图片输出效果不理想,应如何处理?** + + 建议您使用[通用图像编辑](https://help.aliyun.com/zh/model-studio/wanx-image-edit-api-reference)的局部重绘功能重新生成。 + + +/\* 调整 table 宽度 \*/ .aliyun-docs-content table.medium-width { max-width: 1018px; width: 100%; } .aliyun-docs-content table.table-no-border tr td:first-child { padding-left: 0; } .aliyun-docs-content table.table-no-border tr td:last-child { padding-right: 0; } /\* 支持吸顶 \*/ div:has(.aliyun-docs-content), .aliyun-docs-content .markdown-body { overflow: visible; } .stick-top { position: sticky; top: 46px; } /\*\*代码块字体\*\*/ /\* 减少表格中的代码块 margin,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body table .help-code-block { margin: 0 !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre { font-size: 12px !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre code { font-size: 12px !important; } /\*\* API Reference 表格 \*\*/ .aliyun-docs-content table.api-reference tr td:first-child { margin: 0px; border-bottom: 1px solid #d8d8d8; } .aliyun-docs-content table.api-reference tr:last-child td:first-child { border-bottom: none; } .aliyun-docs-content table.api-reference p { color: #6e6e80; } .aliyun-docs-content table.api-reference b, i { color: #181818; } .aliyun-docs-content table.api-reference .collapse { border: none; margin-top: 4px; margin-bottom: 4px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse .expandable-title i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse.expanded .expandable-content { padding: 10px 14px 10px 14px !important; margin: 0; border: 1px solid #e9e9e9; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .collapse .expandable-title b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .tabbed-content-box { border: none; } .aliyun-docs-content table.api-reference .tabbed-content-box section { padding: 8px 0 !important; } .aliyun-docs-content table.api-reference .tabbed-content-box.mini .tab-box { /\* position: absolute; left: 40px; right: 0; \*/ } .aliyun-docs-content .margin-top-33 { margin-top: 33px !important; } .aliyun-docs-content .two-codeblocks pre { max-height: calc(50vh - 136px) !important; height: auto; } .expandable-content section { border-bottom: 1px solid #e9e9e9; padding-top: 6px; padding-bottom: 4px; } .expandable-content section:last-child { border-bottom: none; } .expandable-content section:first-child { padding-top: 0; } \ No newline at end of file diff --git a/docs/协议文档/阿里系列/图像-万相-文生图V2.md b/docs/协议文档/阿里系列/图像-万相-文生图V2.md new file mode 100644 index 0000000..96a3781 --- /dev/null +++ b/docs/协议文档/阿里系列/图像-万相-文生图V2.md @@ -0,0 +1,1378 @@ +万相-文生图模型基于文本生成图像,支持多种艺术风格与写实摄影效果,满足多样化创意需求。 + +**快速入口:**在线体验([北京](https://bailian.console.aliyun.com/?tab=model#/efm/model_experience_center/vision?currentTab=imageGenerate&modelId=qwen-image) | [新加坡](https://modelstudio.console.aliyun.com/?tab=dashboard#/efm/model_experience_center/vision?currentTab=imageGenerate)| [弗吉尼亚](https://modelstudio.console.aliyun.com/us-east-1?tab=dashboard#/efm/model_experience_center/vision?currentTab=imageGenerate&modelId=wan2.6-t2i)) | [万相官网](https://tongyi.aliyun.com/wan/generate/image/text-to-image) | [文生图使用指南](https://help.aliyun.com/zh/model-studio/text-to-image) + +**说明** + +万相官网的功能与API支持的能力可能存在差异。本文档以API的实际能力为准,并会随功能更新及时同步。 + +## 模型概览 + +| **模型名称** | **模型简介** | **输出图像格式** | +| --- | --- | --- | +| wan2.6-t2i `**推荐**` | 万相2.6 支持在总像素面积与宽高比约束内,自由选尺寸(同wan2.5) | 图像分辨率:总像素在\\[1280\\*1280, 1440\\*1440\\]之间 图像宽高比:\\[1:4, 4:1\\] 图像格式:png | +| wan2.5-t2i-preview `**推荐**` | 万相2.5 preview 支持在总像素面积与宽高比约束内,自由选尺寸 > 例如,支持768\\*2700,而2.2及以下版本单边上限 1400 | +| wan2.2-t2i-flash | 万相2.2极速版 较2.1模型速度提升50% | 图像分辨率:宽高均在\\[512, 1440\\]像素之间 图像格式:png | +| wan2.2-t2i-plus | 万相2.2专业版 较2.1模型稳定性与成功率全面提升 | +| wanx2.1-t2i-turbo | 万相2.1极速版 | +| wanx2.1-t2i-plus | 万相2.1专业版 | +| wanx2.0-t2i-turbo | 万相2.0极速版 | + +**说明** + +- 调用前,请查阅各地域支持的模型列表。 + +- **wan2.6模型**:支持[HTTP同步调用](#39337f0fd0wzt)、[HTTP异步调用](#9c68ccc525nta)、[Dashscope Python SDK调用](#b3d1dbc5dbtgx)和[Dashscope Java SDK调用](#e8a7f6d4c9xyz)。 + +- **wan2.5及以下版本模型**:支持[HTTP异步调用](#0d8029dcc8pxl)、[Dashscope Python SDK调用](#7afdfdd68emtf)和[Dashscope Java SDK调用](#589b80853e6rn),不支持HTTP同步调用。 + + +## 前提条件 + +在调用前,先[获取API Key](https://help.aliyun.com/zh/model-studio/get-api-key),再[配置API Key到环境变量](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables)。如需通过SDK进行调用,请[安装DashScope SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。 + +**重要** + +华北2(北京)、新加坡和美国(弗吉尼亚)地域拥有独立的 **API Key** 与**请求地址**,不可混用,跨地域调用将导致鉴权失败或服务报错,详情请参见[选择地域、服务部署范围和接入域名](https://help.aliyun.com/zh/model-studio/regions/)。 + +**重要** + +阿里云百炼为华北2(北京)、新加坡地域推出了业务空间专属域名,**能够为推理请求提供卓越的性能和更高的稳定性**,建议迁移至新域名: + +- 华北2(北京)地域:从 `https://dashscope.aliyuncs.com` 迁移至 `https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com` + +- 新加坡地域:从 `https://dashscope-intl.aliyuncs.com` 迁移至 `https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com` + + +其中 `{WorkspaceId}` 为您的业务空间 ID,可在阿里云百炼控制台的**业务空间详情**页面查看。现有域名仍可正常使用。 + +## **HTTP同步调用(wan2.6)** + +**重要** + +本章节接口为**新版协议**,仅支持 **wan2.6**模型。 + +一次请求即可获得结果,流程简单,推荐大多数场景使用。 + +### 华北2(北京) + +`POST https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +### 新加坡 + +`POST https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +### 美国(弗吉尼亚) + +`POST https://dashscope-us.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` + +| #### 请求参数 | ## **文生图** ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \\ --header 'Content-Type: application/json' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --data '{ "model": "wan2.6-t2i", "input": { "messages": [ { "role": "user", "content": [ { "text": "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵" } ] } ] }, "parameters": { "prompt_extend": true, "watermark": false, "n": 1, "negative_prompt": "", "size": "1280*1280" } }' ``` | +| --- | --- | +| ##### 请求头(Headers) | +| **Content-Type** `*string*` **(必选)** 请求内容类型。此参数必须设置为`application/json`。 | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| ##### 请求体(Request Body) | +| **model** `*string*` **(必选)** 模型名称。示例值:wan2.6-t2i。 **说明** wan2.5及以下版本模型,HTTP调用请参见[HTTP异步调用](#0d8029dcc8pxl)。 | +| **input** `*object*` **(必选)** 输入的基本信息。 **属性** **messages** `*array*` **(必选)** 请求内容数组。当前**仅支持单轮对话**,即传入一组role、content参数,不支持多轮对话。 **属性** **role** `*string*` **(必选)** 消息的角色。此参数必须设置为`user`。 **content** `*array*` **(必选)** 消息内容数组。 **属性** **text** `*string*` **(必选)** 正向提示词,用于描述期望生成的图像内容、风格和构图。 支持中英文,长度不超过2100个字符,每个汉字、字母、数字或符号计为一个字符,超过部分会自动截断。 示例值:一只坐着的橘黄色的猫,表情愉悦,活泼可爱,逼真准确。 **注意**:仅支持传入一个text,不传或传入多个将报错。 | +| **parameters** `*object*` (可选) 图像处理参数。 **属性** **negative\\_prompt** `*string*` (可选) 反向提示词,用于描述不希望在图像中出现的内容,对画面进行限制。 支持中英文,长度不超过500个字符,超出部分将自动截断。 示例值:低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感。构图混乱。文字模糊,扭曲。 **size** `*string*` (可选) 输出图像的分辨率,格式为`**宽*高**`。 - 默认值为 `1280*1280`。 - 总像素在 \\[1280\\*1280, 1440\\*1440\\] 之间且宽高比范围为 \\[1:4, 4:1\\]。例如,768\\*2700符合要求。 示例值:1280\\*1280。 **常见比例推荐的分辨率** - 1:1:1280\\*1280 - 3:4:1104\\*1472 - 4:3:1472\\*1104 - 9:16:960\\*1696 - 16:9:1696\\*960 **n** `*integer*` (可选) **重要** n直接影响费用。费用 = 单价 × 图片张数,请在调用前确认[模型价格](https://help.aliyun.com/zh/model-studio/model-pricing#0006b52b83ua9)。 生成图片的数量。取值范围为1~4张,默认为`4`。 注意:按张计费,测试建议设为 1。 **prompt\\_extend** `*bool*` (可选) 是否开启提示词智能改写。开启后,将使用大模型优化正向提示词,对较短的提示词有明显提升效果,但增加3-4秒耗时。 - true:默认值,开启智能改写。 - false:关闭智能改写。 **watermark** `*bool*` (可选) 是否添加水印标识,水印位于图片右下角,文案固定为“AI生成”。 - false:默认值,不添加水印。 - true:添加水印。 **seed** `*integer*` (可选) 随机数种子,取值范围`[0,2147483647]`。 使用相同的`seed`参数值可使生成内容保持相对稳定。若不提供,算法将自动使用随机数种子。 **注意**:模型生成过程具有概率性,即使使用相同的`seed`,也不能保证每次生成结果完全一致。 | + +| #### 响应参数 | ## 任务执行成功 任务数据(如任务状态、图像URL等)仅保留24小时,超时后会被自动清除。请您务必及时保存生成的图像。 ``` { "output": { "choices": [ { "finish_reason": "stop", "message": { "content": [ { "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxx.png?Expires=xxx", "type": "image" } ], "role": "assistant" } } ], "finished": true }, "usage": { "image_count": 1, "input_tokens": 0, "output_tokens": 0, "size": "1280*1280", "total_tokens": 0 }, "request_id": "815505c6-7c3d-49d7-b197-xxxxx" } ``` ## 任务执行异常 如果因为某种原因导致任务执行失败,将返回相关信息,可以通过code和message字段明确指示错误原因。请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "request_id": "a4d78a5f-655f-9639-8437-xxxxxx", "code": "InvalidParameter", "message": "num_images_per_prompt must be 1" } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **choices** `*array*` 模型生成的输出内容。 **属性** **finish\\_reason** `*string*` 任务停止原因,自然停止时为`stop`。 **message** `*object*` 模型返回的消息。 **属性** **role** `*string*` 消息的角色,固定为`assistant`。 **content** `*array*` **属性** **image** `*string*` 生成图像的 URL,图像格式为PNG。**链接有效期为24小时**,请及时下载并保存图像。 **type** `*string*` 输出的类型,固定为image。 **finished** `*boolean*` 任务是否结束。 - true:已结束。 - false:未结束。 | +| **usage** `*object*` 输出信息统计。只对成功的结果计数。 **属性** **image\\_count** `*integer*` 生成图像的张数。 **size** `*string*` 生成的图像分辨率。示例值:1280\\*1280。 **input\\_tokens** `*integer*` 输入token。文生图按图片张数计费,当前固定为0。 **output\\_tokens** `*integer*` 输出token。文生图按图片张数计费,当前固定为0。 **total\\_tokens** `*integer*` 总token。文生图按图片张数计费,当前固定为0。 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | +| **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | +| **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | + +## **HTTP异步调用(wan2.6)** + +**重要** + +本章节接口为**新版协议**,仅支持 **wan2.6**模型。 + +任务流程包含 **“创建任务 -> 轮询获取”** 两个核心步骤,具体如下: + +### 步骤1:创建任务获取任务ID + +#### 华北2(北京) + +`POST https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/image-generation/generation` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +#### 新加坡 + +`POST https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/services/aigc/image-generation/generation` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +#### 美国(弗吉尼亚) + +`POST https://dashscope-us.aliyuncs.com/api/v1/services/aigc/image-generation/generation` + +#### 德国(法兰克福) + +`POST https://{WorkspaceId}.eu-central-1.maas.aliyuncs.com/api/v1/services/aigc/image-generation/generation` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +**说明** + +- 创建成功后,使用接口返回的 `task_id` 查询结果,task\_id 有效期为 24 小时。**请勿重复创建任务**,轮询获取即可。 + +- 新手指引请参见[Postman](https://help.aliyun.com/zh/model-studio/first-call-to-image-and-video-api)。 + + +| #### 请求参数 | ## **文生图** ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/image-generation/generation' \\ --header 'Content-Type: application/json' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --header 'X-DashScope-Async: enable' \\ --data '{ "model": "wan2.6-t2i", "input": { "messages": [ { "role": "user", "content": [ { "text": "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵" } ] } ] }, "parameters": { "prompt_extend": true, "watermark": false, "n": 1, "negative_prompt": "", "size": "1280*1280" } }' ``` | +| --- | --- | +| ##### 请求头(Headers) | +| **Content-Type** `*string*` **(必选)** 请求内容类型。此参数必须设置为`application/json`。 | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| **X-DashScope-Async** `*string*` **(必选)** 异步处理配置参数。HTTP请求只支持异步,**必须设置为**`**enable**`。 **重要** 缺少此请求头将报错:“current user api does not support synchronous calls”。 | +| ##### 请求体(Request Body) | +| **model** `*string*` **(必选)** 模型名称。示例值:wan2.6-t2i。 **说明** wan2.5及以下版本模型,HTTP调用请参见[HTTP异步调用。](#0d8029dcc8pxl) | +| **input** `*object*` **(必选)** 输入的基本信息。 **属性** **messages** `*array*` **(必选)** 请求内容数组。当前**仅支持单轮对话**,即传入一组role、content参数,不支持多轮对话。 **属性** **role** `*string*` **(必选)** 消息的角色。此参数必须设置为`user`。 **content** `*array*` **(必选)** 消息内容数组。 **属性** **text** `*string*` **(必选)** 正向提示词,用于描述期望生成的图像内容、风格和构图。 支持中英文,长度不超过2100个字符,每个汉字、字母、数字或符号计为一个字符,超过部分会自动截断。 示例值:一间有着精致窗户的花店,漂亮的木质门,摆放着花朵。 **注意**:仅支持传入一个text,不传或传入多个将报错。 | +| **parameters** `*object*` (可选) 图像处理参数。 **属性** **negative\\_prompt** `*string*` (可选) 反向提示词,用于描述不希望在图像中出现的内容,对画面进行限制。 支持中英文,长度不超过500个字符,超出部分将自动截断。 示例值:低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感。构图混乱。文字模糊,扭曲。 **size** `*string*` (可选) 输出图像的分辨率,格式为`**宽*高**`。 - 默认值为 `1280*1280`。 - 总像素在 \\[1280\\*1280, 1440\\*1440\\] 之间且宽高比范围为 \\[1:4, 4:1\\]。例如,768\\*2700符合要求。 示例值:1280\\*1280。 **常见比例推荐的分辨率** - 1:1:1280\\*1280 - 3:4:1104\\*1472 - 4:3:1472\\*1104 - 9:16:960\\*1696 - 16:9:1696\\*960 **n** `*integer*` (可选) **重要** n直接影响费用。费用 = 单价 × 图片张数,请在调用前确认[模型价格](https://help.aliyun.com/zh/model-studio/model-pricing#0006b52b83ua9)。 生成图片的数量。取值范围为1~4张,默认为`4`。 注意:按张计费,测试建议设为 1。 **prompt\\_extend** `*bool*` (可选) 是否开启prompt智能改写。开启后,将使用大模型优化正向提示词,对较短的提示词有明显提升效果,但增加3-4秒耗时。 - true:默认值,开启智能改写。 - false:不开启智能改写。 **watermark** `*bool*` (可选) 是否添加水印标识,水印位于图片右下角,文案固定为“AI生成”。 - false:默认值,不添加水印。 - true:添加水印。 **seed** `*integer*` (可选) 随机数种子,取值范围`[0,2147483647]`。 使用相同的`seed`参数值可使生成内容保持相对稳定。若不提供,算法将自动使用随机数种子。 **注意**:模型生成过程具有概率性,即使使用相同的`seed`,也不能保证每次生成结果完全一致。 | + +| #### 响应参数 | ### 成功响应 请保存 task\\_id,用于查询任务状态与结果。 ``` { "output": { "task_status": "PENDING", "task_id": "0385dc79-5ff8-4d82-bcb6-xxxxxx" }, "request_id": "4909100c-7b5a-9f92-bfe5-xxxxxx" } ``` ### 异常响应 创建任务失败,请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "code": "InvalidApiKey", "message": "No API-key provided.", "request_id": "7438d53d-6eb8-4596-8835-xxxxxx" } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | +| **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | | +| **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | | + +### 步骤2:根据任务ID查询结果 + +#### 华北2(北京) + +`GET https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/tasks/{task_id}` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +#### 新加坡 + +`GET https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/tasks/{task_id}` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +#### **美国(弗吉尼亚)** + +`GET https://dashscope-us.aliyuncs.com/api/v1/tasks/{task_id}` + +#### 德国(法兰克福) + +`GET https://{WorkspaceId}.eu-central-1.maas.aliyuncs.com/api/v1/tasks/{task_id}` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +**说明** + +- **轮询建议**:图像生成过程耗时较长,建议采用**轮询**机制,并设置合理的查询间隔(如 10 秒)来获取结果。 + +- **任务状态流转**:PENDING(排队中)→ RUNNING(处理中)→ SUCCEEDED(成功)/ FAILED(失败)。 + +- **结果链接**:任务成功后返回图像链接,有效期为 **24 小时**。建议在获取链接后立即下载并转存至永久存储(如[阿里云 OSS](https://help.aliyun.com/zh/oss/user-guide/what-is-oss))。 + +- **RPS 限制**:查询接口默认RPS为20。如需更高频查询或事件通知,建议[配置异步任务回调](https://help.aliyun.com/zh/model-studio/async-task-api)。 + +- **更多操作**:如需批量查询、取消任务等操作,请参见[管理异步任务](https://help.aliyun.com/zh/model-studio/manage-asynchronous-tasks#f26499d72adsl)。 + + +| #### 请求参数 | ## 查询任务结果 将`{task_id}`完整替换为上一步接口返回的`task_id`的值。`task_id`查询有效期为24小时,并请将`{WorkspaceId}`替换为真实的[业务空间ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 ``` curl -X GET https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/tasks/{task_id} \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" ``` | +| --- | --- | +| ##### **请求头(Headers)** | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| ##### **URL路径参数(Path parameters)** | +| **task\\_id** `*string*`**(必选)** 任务ID。 | + +| #### 响应参数 | ## 任务执行成功 任务数据(如任务状态、图像URL等)仅保留24小时,超时后会被自动清除。请您务必及时保存生成的图像。 ``` { "request_id": "2ddf53fa-699a-4267-9446-xxxxxx", "output": { "task_id": "3cd3fa4e-53ee-4136-9cab-xxxxxx", "task_status": "SUCCEEDED", "submit_time": "2025-12-18 20:03:01.802", "scheduled_time": "2025-12-18 20:03:01.834", "end_time": "2025-12-18 20:03:29.260", "finished": true, "choices": [ { "finish_reason": "stop", "message": { "role": "assistant", "content": [ { "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxx.png?Expires=xxx", "type": "image" } ] } } ] }, "usage": { "size": "1280*1280", "total_tokens": 0, "image_count": 1, "output_tokens": 0, "input_tokens": 0 } } ``` ## 任务执行异常 如果因为某种原因导致任务执行失败,将返回相关信息,可以通过code和message字段明确指示错误原因。请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "code": "InvalidApiKey", "message": "No API-key provided.", "request_id": "7438d53d-6eb8-4596-8835-xxxxxx" } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 **轮询过程中的状态流转:** - PENDING(排队中) → RUNNING(处理中)→ SUCCEEDED(成功)/ FAILED(失败)。 - 初次查询状态通常为 PENDING(排队中)或 RUNNING(处理中)。 - 当状态变为 SUCCEEDED 时,响应中将包含生成的图像URL。 - 若状态为 FAILED,请检查错误信息并重试。 **submit\\_time** `*string*` 任务提交时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **scheduled\\_time** `*string*` 任务执行时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **end\\_time** `*string*` 任务完成时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **finished** `*boolean*` 任务是否结束。 - true:已结束。 - false:未结束。 **choices** `*array*` 模型生成的输出内容。 **属性** **finish\\_reason** `*string*` 任务停止原因,正常完成时为 `stop`。 **message** `*object*` 模型返回的消息。 **属性** **role** `*string*` 消息的角色,固定为`assistant`。 **content** `*array*` **属性** **image** `*string*` 生成图像的 URL,图像格式为PNG。 **链接有效期为24小时**,请及时下载并保存图像。 **type** `*string*` 输出的类型,固定为image。 | +| **usage** `*object*` 输出信息统计。**只对成功的结果计数。** **属性** **image\\_count** `*integer*` 生成图像的张数。 **size** `*string*` 生成的图像分辨率。示例值:1280\\*1280。 **input\\_tokens** `*integer*` 输入token数量。当前固定为0。 **output\\_tokens** `*integer*` 输出token数量。当前固定为0。 **total\\_tokens** `*integer*` 总token数量。当前固定为0。 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | +| **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | +| **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | + +## **HTTP异步调用(wan2.5及以下版本模型)** + +**重要** + +此接口为**旧版协议**,仅支持**wan2.5及以下版本模型**。 + +由于文生图任务耗时较长(通常为1-2分钟),API采用异步调用。整个流程包含 **“创建任务 -> 轮询获取”** 两个核心步骤,具体如下: + +> 具体耗时受限于排队任务数和服务执行情况,请在获取结果时耐心等待。 + +### 步骤1:创建任务获取任务ID + +## 北京 + +`POST https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis` + +## 新加坡 + +`POST https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis` + +**说明** + +- 创建成功后,使用接口返回的 `task_id` 查询结果,task\_id 有效期为 24 小时。**请勿重复创建任务**,轮询获取即可。 + +- 新手指引请参见[Postman](https://help.aliyun.com/zh/model-studio/first-call-to-image-and-video-api)。 + + +| #### 请求参数 | ## 文生图 ``` curl -X POST https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis \\ -H 'X-DashScope-Async: enable' \\ -H "Authorization: Bearer $DASHSCOPE_API_KEY" \\ -H 'Content-Type: application/json' \\ -d '{ "model": "wan2.5-t2i-preview", "input": { "prompt": "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵" }, "parameters": { "size": "1280*1280", "n": 1 } }' ``` ## 文生图(使用反向提示词) 通过 negative\\_prompt 指定生成的图片避免出现“人物”元素。 ``` curl -X POST https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis \\ -H 'X-DashScope-Async: enable' \\ -H "Authorization: Bearer $DASHSCOPE_API_KEY" \\ -H 'Content-Type: application/json' \\ -d '{ "model": "wan2.2-t2i-flash", "input": { "prompt": "雪地,白色小教堂,极光,冬日场景,柔和的光线。", "negative_prompt": "人物" }, "parameters": { "size": "1024*1024", "n": 1 } }' ``` | +| --- | --- | +| ##### 请求头(Headers) | +| **Content-Type** `*string*` **(必选)** 请求内容类型。此参数必须设置为`application/json`。 | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| **X-DashScope-Async** `*string*` **(必选)** 异步处理配置参数。HTTP请求只支持异步,**必须设置为**`**enable**`。 **重要** 缺少此请求头将报错:“current user api does not support synchronous calls”。 | +| ##### 请求体(Request Body) | +| **model** `*string*` (必选) 模型名称。文生图模型请参见[模型列表](https://help.aliyun.com/zh/model-studio/wanx-image-edit-api-reference)。 示例值:wan2.5-t2i-preview。 **说明** wan2.6模型的HTTP调用请参见[HTTP同步调用](#39337f0fd0wzt)、[HTTP异步调用](#9c68ccc525nta)。 | +| **input** `*object*` (必选) 输入的基本信息,如提示词等。 **属性** **prompt** `*string*` **(必选)** 正向提示词,用来描述生成图像中期望包含的元素和视觉特点。 支持中英文,每个汉字/字母/标点符号占一个字符,超过部分会自动截断。长度限制因模型版本而异: - wan2.5-t2i-preview:长度不超过2000个字符。 - wan2.2、wan2.1系列模型:长度不超过500个字符。 - wanx2.0-t2i-turbo:长度不超过800个字符。 示例值:一只坐着的橘黄色的猫,表情愉悦,活泼可爱,逼真准确。 提示词的使用技巧请参见[文生图Prompt指南](https://help.aliyun.com/zh/model-studio/text-to-image-prompt)。 **negative\\_prompt** `*string*` (可选) 反向提示词,用来描述不希望在画面中看到的内容,可以对画面进行限制。 支持中英文,长度不超过500个字符,超过部分会自动截断。 示例值:低分辨率、错误、最差质量、低质量、残缺、多余的手指、比例不良等。 | +| **parameters** `*object*` (可选) 图像处理参数。如设置图像分辨率、开启prompt智能改写、添加水印等。 **属性** **size** `*string*` (可选) 输出图像的分辨率,格式为`**宽*高**`。默认值和约束因模型版本而异: - wan2.5-t2i-preview:默认值为 `1280*1280`。总像素在 \\[1280\\*1280, 1440\\*1440\\] 之间且宽高比范围为 \\[1:4, 4:1\\]。例如,768\\*2700符合要求。 - wan2.2及以下版本模型:默认值为`1024*1024`。图像宽高在\\[512, 1440\\]之间,最大分辨率为1440\\*1440。例如, 768\\*2700超单边限制,不支持。 示例值:1280\\*1280。 **常见比例推荐的分辨率** 以下分辨率适用于wan2.5-t2i-preview - 1:1:1280\\*1280 - 3:4:1104\\*1472 - 4:3:1472\\*1104 - 9:16:960\\*1696 - 16:9:1696\\*960 **n** `*integer*` (可选) **重要** n直接影响费用。费用 = 单价 × 图片张数,请在调用前确认[模型价格](https://help.aliyun.com/zh/model-studio/model-pricing#0006b52b83ua9)。 生成图片的数量。取值范围为1~4张,默认为`4`。测试阶段建议设置为1,便于低成本验证。 **prompt\\_extend** `*boolean*` (可选) 是否开启prompt智能改写。开启后使用大模型对输入prompt进行智能改写。对于较短的prompt生成效果提升明显,但会增加耗时。 - true:默认值,开启智能改写。 - false:关闭智能改写。 示例值:true。 **watermark** `*boolean*` (可选) 是否添加水印标识,水印位于图片右下角,文案固定为“AI生成”。 - false:默认值,不添加水印。 - true:添加水印。 **seed** `*integer*` (可选) 随机数种子,取值范围`[0,2147483647]`。 使用相同的`seed`参数值可使生成内容保持相对稳定。若不提供,算法将自动使用随机数种子。 **注意**:模型生成过程具有概率性,即使使用相同的`seed`,也不能保证每次生成结果完全一致。 | + +| #### 响应参数 | ## 成功响应 请保存 task\\_id,用于查询任务状态与结果。 ``` { "output": { "task_status": "PENDING", "task_id": "0385dc79-5ff8-4d82-bcb6-xxxxxx" }, "request_id": "4909100c-7b5a-9f92-bfe5-xxxxxx" } ``` ## 异常响应 创建任务失败,请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "code": "InvalidApiKey", "message": "No API-key provided.", "request_id": "7438d53d-6eb8-4596-8835-xxxxxx" } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | +| **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | +| **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | + +### 步骤2:根据任务ID查询结果 + +#### **华北2(北京)** + +`GET https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/tasks/{task_id}` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +#### 新加坡 + +`GET https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/tasks/{task_id}` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +**说明** + +- **轮询建议**:图像生成过程耗时较长,建议采用**轮询**机制,并设置合理的查询间隔(如 10 秒)来获取结果。 + +- **任务状态流转**:PENDING(排队中)→ RUNNING(处理中)→ SUCCEEDED(成功)/ FAILED(失败)。 + +- **结果链接**:任务成功后返回图像链接,有效期为 **24 小时**。建议在获取链接后立即下载并转存至永久存储(如[阿里云 OSS](https://help.aliyun.com/zh/oss/user-guide/what-is-oss))。 + +- **RPS 限制**:查询接口默认RPS为20。如需更高频查询或事件通知,建议[配置异步任务回调](https://help.aliyun.com/zh/model-studio/async-task-api)。 + +- **更多操作**:如需批量查询、取消任务等操作,请参见[管理异步任务](https://help.aliyun.com/zh/model-studio/manage-asynchronous-tasks#f26499d72adsl)。 + + +| #### 请求参数 | ## 查询任务结果 请将`86ecf553-d340-4e21-xxxxxxxxx`替换为真实的task\\_id。 > 若使用新加坡地域的模型,需将base\\_url替换为https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/tasks/86ecf553-d340-4e21-xxxxxxxxx,其中{WorkspaceId}需替换为真实的业务空间ID。 ``` curl -X GET https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/tasks/86ecf553-d340-4e21-xxxxxxxxx \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" ``` | +| --- | --- | +| ##### **请求头(Headers)** | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| ##### **URL路径参数(Path parameters)** | +| **task\\_id** `*string*`**(必选)** 任务ID。 | + +| #### **响应参数** | ## 任务执行成功 图像URL仅保留24小时,超时后会被自动清除,请及时保存生成的图像。 ``` { "request_id": "f767d108-7d50-908b-a6d9-xxxxxx", "output": { "task_id": "d492bffd-10b5-4169-b639-xxxxxx", "task_status": "SUCCEEDED", "submit_time": "2025-01-08 16:03:59.840", "scheduled_time": "2025-01-08 16:03:59.863", "end_time": "2025-01-08 16:04:10.660", "results": [ { "orig_prompt": "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵", "actual_prompt": "一间有着精致雕花窗户的花店,漂亮的深色木质门上挂着铜制把手。店内摆放着各式各样的鲜花,包括玫瑰、百合和向日葵,色彩鲜艳,生机勃勃。背景是温馨的室内场景,透过窗户可以看到街道。高清写实摄影,中景构图。", "url": "https://dashscope-result-wlcb.oss-cn-wulanchabu.aliyuncs.com/1.png" } ], "task_metrics": { "TOTAL": 1, "SUCCEEDED": 1, "FAILED": 0 } }, "usage": { "image_count": 1 } } ``` ## 任务执行失败 若任务执行失败,task\\_status将置为 FAILED,并提供错误码和信息。请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "request_id": "e5d70b02-ebd3-98ce-9fe8-759d7d7b107d", "output": { "task_id": "86ecf553-d340-4e21-af6e-xxxxxx", "task_status": "FAILED", "code": "InvalidParameter", "message": "xxxxxx", "task_metrics": { "TOTAL": 4, "SUCCEEDED": 0, "FAILED": 4 } } } ``` ## 任务部分失败 模型可以在一次任务中生成多张图片。只要有一张图片生成成功,任务状态将标记为`SUCCEEDED`,并且返回相应的图像URL。对于生成失败的图片,结果中会返回相应的失败原因。同时在usage统计中,只会对成功的结果计数。请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "request_id": "85eaba38-0185-99d7-8d16-xxxxxx", "output": { "task_id": "86ecf553-d340-4e21-af6e-xxxxxx", "task_status": "SUCCEEDED", "results": [ { "url": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/123/a1.png" }, { "code": "InternalError.Timeout", "message": "An internal timeout error has occurred during execution, please try again later or contact service support." } ], "task_metrics": { "TOTAL": 2, "SUCCEEDED": 1, "FAILED": 1 } }, "usage": { "image_count": 1 } } ``` ## 任务查询过期 task\\_id查询有效期为 24 小时,超时后将无法查询,返回以下报错信息。 ``` { "request_id": "a4de7c32-7057-9f82-8581-xxxxxx", "output": { "task_id": "502a00b1-19d9-4839-a82f-xxxxxx", "task_status": "UNKNOWN" } } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 **轮询过程中的状态流转:** - PENDING(排队中) → RUNNING(处理中)→ SUCCEEDED(成功)/ FAILED(失败)。 - 初次查询状态通常为 PENDING(排队中)或 RUNNING(处理中)。 - 当状态变为 SUCCEEDED 时,响应中将包含生成的图像URL。 - 若状态为 FAILED,请检查错误信息并重试。 **submit\\_time** `*string*` 任务提交时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **scheduled\\_time** `*string*` 任务执行时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **end\\_time** `*string*` 任务完成时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **results** `*array of object*` 任务结果列表,包括图像URL、prompt、部分任务执行失败报错信息等。 **数据结构** ``` { "results": [ { "orig_prompt": "", "actual_prompt": "", "url": "" }, { "code": "", "message": "" } ] } ``` **属性** **orig\\_prompt** `*string*` 原始输入的prompt,对应请求参数`prompt`。 **actual\\_prompt** `*string*` 开启 prompt 智能改写后,返回实际使用的优化后 prompt。若未开启该功能,则不返回此字段。 **url** `*string*` 图像URL地址。仅在 task\\_status 为 SUCCEEDED 时返回。链接有效期24小时,可通过此URL下载图像。 **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 **task\\_metrics** `*object*` 任务结果统计。 **属性** **TOTAL** `*integer*` 总的任务数。 **SUCCEEDED** `*integer*` 任务状态为成功的任务数。 **FAILED** `*integer*` 任务状态为失败的任务数。 **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | +| **usage** `*object*` 输出信息统计。只对成功的结果计数。 **属性** **image\\_count** `*integer*` 模型成功生成图片的数量。计费公式:费用 = 图片数量 × 单价。 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | + +## **DashScope Python SDK调用** + +SDK 的参数命名与HTTP接口基本一致,参数结构根据语言特性进行封装。 + +由于文生图任务耗时较长,SDK 在底层封装了 HTTP 异步调用流程,支持同步、异步两种调用方式。 + +> 具体耗时受限于排队任务数和服务执行情况,请在获取结果时耐心等待。 + +### **wan2.6** + +**重要** + +- 以下代码仅适合 **wan2.6** 模型。 + +- 请确保 DashScope Python SDK 版本**不低于** `**1.25.7**`,再运行以下代码。更新请参考[安装SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。 + + +各地域的`base_url`和 API Key 不通用,以下示例以北京地域为例进行调用: + +#### **华北2(北京)** + +`https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +#### 新加坡 + +`https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +#### 美国(弗吉尼亚) + +`https://dashscope-us.aliyuncs.com/api/v1` + +#### 德国(法兰克福) + +`https://{WorkspaceId}.eu-central-1.maas.aliyuncs.com/api/v1` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +> 全球部署范围(法兰克福地域)仅支持异步调用。 + +## 同步调用 + +##### **请求示例** + +``` +import os +import dashscope +from dashscope.aigc.image_generation import ImageGeneration +from dashscope.api_entities.dashscope_response import Message + +# 以下为华北2(北京)地域的URL。请将 {WorkspaceId} 替换为您的百炼业务空间ID,各地域的URL不同。 +dashscope.base_http_api_url = 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1' + +# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx" +# 各地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +api_key = os.getenv("DASHSCOPE_API_KEY") + +message = Message( + role="user", + content=[ + { + 'text': '一间有着精致窗户的花店,漂亮的木质门,摆放着花朵' + } + ] +) +print("----sync call, please wait a moment----") +rsp = ImageGeneration.call( + model="wan2.6-t2i", + api_key=api_key, + messages=[message], + negative_prompt="", + prompt_extend=True, + watermark=False, + n=1, + size="1280*1280" +) +print(rsp) +``` + +##### 响应示例 + +> url 有效期24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "820dd0db-eb42-4e05-8d6a-1ddb4axxxxxx", + "code": "", + "message": "", + "output": { + "text": null, + "finish_reason": null, + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + } + ] + } + } + ], + "audio": null, + "finished": true + }, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "characters": 0, + "image_count": 1, + "size": "1280*1280", + "total_tokens": 0 + } +} +``` + +## 异步调用 + +##### **请求示例** + +``` +import os +import dashscope +from dashscope.aigc.image_generation import ImageGeneration +from dashscope.api_entities.dashscope_response import Role, Message +from http import HTTPStatus + +# 以下为华北2(北京)地域的URL。请将 {WorkspaceId} 替换为您的百炼业务空间ID,各地域的URL不同。 +dashscope.base_http_api_url = 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1' + +# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx" +# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +api_key = os.getenv("DASHSCOPE_API_KEY") + +# 创建异步任务 +def create_async_task(): + print("Creating async task...") + message = Message( + role="user", + content=[{'text': '一间有着精致窗户的花店,漂亮的木质门,摆放着花朵'}] + ) + response = ImageGeneration.async_call( + model="wan2.6-t2i", + api_key=api_key, + messages=[message], + negative_prompt="", + prompt_extend=True, + watermark=False, + n=1, + size="1280*1280" + ) + + if response.status_code == 200: + print("Task created successfully:", response) + return response + else: + raise Exception(f"Failed to create task: {response.code} - {response.message}") + +# 等待任务完成 +def wait_for_completion(task_response): + print("Waiting for task completion...") + status = ImageGeneration.wait(task=task_response, api_key=api_key) + + if status.output.task_status == "SUCCEEDED": + print("Task succeeded!") + print("Response:", status) + else: + raise Exception(f"Task failed with status: {status.output.task_status}") + +# 获取异步任务信息 +def fetch_task_status(task): + print("Fetching task status...") + status = ImageGeneration.fetch(task=task, api_key=api_key) + + if status.status_code == HTTPStatus.OK: + print("Task status:", status.output.task_status) + print("Response details:", status) + else: + print(f"Failed to fetch status: {status.code} - {status.message}") + +# 取消异步任务 +def cancel_task(task): + print("Canceling task...") + response = ImageGeneration.cancel(task=task, api_key=api_key) + + if response.status_code == HTTPStatus.OK: + print("Task canceled successfully:", response.output.task_status) + else: + print(f"Failed to cancel task: {response.code} - {response.message}") + +# 主执行流程 +if __name__ == "__main__": + task = create_async_task() + wait_for_completion(task) +``` + +##### 响应示例 + +1、创建任务的响应示例 + +``` +{ + "status_code": 200, + "request_id": "c4f11410-ea42-4996-957d-9c82f9xxxxxx", + "code": "", + "message": "", + "output": { + "text": null, + "finish_reason": null, + "choices": null, + "audio": null, + "task_id": "f470bbfd-d955-4165-935b-d35b8eexxxxxx", + "task_status": "PENDING" + }, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "characters": 0 + } +} +``` + +2、查询任务结果的响应示例 + +> url 有效期24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "7e57e7e8-00b0-4534-9aff-fe31e0xxxxxx", + "code": null, + "message": "", + "output": { + "text": null, + "finish_reason": null, + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxxxxx.png?Expires=xxxxxx", + "type": "image" + } + ] + } + } + ], + "audio": null, + "task_id": "f470bbfd-d955-4165-935b-d35b8exxxxxx", + "task_status": "SUCCEEDED", + "submit_time": "2026-01-09 17:18:17.901", + "scheduled_time": "2026-01-09 17:18:17.941", + "end_time": "2026-01-09 17:18:45.544", + "finished": true + }, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "characters": 0, + "size": "1280*1280", + "total_tokens": 0, + "image_count": 1 + } +} +``` + +### **wan2.5及以下版本模型** + +**重要** + +- 以下代码仅适合wan2.5及以下版本模型。 + +- 请确保 DashScope Python SDK 版本**不低于** `**1.25.2**`,再运行以下代码。 + + 若版本过低,可能会触发 “url error, please check url!” 等错误。请参考[安装SDK](https://help.aliyun.com/zh/model-studio/install-sdk)进行更新。 + + +各地域的`base_url`和 API Key 不通用,以下示例以北京地域为例进行调用: + +#### 华北2(北京) + +`https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +#### 新加坡 + +`https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +## 同步调用 + +##### **请求示例** + +``` +from http import HTTPStatus +from urllib.parse import urlparse, unquote +from pathlib import PurePosixPath +import requests +from dashscope import ImageSynthesis +import os +import dashscope + +# 以下为华北2(北京)地域的URL。请将 {WorkspaceId} 替换为您的百炼业务空间ID,各地域的URL不同。 +dashscope.base_http_api_url = 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1' + +# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx" +# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +api_key = os.getenv("DASHSCOPE_API_KEY") + +print('----sync call, please wait a moment----') +rsp = ImageSynthesis.call(api_key=api_key, + model="wan2.5-t2i-preview", + prompt="一间有着精致窗户的花店,漂亮的木质门,摆放着花朵", + negative_prompt="", + n=1, + size='1280*1280', + prompt_extend=True, + watermark=False, + seed=12345) +print('response: %s' % rsp) +if rsp.status_code == HTTPStatus.OK: + # 在当前目录下保存图片 + for result in rsp.output.results: + file_name = PurePosixPath(unquote(urlparse(result.url).path)).parts[-1] + with open('./%s' % file_name, 'wb+') as f: + f.write(requests.get(result.url).content) +else: + print('sync_call Failed, status_code: %s, code: %s, message: %s' % + (rsp.status_code, rsp.code, rsp.message)) +``` + +##### 响应示例 + +> url 有效期24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "9d634fda-5fe9-9968-a908-xxxxxx", + "code": null, + "message": "", + "output": { + "task_id": "d35658e4-483f-453b-b8dc-xxxxxx", + "task_status": "SUCCEEDED", + "results": [{ + "url": "https://dashscope-result-wlcb.oss-cn-wulanchabu.aliyuncs.com/1.png", + "orig_prompt": "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵", + "actual_prompt": "一间精致的花店,窗户上装饰着优雅的雕花,漂亮的木质门上挂着铜制把手。店内摆放着各种色彩鲜艳的花朵,如玫瑰、郁金香和百合等。背景是温馨的室内场景,光线柔和,营造出宁静舒适的氛围。高清写实摄影,近景中心构图。" + }], + "submit_time": "2025-01-08 19:36:01.521", + "scheduled_time": "2025-01-08 19:36:01.542", + "end_time": "2025-01-08 19:36:13.270", + "task_metrics": { + "TOTAL": 1, + "SUCCEEDED": 1, + "FAILED": 0 + } + }, + "usage": { + "image_count": 1 + } +} +``` + +## 异步调用 + +##### 请求示例 + +``` +from http import HTTPStatus +from urllib.parse import urlparse, unquote +from pathlib import PurePosixPath +import requests +from dashscope import ImageSynthesis +import os +import dashscope + +# 以下为华北2(北京)地域的URL。请将 {WorkspaceId} 替换为您的百炼业务空间ID,各地域的URL不同。 +dashscope.base_http_api_url = 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1' + +# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx" +# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +api_key = os.getenv("DASHSCOPE_API_KEY") + + +def async_call(): + print('----create task----') + task_info = create_async_task() + print('----wait task done then save image----') + wait_async_task(task_info) + + +# 创建异步任务 +def create_async_task(): + rsp = ImageSynthesis.async_call(api_key=api_key, + model="wan2.5-t2i-preview", + prompt="一间有着精致窗户的花店,漂亮的木质门,摆放着花朵", + negative_prompt="", + n=1, + size='1280*1280', + prompt_extend=True, + watermark=False, + seed=12345) + print(rsp) + if rsp.status_code == HTTPStatus.OK: + print(rsp.output) + else: + print('Failed, status_code: %s, code: %s, message: %s' % + (rsp.status_code, rsp.code, rsp.message)) + return rsp + + +# 等待异步任务结束 +def wait_async_task(task): + rsp = ImageSynthesis.wait(task=task, api_key=api_key) + print(rsp) + if rsp.status_code == HTTPStatus.OK: + print(rsp.output) + # save file to current directory + for result in rsp.output.results: + file_name = PurePosixPath(unquote(urlparse(result.url).path)).parts[-1] + with open('./%s' % file_name, 'wb+') as f: + f.write(requests.get(result.url).content) + else: + print('Failed, status_code: %s, code: %s, message: %s' % + (rsp.status_code, rsp.code, rsp.message)) + + +# 获取异步任务信息 +def fetch_task_status(task): + status = ImageSynthesis.fetch(task=task, api_key=api_key) + print(status) + if status.status_code == HTTPStatus.OK: + print(status.output.task_status) + else: + print('Failed, status_code: %s, code: %s, message: %s' % + (status.status_code, status.code, status.message)) + + +# 取消异步任务,只有处于PENDING状态的任务才可以取消 +def cancel_task(task): + rsp = ImageSynthesis.cancel(task=task, api_key=api_key) + print(rsp) + if rsp.status_code == HTTPStatus.OK: + print(rsp.output.task_status) + else: + print('Failed, status_code: %s, code: %s, message: %s' % + (rsp.status_code, rsp.code, rsp.message)) + + +if __name__ == '__main__': + async_call() +``` + +##### 响应示例 + +1、创建任务的响应示例 + +``` +{ + "status_code": 200, + "request_id": "31b04171-011c-96bd-ac00-f0383b669cc7", + "code": "", + "message": "", + "output": { + "task_id": "4f90cf14-a34e-4eae-xxxxxxxx", + "task_status": "PENDING", + "results": [] + }, + "usage": null +} +``` + +2、查询任务结果的响应示例 + +> url 有效期24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "9d634fda-5fe9-9968-a908-xxxxxx", + "code": null, + "message": "", + "output": { + "task_id": "d35658e4-483f-453b-b8dc-xxxxxx", + "task_status": "SUCCEEDED", + "results": [{ + "url": "https://dashscope-result-wlcb.oss-cn-wulanchabu.aliyuncs.com/xxx.png", + "orig_prompt": "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵", + "actual_prompt": "一间精致的花店,窗户上装饰着优雅的雕花,漂亮的木质门上挂着铜制把手。店内摆放着各种色彩鲜艳的花朵,如玫瑰、郁金香和百合等。背景是温馨的室内场景,光线柔和,营造出宁静舒适的氛围。高清写实摄影,近景中心构图。" + }], + "submit_time": "2025-01-08 19:36:01.521", + "scheduled_time": "2025-01-08 19:36:01.542", + "end_time": "2025-01-08 19:36:13.270", + "task_metrics": { + "TOTAL": 1, + "SUCCEEDED": 1, + "FAILED": 0 + } + }, + "usage": { + "image_count": 1 + } +} +``` + +## DashScope Java SDK调用 + +SDK 的参数命名与HTTP接口基本一致,参数结构根据语言特性进行封装。 + +由于文生图任务耗时较长,SDK 在底层封装了 HTTP 异步调用流程,支持同步、异步两种调用方式。 + +> 具体耗时受限于排队任务数和服务执行情况,请在获取结果时耐心等待。 + +### **wan2.6** + +**重要** + +- 以下代码仅适合 **wan2.6-t2i** 模型。 + +- 请确保 DashScope Java SDK 版本**不低于** `**2.22.6**`,再运行以下代码。 + + +各地域的`base_url`和 API Key 不通用,以下示例以北京地域为例进行调用: + +#### **华北2(北京)** + +`https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +#### 新加坡 + +`https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +#### 美国(弗吉尼亚) + +`https://dashscope-us.aliyuncs.com/api/v1` + +#### 德国(法兰克福) + +`https://{WorkspaceId}.eu-central-1.maas.aliyuncs.com/api/v1` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +> 全球部署范围(法兰克福地域)仅支持异步调用。 + +## 同步调用 + +##### **请求示例** + +``` +import com.alibaba.dashscope.aigc.imagegeneration.*; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.exception.UploadFileException; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; +import java.util.Collections; + +public class Main { + + static { + // 以下为北京地域url,各地域的base_url不同 + Constants.baseHttpApiUrl = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"; + } + + // 若没有配置环境变量,请用百炼API Key将下行替换为:apiKey="sk-xxx" + // 各地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + static String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + public static void basicCall() throws ApiException, NoApiKeyException, UploadFileException { + ImageGenerationMessage message = ImageGenerationMessage.builder() + .role("user") + .content(Collections.singletonList( + Collections.singletonMap("text", "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵") + )).build(); + + ImageGenerationParam param = ImageGenerationParam.builder() + .apiKey(apiKey) + .model("wan2.6-t2i") + .n(1) + .size("1280*1280") + .negativePrompt("") + .promptExtend(true) + .watermark(false) + .messages(Collections.singletonList(message)) + .build(); + + ImageGeneration imageGeneration = new ImageGeneration(); + ImageGenerationResult result = null; + try { + System.out.println("---sync call, please wait a moment----"); + result = imageGeneration.call(param); + } catch (ApiException | NoApiKeyException | UploadFileException e) { + throw new RuntimeException(e.getMessage()); + } + System.out.println(JsonUtils.toJson(result)); + } + + public static void main(String[] args) { + try { + basicCall(); + } catch (ApiException | NoApiKeyException | UploadFileException e) { + System.out.println(e.getMessage()); + } + } +} +``` + +##### 响应示例 + +> url 有效期24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "50b57166-eaaa-4f17-b1e0-35a5ca88672c", + "code": "", + "message": "", + "output": { + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-sh.oss-cn-shanghai.aliyuncs.com/xxx.png?Expires=xxx", + "type": "image" + } + ] + } + } + ], + "finished": true + }, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "image_count": 1, + "size": "1280*1280", + "total_tokens": 0 + } +} +``` + +## 异步调用 + +##### **请求示例** + +``` +import com.alibaba.dashscope.aigc.imagegeneration.*; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.exception.UploadFileException; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; +import java.util.Collections; + +public class Main { + + static { + // 以下为北京地域url,各地域的base_url不同 + Constants.baseHttpApiUrl = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"; + } + + // 若没有配置环境变量,请用百炼API Key将下行替换为:apiKey="sk-xxx" + // 各地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + static String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + public static void asyncCall() throws ApiException, NoApiKeyException, UploadFileException { + ImageGenerationMessage message = ImageGenerationMessage.builder() + .role("user") + .content(Collections.singletonList( + Collections.singletonMap("text", "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵") + )).build(); + + ImageGenerationParam param = ImageGenerationParam.builder() + .apiKey(apiKey) + .model("wan2.6-t2i") + .n(1) + .size("1280*1280") + .negativePrompt("") + .promptExtend(true) + .watermark(false) + .messages(Collections.singletonList(message)) + .build(); + + ImageGeneration imageGeneration = new ImageGeneration(); + ImageGenerationResult result = null; + try { + System.out.println("---async call, creating task----"); + result = imageGeneration.asyncCall(param); + } catch (ApiException | NoApiKeyException | UploadFileException e) { + throw new RuntimeException(e.getMessage()); + } + System.out.println(JsonUtils.toJson(result)); + + String taskId = result.getOutput().getTaskId(); + // 等待任务完成 + waitTask(taskId); + } + + public static void waitTask(String taskId) throws ApiException, NoApiKeyException { + ImageGeneration imageGeneration = new ImageGeneration(); + ImageGenerationResult result = imageGeneration.wait(taskId, apiKey); + System.out.println(JsonUtils.toJson(result)); + } + + public static void main(String[] args) { + try { + asyncCall(); + } catch (ApiException | NoApiKeyException | UploadFileException e) { + System.out.println(e.getMessage()); + } + } +} +``` + +##### 响应示例 + +1、创建任务的响应示例 + +``` +{ + "status_code": 200, + "request_id": "9cd85950-2e26-4b2c-b562-1694cf9288e5", + "code": "", + "message": "", + "output": { + "task_id": "4c861fbe-af89-4a2f-8fc5-4bb15c3139ba", + "task_status": "PENDING" + }, + "usage": null +} +``` + +2、查询任务结果的响应示例 + +> url 有效期24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "cbdf1424-306e-4a52-82f3-8bf5d8a99103", + "code": "", + "message": "", + "output": { + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-sh.oss-cn-shanghai.aliyuncs.com/xxx.png?Expires=xxx", + "type": "image" + } + ] + } + } + ], + "task_id": "4c861fbe-af89-4a2f-8fc5-4bb15c3139ba", + "task_status": "SUCCEEDED", + "submit_time": "2026-01-16 16:36:06.556", + "scheduled_time": "2026-01-16 16:36:06.591", + "end_time": "2026-01-16 16:36:25.190", + "finished": true + }, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "size": "1280*1280", + "total_tokens": 0, + "image_count": 1 + } +} +``` + +### wan2.5及以下版本模型 + +**重要** + +- 以下代码仅适合wan2.5及以下版本模型。 + +- 请确保 DashScope Java SDK 版本**不低于** `**2.22.2**`,再运行以下代码。 + + 若版本过低,可能会触发 “url error, please check url!” 等错误。请参考[安装SDK](https://help.aliyun.com/zh/model-studio/install-sdk)进行更新。 + + +各地域的`base_url`和 API Key 不通用,以下示例以北京地域为例进行调用: + +#### 华北2(北京) + +`https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +#### 新加坡 + +`https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +## 同步调用 + +##### 请求示例 + +``` +// Copyright (c) Alibaba, Inc. and its affiliates. + +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisListResult; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; +import com.alibaba.dashscope.task.AsyncTaskListParam; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; + +import java.util.HashMap; +import java.util.Map; + +public class Main { + + static { + // 以下为华北2(北京)地域的URL。请将 {WorkspaceId} 替换为您的百炼业务空间ID,各地域的URL不同。 + Constants.baseHttpApiUrl = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"; + } + + // 若没有配置环境变量,请用百炼API Key将下行替换为:apiKey="sk-xxx" + // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + static String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + + public static void basicCall() throws ApiException, NoApiKeyException { + // 设置parameters参数 + Map parameters = new HashMap<>(); + parameters.put("prompt_extend", true); + parameters.put("watermark", false); + parameters.put("seed", 12345); + + ImageSynthesisParam param = + ImageSynthesisParam.builder() + .apiKey(apiKey) + .model("wan2.5-t2i-preview") + .prompt("一间有着精致窗户的花店,漂亮的木质门,摆放着花朵") + .n(1) + .size("1280*1280") + .negativePrompt("") + .parameters(parameters) + .build(); + + ImageSynthesis imageSynthesis = new ImageSynthesis(); + ImageSynthesisResult result = null; + try { + System.out.println("---sync call, please wait a moment----"); + result = imageSynthesis.call(param); + } catch (ApiException | NoApiKeyException e){ + throw new RuntimeException(e.getMessage()); + } + System.out.println(JsonUtils.toJson(result)); + } + + public static void listTask() throws ApiException, NoApiKeyException { + ImageSynthesis is = new ImageSynthesis(); + AsyncTaskListParam param = AsyncTaskListParam.builder().build(); + param.setApiKey(apiKey); + ImageSynthesisListResult result = is.list(param); + System.out.println(result); + } + + public static void fetchTask(String taskId) throws ApiException, NoApiKeyException { + ImageSynthesis is = new ImageSynthesis(); + // If set DASHSCOPE_API_KEY environment variable, apiKey can null. + ImageSynthesisResult result = is.fetch(taskId, apiKey); + System.out.println(result.getOutput()); + System.out.println(result.getUsage()); + } + + public static void main(String[] args){ + try{ + basicCall(); + //listTask(); + }catch(ApiException|NoApiKeyException e){ + System.out.println(e.getMessage()); + } + } +} +``` + +##### 响应示例 + +> url 有效期24小时,请及时下载图像。 + +``` +{ + "request_id": "22f9c744-206c-9a78-899a-xxxxxx", + "output": { + "task_id": "4a0f8fc6-03fb-4c44-a13a-xxxxxx", + "task_status": "SUCCEEDED", + "results": [{ + "orig_prompt": "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵", + "actual_prompt": "一间有着精致雕花窗户的花店,漂亮的深色木质门微微敞开。店内摆放着各式各样的鲜花,包括玫瑰、百合和向日葵,色彩鲜艳,香气扑鼻。背景是温馨的室内场景,光线柔和,透过窗户洒在花朵上。高清写实摄影,中景构图。", + "url": "https://dashscope-result-wlcb.oss-cn-wulanchabu.aliyuncs.com/1.png" + }], + "task_metrics": { + "TOTAL": 1, + "SUCCEEDED": 1, + "FAILED": 0 + } + }, + "usage": { + "image_count": 1 + } +} +``` + +## 异步调用 + +##### 请求示例 + +``` +// Copyright (c) Alibaba, Inc. and its affiliates. + +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; + +import java.util.HashMap; +import java.util.Map; + +public class Main { + + static { + // 以下为华北2(北京)地域的URL。请将 {WorkspaceId} 替换为您的百炼业务空间ID,各地域的URL不同。 + Constants.baseHttpApiUrl = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"; + } + + // 若没有配置环境变量,请用百炼API Key将下行替换为:apiKey="sk-xxx" + // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + public void asyncCall() { + System.out.println("---create task----"); + String taskId = this.createAsyncTask(); + System.out.println("---wait task done then return image url----"); + this.waitAsyncTask(taskId); + } + + + /** + * 创建异步任务 + * @return taskId + */ + public String createAsyncTask() { + // 设置parameters参数 + Map parameters = new HashMap<>(); + parameters.put("prompt_extend", true); + parameters.put("watermark", false); + parameters.put("seed", 12345); + + ImageSynthesisParam param = + ImageSynthesisParam.builder() + .apiKey(apiKey) + .model("wan2.5-t2i-preview") + .prompt("一间有着精致窗户的花店,漂亮的木质门,摆放着花朵") + .n(1) + .size("1280*1280") + .negativePrompt("") + .parameters(parameters) + .build(); + + ImageSynthesis imageSynthesis = new ImageSynthesis(); + ImageSynthesisResult result = null; + try { + result = imageSynthesis.asyncCall(param); + } catch (Exception e){ + throw new RuntimeException(e.getMessage()); + } + System.out.println(JsonUtils.toJson(result)); + String taskId = result.getOutput().getTaskId(); + System.out.println("taskId=" + taskId); + return taskId; + } + + + /** + * 等待异步任务结束 + * @param taskId 任务id + * */ + public void waitAsyncTask(String taskId) { + ImageSynthesis imageSynthesis = new ImageSynthesis(); + ImageSynthesisResult result = null; + try { + //环境变量配置后,可在这里将apiKey设置为null + result = imageSynthesis.wait(taskId, apiKey); + } catch (ApiException | NoApiKeyException e){ + throw new RuntimeException(e.getMessage()); + } + System.out.println(JsonUtils.toJson(result)); + System.out.println(JsonUtils.toJson(result.getOutput())); + } + + + public static void main(String[] args){ + Main main = new Main(); + main.asyncCall(); + } + +} +``` + +##### 响应示例 + +1、创建任务的响应示例 + +``` +{ + "request_id": "5dbf9dc5-4f4c-9605-85ea-542f97709ba8", + "output": { + "task_id": "7277e20e-aa01-4709-xxxxxxxx", + "task_status": "PENDING" + } +} +``` + +2、查询任务结果的响应示例 + +``` +{ + "request_id": "22f9c744-206c-9a78-899a-xxxxxx", + "output": { + "task_id": "4a0f8fc6-03fb-4c44-a13a-xxxxxx", + "task_status": "SUCCEEDED", + "results": [{ + "orig_prompt": "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵", + "actual_prompt": "一间有着精致雕花窗户的花店,漂亮的深色木质门微微敞开。店内摆放着各式各样的鲜花,包括玫瑰、百合和向日葵,色彩鲜艳,香气扑鼻。背景是温馨的室内场景,光线柔和,透过窗户洒在花朵上。高清写实摄影,中景构图。", + "url": "https://dashscope-result-wlcb.oss-cn-wulanchabu.aliyuncs.com/1.png" + }], + "task_metrics": { + "TOTAL": 1, + "SUCCEEDED": 1, + "FAILED": 0 + } + }, + "usage": { + "image_count": 1 + } +} +``` + +## **使用限制** + +- **数据时效**:任务`task_id`和 图像`url`均只保留 24 小时,过期后将无法查询或下载。 + +- **内容审核**:输入的 `prompt` 和输出的图像均会经过内容安全审核,包含违规内容的请求将报错“IPInfringementSuspect”或“DataInspectionFailed”,具体参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 + + +## **计费与限流** + +- 模型免费额度和计费单价请参见百炼控制台。 + +- 模型限流请参见[万相](https://help.aliyun.com/zh/model-studio/rate-limit#513e0a3df24v7)。 + +- 计费说明:按成功生成的 **图像张数** 计费。模型调用失败或处理错误不产生任何费用,也不消耗[新人免费额度](https://help.aliyun.com/zh/model-studio/new-free-quota)。 + + +## **错误码** + +如果模型调用失败并返回报错信息,请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 + +/\* 调整 table 宽度 \*/ .aliyun-docs-content table.medium-width { max-width: 1018px; width: 100%; } .aliyun-docs-content table.table-no-border tr td:first-child { padding-left: 0; } .aliyun-docs-content table.table-no-border tr td:last-child { padding-right: 0; } /\* 支持吸顶 \*/ div:has(.aliyun-docs-content), .aliyun-docs-content .markdown-body { overflow: visible; } .stick-top { position: sticky; top: 46px; } /\*\*代码块字体\*\*/ /\* 减少表格中的代码块 margin,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body table .help-code-block { margin: 0 !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre { font-size: 12px !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre code { font-size: 12px !important; } /\*\* API Reference 表格 \*\*/ .aliyun-docs-content table.api-reference tr td:first-child { margin: 0px; border-bottom: 1px solid #d8d8d8; } .aliyun-docs-content table.api-reference tr:last-child td:first-child { border-bottom: none; } .aliyun-docs-content table.api-reference p { color: #6e6e80; } .aliyun-docs-content table.api-reference b, i { color: #181818; } .aliyun-docs-content table.api-reference .collapse { border: none; margin-top: 4px; margin-bottom: 4px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse .expandable-title i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse.expanded .expandable-content { padding: 10px 14px 10px 14px !important; margin: 0; border: 1px solid #e9e9e9; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .collapse .expandable-title b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .tabbed-content-box { border: none; } .aliyun-docs-content table.api-reference .tabbed-content-box section { padding: 8px 0 !important; } .aliyun-docs-content table.api-reference .tabbed-content-box.mini .tab-box { /\* position: absolute; left: 40px; right: 0; \*/ } .aliyun-docs-content .margin-top-33 { margin-top: 33px !important; } .aliyun-docs-content .two-codeblocks pre { max-height: calc(50vh - 136px) !important; height: auto; } .expandable-content section { border-bottom: 1px solid #e9e9e9; padding-top: 6px; padding-bottom: 4px; } .expandable-content section:last-child { border-bottom: none; } .expandable-content section:first-child { padding-top: 0; } + +/\* 让表格显示成类似钉钉文档的分栏卡片 \*/ table.help-table-card td { border: 10px solid #FFF !important; background: #F4F6F9; padding: 16px !important; vertical-align: top; } /\* 减少表格中的代码块 margin,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body table .help-code-block { margin: 0 !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre { font-size: 12px !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre code { font-size: 12px !important; } /\* 表格中的引用上下间距调小,避免内容显示过于稀疏 \*/ .unionContainer .markdown-body table blockquote { margin: 4px 0 0 0; } \ No newline at end of file diff --git a/docs/协议文档/阿里系列/图像-万相-涂鸦作画.md b/docs/协议文档/阿里系列/图像-万相-涂鸦作画.md new file mode 100644 index 0000000..cd9850b --- /dev/null +++ b/docs/协议文档/阿里系列/图像-万相-涂鸦作画.md @@ -0,0 +1,545 @@ +本文介绍万相-涂鸦作画模型的API输入输出参数。 + +**相关指南**:[涂鸦作画](https://help.aliyun.com/zh/model-studio/sketch-to-image) + +**重要** + +本文档仅适用于华北2(北京)地域,且必须使用该地域的[API Key](https://bailian.console.aliyun.com/?tab=model#/api-key)。 + +**重要** + +百炼为华北2(北京)地域推出了业务空间专属域名 `https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com`,**能够为推理请求提供卓越的性能和更高的稳定性**,建议从 `https://dashscope.aliyuncs.com` 迁移至新域名。 + +其中 `{WorkspaceId}` 为您的业务空间 ID,可在百炼控制台的**业务空间详情**页面查看。现有域名仍可正常使用。 + +## **模型概览** + +**模型效果示意** + +![image](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/2400704371/p883780.png) + +**模型简介** + +| **模型名称** | **模型简介** | +| --- | --- | +| wanx-sketch-to-image-lite | 万相-涂鸦作画通过手绘图案和文字描述,生成精美的涂鸦绘画作品。 | + +**模型说明** + +| **模型名称** | **计费单价** | **限流(主账号与RAM子账号共用)** | | **免费额度**[(查看)](https://help.aliyun.com/zh/model-studio/new-free-quota) | +| --- | --- | --- | --- | --- | +| **任务下发接口QPS限制** | **同时处理中任务数量** | +| wanx-sketch-to-image-lite | 0.06元/张 | 2 | 1 | 500张 | + +更多说明请参见[模型计费及限流](#b8457b7223zhp)。 + +## **前提条件** + +涂鸦作画API支持通过HTTP和DashScope SDK进行调用。 + +在调用前,您需要[获取API Key](https://help.aliyun.com/zh/model-studio/get-api-key),再[配置API Key到环境变量](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables)。 + +如需通过SDK进行调用,请[安装DashScope SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。目前,该SDK已支持Python和Java。 + +## HTTP调用 + +图像模型处理时间较长,为了避免请求超时,HTTP调用仅支持异步获取模型结果。您需要发起两个请求: + +1. **创建任务获取任务ID**:首先发起创建任务请求,该请求会返回任务ID(task\_id)。 + +2. **根据任务ID查询结果**:使用上一步获得的任务ID,查询任务状态及结果。任务成功执行时将返回图像URL,有效期24小时。 + + +**说明** + +创建任务后,该任务将被加入到排队队列,等待调度执行。后续需要调用“根据任务ID查询结果接口”获取任务状态及结果。 + +### **步骤1:创建任务获取任务ID** + +`POST https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/image2image/image-synthesis/` + +| #### 请求参数 | ## curl ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/image2image/image-synthesis' \\ --header 'X-DashScope-Async: enable' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --header 'Content-Type: application/json' \\ --data '{ "model": "wanx-sketch-to-image-lite", "input": { "sketch_image_url": "https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/6609471071/p743851.jpg", "prompt": "一棵参天大树" }, "parameters": { "size": "768*768", "n": 2, "sketch_weight": 3, "style": "" } }' ``` | +| --- | --- | +| ##### **请求头(Headers)** | +| **Content-Type** `*string*` **(必选)** 请求内容类型。此参数必须设置为`application/json`。 | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| **X-DashScope-Async** `*string*` **(必选)** 异步处理配置参数。HTTP请求只支持异步,**必须设置为**`**enable**`。 **重要** 缺少此请求头将报错:“current user api does not support synchronous calls”。 | +| **X-DashScope-WorkSpace** `*string*` (可选) 阿里云百炼业务空间ID。示例值:llm-xxxx。 您可以在此[获取Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 **详细说明** 此参数根据阿里云百炼API Key进行填写。 - 若为主账号API Key,可不填。不填则使用主账号权限,填写则使用对应的业务空间权限。 - 若为RAM子账号API Key,则必填。RAM子账号一定归属于某个业务空间。 业务空间必须具备访问模型的权限,才能调用API。若无权限,请参考[授权子业务空间模型调用、训练和部署](https://help.aliyun.com/zh/model-studio/use-workspace#f2e68d7ba7ubk)。 > 关于如何区分阿里云百炼主账号和RAM子账号,请参考[主账号管理](https://help.aliyun.com/zh/model-studio/business-space-management)。 | +| ##### **请求体(Request Body)** | +| **model** `*string*` **(必选)** 调用模型。 | +| **input** `*object*` **(必选)** 输入的基本信息,比如提示词、图像URL地址。 **属性** **prompt** `*string*` **(必选)** 提示词,用来描述生成图像中期望包含的元素和视觉特点。 支持中英文,长度不超过75个字符,超过部分会自动截断。 示例值:一棵参天大树。 **sketch\\_image\\_url** `*string*` **(必选)** 输入草图的URL地址。输入草图需要与输出图像的分辨率比例保持一致,否则会导致图片拉伸变形,建议使用白色背景图。 URL 需为公网可访问的地址,并支持 HTTP 或 HTTPS 协议。您也可在此[获取临时公网URL](https://help.aliyun.com/zh/model-studio/get-temporary-file-url)。 图像限制: - 图像格式:JPG、JPEG、PNG、TIFF、WEBP。 - 图像分辨率:不小于256×256像素且不超过2048×2048像素。 - 图像大小:不超过10 MB。 - URL地址中不能包含中文字符。 草图示例: ![image](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/9289386271/p850798.png) | +| **parameters** `*object*` (可选) 图像处理参数。 **属性** **style** `*string*` (可选) 输出图像的风格,目前支持以下风格取值: - :默认值,由模型随机输出图像风格。 - <3d cartoon>:3D卡通。 - :二次元。 - :油画。 - :水彩。 - :素描。 - :中国画。 - :扁平插画。 **size** `*string*` (可选) 输出图像的分辨率。目前仅支持一种图像分辨率:768\\*768,且为默认值。 **n** `*integer*` (可选) 生成图片的数量。取值范围为1~4张,默认为4。 **sketch\\_weight** `*integer*` (可选) 输入草图对输出图像的约束程度。 取值范围为0-10,取值间隔为1, 默认值为10。取值越大表示输出图像跟输入草图越相似。 **sketch\\_extraction** `*boolean*` (可选) 如果上传图片是RGB图片,而非草图(sketch线稿),此参数可控制是否对输入图片进行sketch边缘提取。 默认值为False,表示不进行提取。设置为True时,表示进行提取,此时,`sketch_color`字段失效。 **sketch\\_color** `*array*` (可选) 此字段在`sketch_extraction=false`时生效,所包含数值均被视为画笔色,其余数值均会视为背景色。模型会基于一种或多种画笔色描绘的区域生成新的画作。默认值为\\[\\]。 当sketch\\_image\\_url线稿中的线条不是黑色,而是包含其他一种或多种颜色时,可以指定一个或多个RGB颜色数值作为画笔色。 示例值:\\[\\[134, 134, 134\\], \\[0, 0, 0\\]\\] | + +| #### **响应参数** | #### 成功响应 请保存 task\\_id,用于查询任务状态与结果。 ``` { "output": { "task_status": "PENDING", "task_id": "0385dc79-5ff8-4d82-bcb6-xxxxxx" }, "request_id": "4909100c-7b5a-9f92-bfe5-xxxxxx" } ``` #### 异常响应 创建任务失败,请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "code": "InvalidApiKey", "message": "No API-key provided.", "request_id": "7438d53d-6eb8-4596-8835-xxxxxx" } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | +| **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | +| **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | + +### 步骤2:根据任务ID查询结果 + +`GET https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/tasks/{task_id}` + +| #### 请求参数 | #### 查询任务结果 请将`86ecf553-d340-4e21-xxxxxxxxx`替换为真实的task\\_id。 > 若使用新加坡地域的模型,需将base\\_url替换为https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/tasks/86ecf553-d340-4e21-xxxxxxxxx,其中WorkspaceId需替换为真实的业务空间ID。 ``` curl -X GET https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/tasks/86ecf553-d340-4e21-xxxxxxxxx \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" ``` | +| --- | --- | +| #### **请求头(Headers)** | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| #### **URL路径参数(Path parameters)** | +| **task\\_id** `*string*`**(必选)** 任务ID。 | + +| #### **响应参数** | #### 任务执行成功 任务数据(如任务状态、图像URL等)仅保留24小时,超时后会被自动清除。请您务必及时保存生成的图像。 ``` { "request_id": "85eaba38-0185-99d7-8d16-4d9135238846", "output": { "task_id": "86ecf553-d340-4e21-af6e-a0c6a421c010", "task_status": "SUCCEEDED", "results": [ { "url": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/123/a1.png" }, { "url": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/123/b2.png" } ], "task_metrics": { "TOTAL": 2, "SUCCEEDED": 2, "FAILED": 0 } }, "usage": { "image_count": 2 } } ``` #### 任务执行失败 若任务执行失败,task\\_status将置为 FAILED,并提供错误码和信息。请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "request_id": "e5d70b02-ebd3-98ce-9fe8-759d7d7b107d", "output": { "task_id": "86ecf553-d340-4e21-af6e-a0c6a421c010", "task_status": "FAILED", "code": "InvalidParameter", "message": "The size is not match the allowed size ['1024*1024', '720*1280', '1280*720']", "task_metrics": { "TOTAL": 4, "SUCCEEDED": 0, "FAILED": 4 } } } ``` #### 任务部分失败 模型可以在一次任务中生成多张图片。只要有一张图片生成成功,任务状态将标记为`SUCCEEDED`,并且返回相应的图像URL。对于生成失败的图片,结果中会返回相应的失败原因。同时在usage统计中,只会对成功的结果计数。请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "request_id": "85eaba38-0185-99d7-8d16-4d9135238846", "output": { "task_id": "86ecf553-d340-4e21-af6e-a0c6a421c010", "task_status": "SUCCEEDED", "results": [ { "url": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/123/a1.png" }, { "code": "InternalError.Timeout", "message": "An internal timeout error has occured during execution, please try again later or contact service support." } ], "task_metrics": { "TOTAL": 2, "SUCCEEDED": 1, "FAILED": 1 } }, "usage": { "image_count": 1 } } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 **task\\_metrics** `*object*` 任务结果统计。 **属性** **TOTAL** `*integer*` 总的任务数。 **SUCCEEDED** `*integer*` 任务状态为成功的任务数。 **FAILED** `*integer*` 任务状态为失败的任务数。 **results** `*array of object*` 任务结果列表,包括图像URL、部分任务执行失败报错信息等。 **数据结构** ``` { "results": [ { "url": "" }, { "code": "", "message": "" } ] } ``` **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | +| **usage** `*object*` 输出信息统计。只对成功的结果计数。 **属性** **image\\_count** `*integer*` 模型成功生成图片的数量。计费公式:费用 = 图片数量 × 单价。 | | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | | + +## DashScope SDK调用 + +请先确认已安装最新版DashScope SDK:[安装SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。 + +DashScope SDK目前已支持Python和Java。 + +SDK与HTTP接口的参数名基本一致,参数结构根据不同语言的SDK封装而定。参数说明可参考[HTTP调用](https://help.aliyun.com/zh/model-studio/text-to-image-api-reference#42703589880ts)。 + +由于图像模型处理时间较长,底层服务采用异步方式提供。SDK在上层进行了封装,支持同步、异步两种调用方式。 + +### Python SDK调用 + +## 同步调用 + +##### **请求示例** + +``` +from http import HTTPStatus +from urllib.parse import urlparse, unquote +from pathlib import PurePosixPath +import requests +import dashscope +from dashscope import ImageSynthesis +import os + +dashscope.base_http_api_url = 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1' + +prompt = "一棵参天大树" +sketch_image_url = "https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/6609471071/p743851.jpg" +model = "wanx-sketch-to-image-lite" +task = "image2image" + + +print('----sync call, please wait a moment----') +rsp = ImageSynthesis.call(api_key=os.getenv("DASHSCOPE_API_KEY"), + model=model, + prompt=prompt, + n=1, + style='', + size='768*768', + sketch_image_url=sketch_image_url, + task=task) +print('response: %s' % rsp) +if rsp.status_code == HTTPStatus.OK: + print(rsp.output) + # save file to current directory + for result in rsp.output.results: + file_name = PurePosixPath(unquote(urlparse(result.url).path)).parts[-1] + with open('./%s' % file_name, 'wb+') as f: + f.write(requests.get(result.url).content) +else: + print('sync_call Failed, status_code: %s, code: %s, message: %s' % + (rsp.status_code, rsp.code, rsp.message)) +``` + +##### **响应示例** + +``` +{ + "status_code": 200, + "request_id": "4126d9dd-e037-9f32-8d56-6d29ab3f9a06", + "code": null, + "message": "", + "output": { + "task_id": "b476bc4e-35c1-4c4e-a4d9-xxxxxxx", + "task_status": "SUCCEEDED", + "results": [{ + "url": "https://dashscope-result-sh.oss-cn-shanghai.aliyuncs.com/xxxx.png" + }], + "submit_time": "2024-11-01 09:50:56.081", + "scheduled_time": "2024-11-01 09:50:56.104", + "end_time": "2024-11-01 09:51:22.740", + "task_metrics": { + "TOTAL": 1, + "SUCCEEDED": 1, + "FAILED": 0 + } + }, + "usage": { + "image_count": 1 + } +} +``` + +## 异步调用 + +##### **请求示例** + +``` +from http import HTTPStatus +from urllib.parse import urlparse, unquote +from pathlib import PurePosixPath +import requests +import dashscope +from dashscope import ImageSynthesis +import os + +dashscope.base_http_api_url = 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1' + +prompt = "一棵参天大树" +sketch_image_url = "https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/6609471071/p743851.jpg" +model = "wanx-sketch-to-image-lite" +task = "image2image" + + +# 异步调用 +def async_call(): + print('----create task----') + task_info = create_async_task() + print('----wait task done then save image----') + wait_async_task(task_info) + + +# 创建异步任务 +def create_async_task(): + rsp = ImageSynthesis.async_call(api_key=os.getenv("DASHSCOPE_API_KEY"), + model=model, + prompt=prompt, + n=1, + style='', + size='768*768', + sketch_image_url=sketch_image_url, + task=task) + print(rsp) + if rsp.status_code == HTTPStatus.OK: + print(rsp.output) + else: + print('create_async_task Failed, status_code: %s, code: %s, message: %s' % + (rsp.status_code, rsp.code, rsp.message)) + return rsp + + +# 等待异步任务结束 +def wait_async_task(task): + rsp = ImageSynthesis.wait(task) + print(rsp) + if rsp.status_code == HTTPStatus.OK: + print(rsp.output.task_status) + # save file to current directory + for result in rsp.output.results: + file_name = PurePosixPath(unquote(urlparse(result.url).path)).parts[-1] + with open('./%s' % file_name, 'wb+') as f: + f.write(requests.get(result.url).content) + else: + print('Failed, status_code: %s, code: %s, message: %s' % + (rsp.status_code, rsp.code, rsp.message)) + + +if __name__ == '__main__': + async_call() +``` + +##### **响应示例** + +**1、创建任务的响应示例** + +``` +{ + "status_code": 200, + "request_id": "31b04171-011c-96bd-ac00-f0383b669cc7", + "code": "", + "message": "", + "output": { + "task_id": "4f90cf14-a34e-4eae-xxxxxxxx", + "task_status": "PENDING", + "results": [] + }, + "usage": null +} +``` + +**2、查询任务结果的响应示例** + +``` +{ + "status_code": 200, + "request_id": "d861d3ba-4b29-9491-abad-266ef4fb2f08", + "code": null, + "message": "", + "output": { + "task_id": "4f90cf14-a34e-4eae-xxxxxxxx", + "task_status": "SUCCEEDED", + "results": [{ + "url": "https://dashscope-result-hz.oss-cn-hangzhou.aliyuncs.com/xxxx.png" + }], + "submit_time": "2024-10-31 20:40:35.631", + "scheduled_time": "2024-10-31 20:40:35.684", + "end_time": "2024-10-31 20:41:02.700", + "task_metrics": { + "TOTAL": 1, + "SUCCEEDED": 1, + "FAILED": 0 + } + }, + "usage": { + "image_count": 1 + } +} +``` + +### Java SDK调用 + +## 同步调用 + +##### 请求示例 + +``` +// Copyright (c) Alibaba, Inc. and its affiliates. + +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.utils.JsonUtils; +import com.alibaba.dashscope.utils.Constants; + +public class Main { + static {Constants.baseHttpApiUrl="https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1";} + + public void syncCall() { + String prompt = "一棵参天大树"; + String sketchImageUrl = "https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/6609471071/p743851.jpg"; + String model = "wanx-sketch-to-image-lite"; + ImageSynthesisParam param = ImageSynthesisParam.builder() + .model(model) + .prompt(prompt) + .n(1) + .size("768*768") + .sketchImageUrl(sketchImageUrl) + .style("") + .build(); + + String task = "image2image"; + ImageSynthesis imageSynthesis = new ImageSynthesis(task); + ImageSynthesisResult result = null; + try { + System.out.println("---sync call, please wait a moment----"); + result = imageSynthesis.call(param); + } catch (ApiException | NoApiKeyException e){ + throw new RuntimeException(e.getMessage()); + } + System.out.println(JsonUtils.toJson(result)); + } + + + public static void main(String[] args){ + Main text2Image = new Main(); + text2Image.syncCall(); + } + +} +``` + +##### **响应示例** + +``` +{ + "request_id": "150edcda-05d5-9ffe-8803-84626d1db623", + "output": { + "task_id": "f2098ff0-146e-404c-bb25-xxxxxxxx", + "task_status": "SUCCEEDED", + "results": [{ + "url": "https://dashscope-result-hz.oss-cn-hangzhou.aliyuncs.com/xxxx.png" + }], + "task_metrics": { + "TOTAL": 1, + "SUCCEEDED": 1, + "FAILED": 0 + } + }, + "usage": { + "image_count": 1 + } +} +``` + +## 异步调用 + +##### **请求示例** + +``` +// Copyright (c) Alibaba, Inc. and its affiliates. + +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.utils.JsonUtils; +import com.alibaba.dashscope.utils.Constants; + +public class Main { + static {Constants.baseHttpApiUrl="https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1";} + + public void asyncCall() { + System.out.println("---create task----"); + String taskId = this.createAsyncTask(); + System.out.println("---wait task done then return image url----"); + this.waitAsyncTask(taskId); + } + + /** + * 创建异步任务 + * @return taskId + */ + public String createAsyncTask() { + String prompt = "一棵参天大树"; + String sketchImageUrl = "https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/6609471071/p743851.jpg"; + String model = "wanx-sketch-to-image-lite"; + ImageSynthesisParam param = ImageSynthesisParam.builder() + .model(model) + .prompt(prompt) + .n(1) + .size("768*768") + .sketchImageUrl(sketchImageUrl) + .style("") + .build(); + + String task = "image2image"; + ImageSynthesis imageSynthesis = new ImageSynthesis(task); + ImageSynthesisResult result = null; + try { + result = imageSynthesis.asyncCall(param); + } catch (Exception e){ + throw new RuntimeException(e.getMessage()); + } + String taskId = result.getOutput().getTaskId(); + System.out.println("taskId=" + taskId); + return taskId; + } + + + /** + * 等待异步任务结束 + * @param taskId 任务id + * */ + public void waitAsyncTask(String taskId) { + ImageSynthesis imageSynthesis = new ImageSynthesis(); + ImageSynthesisResult result = null; + try { + // If you have set the DASHSCOPE_API_KEY in the system environment variable, the apiKey can be null. + result = imageSynthesis.wait(taskId, null); + } catch (ApiException | NoApiKeyException e){ + throw new RuntimeException(e.getMessage()); + } + + System.out.println(JsonUtils.toJson(result.getOutput())); + System.out.println(JsonUtils.toJson(result.getUsage())); + } + + + public static void main(String[] args){ + Main text2Image = new Main(); + text2Image.asyncCall(); + } + +} +``` + +##### **响应示例** + +**1、步骤1:创建任务获取任务ID的响应示例** + +``` +{ + "request_id": "5dbf9dc5-4f4c-9605-85ea-542f97709ba8", + "output": { + "task_id": "7277e20e-aa01-4709-xxxxxxxx", + "task_status": "PENDING" + } +} +``` + +**2、步骤2:根据任务ID查询结果的响应示例** + +``` +{ + "request_id": "c44213ba-7aa3-91e4-97c1-c527ade82597", + "output": { + "task_id": "7277e20e-aa01-4709-xxxxxxxx", + "task_status": "SUCCEEDED", + "results": [{ + "url": "https://dashscope-result-hz.oss-cn-hangzhou.aliyuncs.com/xxxx.png" + }], + "task_metrics": { + "TOTAL": 1, + "SUCCEEDED": 1, + "FAILED": 0 + } + }, + "usage": { + "image_count": 1 + } +} +``` + +## 错误码 + +如果模型调用失败并返回报错信息,请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 + +此API还有特定状态码,具体如下所示。 + +| **HTTP状态码** | **接口错误码(code)** | **接口错误信息(message)** | **含义说明** | +| --- | --- | --- | --- | +| 400 | InvalidParameter.DataInspection | Unable to download the media resource during the data inspection process. | 输入图片无法下载,请检查URL地址是否正确且可访问。 | +| 400 | InvalidParameter | Value error, format of image {url} is not valid : payload.input.sketch | 输入图片格式不合法,请确认图片格式为JPG、JPEG、PNG、TIFF或WEBP。 | + +## **常见问题** + +### 模型计费及限流 + +**免费额度** + +- 额度说明:免费额度是指模型成功生成的输出图片数量。输入图片及模型处理失败的情况不占用免费额度。 + +- 领取方式:开通阿里云百炼大模型服务后自动发放,有效期90天。 + +- 使用账号:阿里云主账号与其RAM子账号共享免费额度。 + +- 更多详情请参见[新人免费额度](https://help.aliyun.com/zh/model-studio/new-free-quota)。 + + +**限时免费** + +- 当计费为限时免费时,表示该模型处于公测阶段,免费额度用尽后不可使用。 + + +**计费说明** + +- 当计费有明确单价时,如0.2元/秒,表示该模型已商业化,免费额度用尽或过期后需付费使用。 + +- 计费项:只对模型成功生成的输出图片进行收费,其余情况暂不计费。 + +- 付费方式:由阿里云主账号统一付费。RAM子账号不能独立计量计费,必须由所属的主账号付费。如果您需要查询账单信息,请前往阿里云控制台[账单概览](https://billing-cost.console.aliyun.com/finance/month-bill/account)。 + +- 充值途径:您可以在阿里云控制台[费用与成本](https://billing-cost.console.aliyun.com/home?spm=a2c4g.11186623.0.0.2d543048F4KRQP)页面进行充值。 + +- 模型调用情况:您可以前往阿里云百炼的[模型观测](https://bailian.console.aliyun.com/#/model-telemetry)查看模型调用量及调用次数。 + +- 更多计费问题请参见[计费项](https://help.aliyun.com/zh/model-studio/billing-for-model-studio)。 + + +**限流** + +- 限流说明:阿里云主账号与其RAM子账号共享限流限制。 + + +/\* 调整 table 宽度 \*/ .aliyun-docs-content table.medium-width { max-width: 1018px; width: 100%; } .aliyun-docs-content table.table-no-border tr td:first-child { padding-left: 0; } .aliyun-docs-content table.table-no-border tr td:last-child { padding-right: 0; } /\* 支持吸顶 \*/ div:has(.aliyun-docs-content), .aliyun-docs-content .markdown-body { overflow: visible; } .stick-top { position: sticky; top: 46px; } /\*\*代码块字体\*\*/ /\* 减少表格中的代码块 margin,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body table .help-code-block { margin: 0 !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre { font-size: 12px !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre code { font-size: 12px !important; } /\*\* API Reference 表格 \*\*/ .aliyun-docs-content table.api-reference tr td:first-child { margin: 0px; border-bottom: 1px solid #d8d8d8; } .aliyun-docs-content table.api-reference tr:last-child td:first-child { border-bottom: none; } .aliyun-docs-content table.api-reference p { color: #6e6e80; } .aliyun-docs-content table.api-reference b, i { color: #181818; } .aliyun-docs-content table.api-reference .collapse { border: none; margin-top: 4px; margin-bottom: 4px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse .expandable-title i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse.expanded .expandable-content { padding: 10px 14px 10px 14px !important; margin: 0; border: 1px solid #e9e9e9; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .collapse .expandable-title b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .tabbed-content-box { border: none; } .aliyun-docs-content table.api-reference .tabbed-content-box section { padding: 8px 0 !important; } .aliyun-docs-content table.api-reference .tabbed-content-box.mini .tab-box { /\* position: absolute; left: 40px; right: 0; \*/ } .aliyun-docs-content .margin-top-33 { margin-top: 33px !important; } .aliyun-docs-content .two-codeblocks pre { max-height: calc(50vh - 136px) !important; height: auto; } .expandable-content section { border-bottom: 1px solid #e9e9e9; padding-top: 6px; padding-bottom: 4px; } .expandable-content section:last-child { border-bottom: none; } .expandable-content section:first-child { padding-top: 0; } \ No newline at end of file diff --git a/docs/协议文档/阿里系列/图像-千问-图像编辑.md b/docs/协议文档/阿里系列/图像-千问-图像编辑.md new file mode 100644 index 0000000..c720fa9 --- /dev/null +++ b/docs/协议文档/阿里系列/图像-千问-图像编辑.md @@ -0,0 +1,581 @@ +千问-图像编辑模型支持多图输入和多图输出,可精确修改图内文字、增删或移动物体、改变主体动作、迁移图片风格及增强画面细节。 + +| **快速入口:**[使用指南](https://help.aliyun.com/zh/model-studio/qwen-image-edit-guide) **\\|** [技术博客](https://qwen.ai/blog?id=1675c295dc29dd31073e5b3f72876e9d684e41c6&from=research.research-list) \\| [在线体验](https://bailian.console.aliyun.com/?tab=model#/efm/model_experience_center/vision?currentTab=imageGenerate&modelId=qwen-image-edit) | +| --- | --- | --- | + +## **模型概览** + +| **提示词** | **输入图** | **输出图像** | +| --- | --- | --- | +| 在画面右下角石板路旁、靠近树干根部的位置,以浅灰墨色手写体题写一首七言绝句,字体为行楷风格,笔触自然流畅、略带飞白,大小适中(约占画面高度1/10),与整体水墨淡雅氛围协调。诗文内容为:“青石桥畔柳风轻, 素手拈花闭目听。 一水碧痕浮旧梦, 半篙烟雨入空舲。”诗句横向排列,四句分两行书写(前两句一行,后两句一行),末句“舲”字右下角钤一枚朱红小印,印文为“江南”二字篆书,尺寸约等于单字高度的1/3。 | ![image (18)-2026-03-10-16-39-59](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/3279833771/p1058430.webp) | ![image (19)](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/3279833771/p1058432.png) | +| 使用图一的城市照片作为底图。请勿更改照片中的真实建筑、街道、车辆或人物。保持照片的真实性。三个图二中的卡通形象在建筑物周围,一个趴在建筑物上方,一个从建筑物的右边探出头来,一个坐在建筑物前的空地上。该形象应采用扁平化的图形风格绘制,轮廓清晰,类似于壁画或海报插图。 | ![image (15)-combine](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/3279833771/p1058424.webp) | ![image (17)](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/3279833771/p1058425.png) | + +| **模型名称** | **模型简介** | **输出图像规格** | +| --- | --- | --- | +| qwen-image-2.0-pro `**推荐**` > 当前与qwen-image-2.0-pro-2026-04-22能力相同 | 千问图像生成与编辑模型Pro系列。文字渲染、真实质感、语义遵循能力更强。 > 图像生成请参考[千问-文生图](https://help.aliyun.com/zh/model-studio/qwen-image-api)。 | 图像分辨率: - **可指定**:图像总像素需在512\\*512至2048\\*2048之间。 - **默认**:总像素数接近 1024\\*1024,宽高比与输入图(多图输入时为最后一张)相近。 图像格式:png 图像张数:1-6张 | +| qwen-image-2.0-pro-2026-06-22 `**推荐**` | +| qwen-image-2.0-pro-2026-04-22 | +| qwen-image-2.0-pro-2026-03-03 | +| qwen-image-2.0 `**推荐**` > 当前与qwen-image-2.0-2026-03-03能力相同 | 千问图像生成与编辑模型加速版,兼顾效果与响应速度。 > 图像生成请参考[千问-文生图](https://help.aliyun.com/zh/model-studio/qwen-image-api)。 | +| qwen-image-2.0-2026-03-03 `**推荐**` | +| qwen-image-edit-max > 当前与qwen-image-edit-max-2026-01-16能力相同 | 千问图像编辑Max系列。工业设计、几何推理、角色一致性更强。 | 图像分辨率: - **可指定**:宽和高的取值范围均为`[512, 2048]`像素。 - **默认**:总像素数接近 1024\\*1024,宽高比与输入图(多图输入时为最后一张)相近。 图像格式:png 图像张数:1-6张 | +| qwen-image-edit-max-2026-01-16 | +| qwen-image-edit-plus > 当前与qwen-image-edit-plus-2025-10-30能力相同 | 千问图像编辑Plus系列,支持多图输出与自定义分辨率。 | +| qwen-image-edit-plus-2025-12-15 | +| qwen-image-edit-plus-2025-10-30 | +| qwen-image-edit | 支持单图编辑和多图融合。 | 图像分辨率:**不可指定**。生成规则同上方的**默认**规则。 图像格式:png 图像张数:固定1张 | + +各地域支持的模型请参见[百炼控制台](https://bailian.console.aliyun.com/cn-beijing?tab=model#/model-market/all)。 + +## **前提条件** + +在调用前,您需要[获取API Key](https://help.aliyun.com/zh/model-studio/get-api-key),再[配置API Key到环境变量](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables)。 + +如需通过SDK进行调用,请[安装DashScope SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。目前,该SDK已支持Python和Java。 + +**重要** + +华北2(北京)和新加坡地域拥有独立的 **API Key** 与**请求地址**,不可混用,跨地域调用将导致鉴权失败或服务报错。 + +**重要** + +阿里云百炼为华北2(北京)、新加坡地域推出了业务空间专属域名,**能够为推理请求提供卓越的性能和更高的稳定性**,建议迁移至新域名: + +- 华北2(北京)地域:从 `https://dashscope.aliyuncs.com` 迁移至 `https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com` + +- 新加坡地域:从 `https://dashscope-intl.aliyuncs.com` 迁移至 `https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com` + + +其中 `{WorkspaceId}` 为您的业务空间 ID,可在阿里云百炼控制台的**业务空间详情**页面查看。现有域名仍可正常使用。 + +## HTTP调用 + +**北京地域**:`POST https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` + +**新加坡地域**:`POST https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +| #### 请求参数 | ## 单图编辑 ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \\ --header 'Content-Type: application/json' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --data '{ "model": "qwen-image-2.0-pro", "input": { "messages": [ { "role": "user", "content": [ { "image": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20260310/jiydyi/image+%2818%29-2026-03-10-16-39-59.webp" }, { "text": "在画面右下角石板路旁、靠近树干根部的位置,以浅灰墨色手写体题写一首七言绝句,字体为行楷风格,笔触自然流畅、略带飞白,大小适中(约占画面高度1/10),与整体水墨淡雅氛围协调。诗文内容为:“青石桥畔柳风轻, 素手拈花闭目听。 一水碧痕浮旧梦, 半篙烟雨入空舲。”诗句横向排列,四句分两行书写(前两句一行,后两句一行),末句“舲”字右下角钤一枚朱红小印,印文为“江南”二字篆书,尺寸约等于单字高度的1/3。" } ] } ] }, "parameters": { "n": 1, "negative_prompt": " ", "prompt_extend": true, "watermark": false, "size": "2048*2048" } }' ``` ## 多图融合 ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \\ --header 'Content-Type: application/json' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --data '{ "model": "qwen-image-2.0-pro", "input": { "messages": [ { "role": "user", "content": [ { "image": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20260310/rdsgaa/image+%2815%29.png" }, { "image": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20260310/qokhtl/image+%2816%29.png" }, { "text": "使用图一的城市照片作为底图。请勿更改照片中的真实建筑、街道、车辆或人物。保持照片的真实性。三个图二中的卡通形象在建筑物周围,一个趴在建筑物上方,一个从建筑物的右边探出头来,一个坐在建筑物前的空地上。该形象应采用扁平化的图形风格绘制,轮廓清晰,类似于壁画或海报插图。" } ] } ] }, "parameters": { "n": 1, "negative_prompt": " ", "prompt_extend": true, "watermark": false, "size": "2048*2048" } }' ``` | +| --- | --- | +| ##### 请求头(Headers) | +| **Content-Type** `*string*` **(必选)** 请求内容类型。此参数必须设置为`application/json`。 | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| ##### 请求体(Request Body) | +| **model** `*string*` **(必选)** 模型名称,示例值qwen-image-2.0-pro。 | +| **input** `*object*` **(必选)** 输入参数对象,包含以下字段: **属性** **messages** `*array*` **(必选)** 请求内容数组。**当前仅支持单轮对话**,因此数组内**有且只有一个对象**,该对象包含`role`和`content`两个属性。 **属性** **role** `*string*` **(必选)** 消息发送者角色,必须设置为`user`。 **content** `*array*` **(必选)** 消息内容,包含1-3张图像,格式为 `{"image": "..."}`;以及单个编辑指令,格式为 `{"text": "..."}`。 **属性** **image** `*string*` **(必选)** 输入图像的 URL 或 Base64 编码数据。支持传入1-3张图像。 多图输入时,按照数组顺序定义图像顺序,输出图像的比例以最后一张为准。 **图像要求:** - 图像格式:JPG、JPEG、PNG、BMP、TIFF、WEBP和GIF。 > 输出图像为PNG格式,对于GIF动图,仅处理其第一帧。 - 图像分辨率:为获得最佳效果,建议图像的宽和高均在384像素至3072像素之间。分辨率过低可能导致生成效果模糊,过高则会增加处理时长。 - 图像大小:不超过10MB。 **支持的输入格式** 1. 公网URL: - 支持 HTTP 和 HTTPS 协议。 - 示例值:`https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250925/fpakfo/image36.webp`。 2. 临时URL: - 支持OSS协议,必须通过[上传文件获取临时 URL](https://help.aliyun.com/zh/model-studio/get-temporary-file-url)。 - 示例值:`oss://dashscope-instant/xxx/2024-07-18/xxx/cat.png`。 3. 传入 Base64 编码图像后的字符串 - 示例值:`data:image/jpeg;base64,GDU7MtCZz...`(示例已截断,仅做演示) - Base64 编码规范请参见[通过Base64编码传入图片](#907c84c1a6wrm)。 **text** `*string*` **(必选)** 正向提示词用于描述您期望生成的图像内容、风格和构图。 支持中英文,qwen-image-2.0系列模型长度上限为 1300 Token,其他模型为 800 Token,超出部分将自动截断。 **注意**:仅支持传入一个text,不传或传入多个将报错。 | +| **parameters** `*object*` (可选) 控制图像生成的附加参数。 **属性** **n** `*integer*` (可选) 输出图像的数量,默认值为1。 对于qwen-image-2.0系列、qwen-image-edit-max、qwen-image-edit-plus系列模型,可选择输出1-6张图片。 对于`qwen-image-edit`,仅支持输出1张图片。 **negative\\_prompt** `*string*` (可选) 反向提示词,用来描述不希望在画面中看到的内容,可以对画面进行限制。 支持中英文,长度上限500个字符,每个汉字、字母、数字或符号计为一个字符,超过部分会自动截断。 示例值:低分辨率、错误、最差质量、低质量、残缺、多余的手指、比例不良等。 **size** `*string*` (可选) 设置输出图像的分辨率,格式为`宽*高`,例如`"1024*1536"`。 **qwen-image-2.0系列模型**: - 图像总像素需在512\\*512至2048\\*2048之间。 - 默认总像素数接近 `1024*1024`,宽高比与输入图(多图输入时为最后一张)相近。 **qwen-image-edit-max、qwen-image-edit-plus系列模型**: - 宽和高的取值范围均为\\[512, 2048\\]像素。 - 默认总像素数接近 `1024*1024`,宽高比与输入图(多图输入时为最后一张)相近。 > 指定 `size` 参数,系统会以 `size`指定的宽高为目标,将实际输出图像的宽高调整为最接近的16的倍数。例如,设置`1033*1032`,输出图像尺寸为`1040*1024`。 常见比例推荐分辨率 - 1:1: 1024\\*1024、1536\\*1536 - 2:3: 768\\*1152、1024\\*1536 - 3:2: 1152\\*768、1536\\*1024 - 3:4: 960\\*1280、1080\\*1440 - 4:3: 1280\\*960、1440\\*1080 - 9:16: 720\\*1280、1080\\*1920 - 16:9: 1280\\*720、1920\\*1080 - 21:9: 1344\\*576、2048\\*872 **支持模型**:除`qwen-image-edit`以外的模型。 **prompt\\_extend** `*boolean*` (可选) 是否开启提示词智能改写,默认值为 `true`。开启后,模型会优化正向提示词(`text`),对描述较简单的提示词效果提升明显。 **支持模型**:除`qwen-image-edit`以外的模型。 **watermark** `*boolean*` (可选) 是否在图像右下角添加 "Qwen-Image" 水印。默认值为 `false`。 **seed** `*integer*` (可选) 随机数种子,取值范围`[0,2147483647]`。 使用相同的`seed`参数值可使生成内容保持相对稳定。若不提供,算法将自动使用随机数种子。 **注意**:模型生成过程具有概率性,即使使用相同的`seed`,也不能保证每次生成结果完全一致。 | + +| #### 响应参数 | ## 任务执行成功 任务数据(如任务状态、图像URL等)仅保留24小时,超时后会被自动清除。请您务必及时保存生成的图像。 ``` { "output": { "choices": [ { "finish_reason": "stop", "message": { "content": [ { "image": "https://dashscope-result-sz.oss-cn-shenzhen.aliyuncs.com/xxx.png?Expires=xxx" } ], "role": "assistant" } } ] }, "usage": { "height": 2048, "image_count": 1, "width": 2048 }, "request_id": "571ae02f-5c9d-436c-83c2-f221e6df0xxx" } ``` ## 任务执行异常 如果因为某种原因导致任务执行失败,将返回相关信息,可以通过code和message字段明确指示错误原因。请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "request_id": "31f808fd-8eef-9004-xxxxx", "code": "InvalidApiKey", "message": "Invalid API-key provided." } ``` | +| --- | --- | +| **output** `*object*` 包含模型生成结果。 **属性** **choices** `*array*` 结果选项列表。 **属性** **finish\\_reason** `*string*` 任务停止原因,自然停止时为`stop`。 **message** `*object*` 模型返回的消息。 **属性** **role** `*string*` 消息的角色,固定为`assistant`。 **content** `*array*` 消息内容,包含生成的图像信息。 **属性** **image** `*string*` 生成图像的 URL,格式为PNG。**链接有效期为24小时**,请及时下载并保存图像。 | +| **usage** `*object*` 本次调用的资源使用情况,仅调用成功时返回。 **属性** **image\\_count** `*integer*` 生成图像的张数。 **width** `*integer*` 生成图像的宽度(像素)。 **height** `*integer*` 生成图像的高度(像素)。 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | +| **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | +| **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | + +## DashScope SDK调用 + +SDK 的参数命名与[HTTP接口](#42703589880ts)基本一致,参数结构根据语言特性进行封装,完整参数列表请参见[千问 API 参考](https://help.aliyun.com/zh/model-studio/qwen-api-reference/)。 + +### Python SDK调用 + +**说明** + +- 推荐安装最新版DashScope Python SDK,否则可能运行报错:[安装或升级SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。 + +- 不支持异步接口。 + + +#### **请求示例** + +## 通过公网URL传入图片 + +``` +import json +import os +from dashscope import MultiModalConversation +import dashscope + +# 以下为华北2(北京)地域的URL。请将 {WorkspaceId} 替换为您的百炼业务空间ID,各地域的URL不同。 +dashscope.base_http_api_url = 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1' + +# 模型支持输入1-3张图片 +messages = [ + { + "role": "user", + "content": [ + {"image": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20260310/rdsgaa/image+%2815%29.png"}, + {"image": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20260310/qokhtl/image+%2816%29.png"}, + {"text": "使用图一的城市照片作为底图。请勿更改照片中的真实建筑、街道、车辆或人物。保持照片的真实性。三个图二中的卡通形象在建筑物周围,一个趴在建筑物上方,一个从建筑物的右边探出头来,一个坐在建筑物前的空地上。该形象应采用扁平化的图形风格绘制,轮廓清晰,类似于壁画或海报插图。"} + ] + } +] + +# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +# 若没有配置环境变量,请用百炼 API Key 将下行替换为:api_key="sk-xxx" +api_key = os.getenv("DASHSCOPE_API_KEY") + +# qwen-image-2.0系列、qwen-image-edit-max、qwen-image-edit-plus系列支持输出1-6张图片 +response = MultiModalConversation.call( + api_key=api_key, + model="qwen-image-2.0-pro", + messages=messages, + stream=False, + n=1, + watermark=False, + negative_prompt=" ", + prompt_extend=True, + size="2048*2048", +) + +if response.status_code == 200: + # 如需查看完整响应,请取消下行注释 + # print(json.dumps(response, ensure_ascii=False)) + for i, content in enumerate(response.output.choices[0].message.content): + print(f"输出图像{i+1}的URL:{content['image']}") +else: + print(f"HTTP返回码:{response.status_code}") + print(f"错误码:{response.code}") + print(f"错误信息:{response.message}") + print("请参考文档:https://help.aliyun.com/zh/model-studio/error-code") +``` + +## 通过Base64编码传入图片 + +``` +import json +import os +from dashscope import MultiModalConversation +import base64 +import mimetypes +import dashscope + +# 以下为华北2(北京)地域的URL。请将 {WorkspaceId} 替换为您的百炼业务空间ID,各地域的URL不同。 +dashscope.base_http_api_url = 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1' + +# ---用于 Base64 编码 --- +# 格式为 data:{mime_type};base64,{base64_data} +def encode_file(file_path): + mime_type, _ = mimetypes.guess_type(file_path) + if not mime_type or not mime_type.startswith("image/"): + raise ValueError("不支持或无法识别的图像格式") + + try: + with open(file_path, "rb") as image_file: + encoded_string = base64.b64encode( + image_file.read()).decode('utf-8') + return f"data:{mime_type};base64,{encoded_string}" + except IOError as e: + raise IOError(f"读取文件时出错: {file_path}, 错误: {str(e)}") + + +# 获取图像的 Base64 编码 +# 调用编码函数,请将 "/path/to/your/image.png" 替换为您的本地图片文件路径,否则无法运行 +image = encode_file("/path/to/your/image.png") + +messages = [ + { + "role": "user", + "content": [ + {"image": image}, + {"text": "在画面右下角石板路旁、靠近树干根部的位置,以浅灰墨色手写体题写一首七言绝句,字体为行楷风格,笔触自然流畅、略带飞白,大小适中(约占画面高度1/10),与整体水墨淡雅氛围协调。诗文内容为:“青石桥畔柳风轻, 素手拈花闭目听。 一水碧痕浮旧梦, 半篙烟雨入空舲。”诗句横向排列,四句分两行书写(前两句一行,后两句一行),末句“舲”字右下角钤一枚朱红小印,印文为“江南”二字篆书,尺寸约等于单字高度的1/3。"} + ] + } +] + +# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +# 若没有配置环境变量,请用百炼 API Key 将下行替换为:api_key="sk-xxx" +api_key = os.getenv("DASHSCOPE_API_KEY") + +# qwen-image-2.0系列、qwen-image-edit-max、qwen-image-edit-plus系列支持输出1-6张图片 +response = MultiModalConversation.call( + api_key=api_key, + model="qwen-image-2.0-pro", + messages=messages, + stream=False, + n=1, + watermark=False, + negative_prompt=" ", + prompt_extend=True, + size="2048*2048", +) + +if response.status_code == 200: + # 如需查看完整响应,请取消下行注释 + # print(json.dumps(response, ensure_ascii=False)) + for i, content in enumerate(response.output.choices[0].message.content): + print(f"输出图像{i+1}的URL:{content['image']}") +else: + print(f"HTTP返回码:{response.status_code}") + print(f"错误码:{response.code}") + print(f"错误信息:{response.message}") + print("请参考文档:https://help.aliyun.com/zh/model-studio/error-code") +``` + +## 通过URL下载图像 + +``` +# 需要安装requests以下载图像: pip install requests +import requests + + +def download_image(image_url, save_path='output.png'): + try: + response = requests.get(image_url, stream=True, timeout=300) # 设置超时 + response.raise_for_status() # 如果HTTP状态码不是200,则引发异常 + with open(save_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + print(f"图像已成功下载到: {save_path}") + + except requests.exceptions.RequestException as e: + print(f"图像下载失败: {e}") + + +image_url = "https://dashscope-result-sz.oss-cn-shenzhen.aliyuncs.com/xxx.png?Expires=xxx" +download_image(image_url, save_path='output.png') +``` + +#### **响应示例** + +图像链接的有效期为24小时,请及时下载图像。 + +> `input_tokens`、`output_tokens`和`characters`为兼容字段,当前固定为0。 + +``` +{ + "status_code": 200, + "request_id": "959afba6-544e-487e-b58a-6bd9fea97xxx", + "code": "", + "message": "", + "output": { + "text": null, + "finish_reason": null, + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-hz.oss-cn-hangzhou.aliyuncs.com/xxx.png?Expires=xxx" + } + ] + } + } + ], + "audio": null + }, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "characters": 0, + "height": 2048, + "image_count": 1, + "width": 2048 + } +} +``` + +### Java SDK调用 + +**说明** + +推荐安装最新版DashScope Java SDK,否则可能运行报错:[安装或升级SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。 + +#### **请求示例** + +## 通过公网URL传入图片 + +``` +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult; +import com.alibaba.dashscope.common.MultiModalMessage; +import com.alibaba.dashscope.common.Role; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.exception.UploadFileException; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; + +import java.io.IOException; +import java.util.*; + +public class QwenImageEdit { + + static { + // 以下为华北2(北京)地域的URL。请将 {WorkspaceId} 替换为您的百炼业务空间ID,各地域的URL不同。 + Constants.baseHttpApiUrl = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"; + } + + // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + // 若没有配置环境变量,请用百炼 API Key 将下行替换为:apiKey="sk-xxx" + static String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + public static void call() throws ApiException, NoApiKeyException, UploadFileException, IOException { + + MultiModalConversation conv = new MultiModalConversation(); + + // 模型支持输入1-3张图片 + MultiModalMessage userMessage = MultiModalMessage.builder().role(Role.USER.getValue()) + .content(Arrays.asList( + Collections.singletonMap("image", "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20260310/rdsgaa/image+%2815%29.png"), + Collections.singletonMap("image", "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20260310/qokhtl/image+%2816%29.png"), + Collections.singletonMap("text", "使用图一的城市照片作为底图。请勿更改照片中的真实建筑、街道、车辆或人物。保持照片的真实性。三个图二中的卡通形象在建筑物周围,一个趴在建筑物上方,一个从建筑物的右边探出头来,一个坐在建筑物前的空地上。该形象应采用扁平化的图形风格绘制,轮廓清晰,类似于壁画或海报插图。") + )).build(); + // qwen-image-2.0系列、qwen-image-edit-max、qwen-image-edit-plus系列支持输出1-6张图片 + Map parameters = new HashMap<>(); + parameters.put("watermark", false); + parameters.put("negative_prompt", " "); + parameters.put("n", 1); + parameters.put("prompt_extend", true); + parameters.put("size", "2048*2048"); + + MultiModalConversationParam param = MultiModalConversationParam.builder() + .apiKey(apiKey) + .model("qwen-image-2.0-pro") + .messages(Collections.singletonList(userMessage)) + .parameters(parameters) + .build(); + + MultiModalConversationResult result = conv.call(param); + // 如需查看完整响应,请取消下行注释 + // System.out.println(JsonUtils.toJson(result)); + List> contentList = result.getOutput().getChoices().get(0).getMessage().getContent(); + int imageIndex = 1; + for (Map content : contentList) { + if (content.containsKey("image")) { + System.out.println("输出图像" + imageIndex + "的URL:" + content.get("image")); + imageIndex++; + } + } + } + + public static void main(String[] args) { + try { + call(); + } catch (ApiException | NoApiKeyException | UploadFileException | IOException e) { + System.out.println(e.getMessage()); + } + } +} +``` + +## 通过Base64编码传入图片 + +``` +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult; +import com.alibaba.dashscope.common.MultiModalMessage; +import com.alibaba.dashscope.common.Role; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.exception.UploadFileException; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +public class QwenImageEdit { + + static { + // 以下为华北2(北京)地域的URL。请将 {WorkspaceId} 替换为您的百炼业务空间ID,各地域的URL不同。 + Constants.baseHttpApiUrl = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"; + } + + // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + // 若没有配置环境变量,请用百炼 API Key 将下行替换为:apiKey="sk-xxx" + static String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + public static void call() throws ApiException, NoApiKeyException, UploadFileException, IOException { + + // 请将 "/path/to/your/image.png" 替换为您的本地图片文件路径,否则无法运行 + String image = encodeFile("/path/to/your/image.png"); + + MultiModalConversation conv = new MultiModalConversation(); + + MultiModalMessage userMessage = MultiModalMessage.builder().role(Role.USER.getValue()) + .content(Arrays.asList( + Collections.singletonMap("image", image), + Collections.singletonMap("text", "在画面右下角石板路旁、靠近树干根部的位置,以浅灰墨色手写体题写一首七言绝句,字体为行楷风格,笔触自然流畅、略带飞白,大小适中(约占画面高度1/10),与整体水墨淡雅氛围协调。诗文内容为:“青石桥畔柳风轻, 素手拈花闭目听。 一水碧痕浮旧梦, 半篙烟雨入空舲。”诗句横向排列,四句分两行书写(前两句一行,后两句一行),末句“舲”字右下角钤一枚朱红小印,印文为“江南”二字篆书,尺寸约等于单字高度的1/3。") + )).build(); + // qwen-image-2.0系列、qwen-image-edit-max、qwen-image-edit-plus系列支持输出1-6张图片 + Map parameters = new HashMap<>(); + parameters.put("watermark", false); + parameters.put("negative_prompt", " "); + parameters.put("n", 1); + parameters.put("prompt_extend", true); + parameters.put("size", "2048*2048"); + + MultiModalConversationParam param = MultiModalConversationParam.builder() + .apiKey(apiKey) + .model("qwen-image-2.0-pro") + .messages(Collections.singletonList(userMessage)) + .parameters(parameters) + .build(); + + MultiModalConversationResult result = conv.call(param); + // 如需查看完整响应,请取消下行注释 + // System.out.println(JsonUtils.toJson(result)); + List> contentList = result.getOutput().getChoices().get(0).getMessage().getContent(); + int imageIndex = 1; + for (Map content : contentList) { + if (content.containsKey("image")) { + System.out.println("输出图像" + imageIndex + "的URL:" + content.get("image")); + imageIndex++; + } + } + } + + /** + * 将文件编码为Base64字符串 + * @param filePath 文件路径 + * @return Base64字符串,格式为 data:{mime_type};base64,{base64_data} + */ + public static String encodeFile(String filePath) { + Path path = Paths.get(filePath); + if (!Files.exists(path)) { + throw new IllegalArgumentException("文件不存在: " + filePath); + } + // 检测MIME类型 + String mimeType = null; + try { + mimeType = Files.probeContentType(path); + } catch (IOException e) { + throw new IllegalArgumentException("无法检测文件类型: " + filePath); + } + if (mimeType == null || !mimeType.startsWith("image/")) { + throw new IllegalArgumentException("不支持或无法识别的图像格式"); + } + // 读取文件内容并编码 + byte[] fileBytes = null; + try{ + fileBytes = Files.readAllBytes(path); + } catch (IOException e) { + throw new IllegalArgumentException("无法读取文件内容: " + filePath); + } + + String encodedString = Base64.getEncoder().encodeToString(fileBytes); + return "data:" + mimeType + ";base64," + encodedString; + } + + public static void main(String[] args) { + try { + call(); + } catch (ApiException | NoApiKeyException | UploadFileException | IOException e) { + System.out.println(e.getMessage()); + } + } +} +``` + +## 通过URL下载图像 + +``` +import java.io.FileOutputStream; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +public class ImageDownloader { + public static void downloadImage(String imageUrl, String savePath) { + try { + URL url = new URL(imageUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(5000); + connection.setReadTimeout(300000); + connection.setRequestMethod("GET"); + InputStream inputStream = connection.getInputStream(); + FileOutputStream outputStream = new FileOutputStream(savePath); + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + inputStream.close(); + outputStream.close(); + + System.out.println("图像已成功下载到: " + savePath); + } catch (Exception e) { + System.err.println("图像下载失败: " + e.getMessage()); + } + } + + public static void main(String[] args) { + String imageUrl = "http://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxx?Expires=xxx"; + String savePath = "output.png"; + downloadImage(imageUrl, savePath); + } +} +``` + +#### **响应示例** + +图像链接的有效期为24小时,请及时下载图像。 + +``` +{ + "requestId": "5d5c3260-fc6c-4b4f-8b35-c06366effxxx", + "usage": { + "image_count": 1, + "width": 2048, + "height": 2048 + }, + "output": { + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-sz.oss-cn-shenzhen.aliyuncs.com/xxx.png?Expires=xxx" + } + ] + } + } + ] + }, + "status_code": 200, + "code": "", + "message": "" +} +``` + +## **错误码** + +如果模型调用失败并返回报错信息,请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 + +## **计费与限流** + +- 模型免费额度和计费单价请参见[选择模型](https://help.aliyun.com/zh/model-studio/models#bfe15d8aa2lxh)。 + +- 模型限流请参见[千问(Qwen-Image)](https://help.aliyun.com/zh/model-studio/rate-limit#f812e7c63axvx)。 + +- 计费说明:按成功生成的 **图像张数** 计费。模型调用失败或处理错误不产生任何费用,也不消耗[新人免费额度](https://help.aliyun.com/zh/model-studio/new-free-quota)。 + + +## **常见问题** + +#### **Q:千问图像编辑模型支持哪些语言?** + +A:目前正式支持**简体中文和英文**;其他语言可自行尝试,但效果存在不确定性。 + +##### **Q: 如何查看模型调用量?** + +A: 模型调用完一小时后,请在[**模型监控**(北京)](https://bailian.console.aliyun.com/cn-beijing/?tab=model#/model-telemetry)或[**模型监控**(新加坡)](https://modelstudio.console.aliyun.com/ap-southeast-1?tab=dashboard#/model-telemetry) 页面,查看模型的调用次数、成功率等指标。详情请参见[账单查询与成本管理](https://help.aliyun.com/zh/model-studio/bill-query-and-cost-management)。 + +##### **Q:如何获取图像存储的访问域名白名单?** + +A: 模型生成的图像存储于阿里云OSS,API将返回一个临时的公网URL。**若需要对该下载地址进行防火墙白名单配置**,请注意:由于底层存储会根据业务情况进行动态变更,为避免过期信息影响访问,文档不提供固定的OSS域名白名单。如有安全管控需求,请联系客户经理获取最新OSS域名列表。 + +/\*表格图片设置为块元素(独占一行),居中展示,鼠标放在图片上可以点击查看原图\*/ .unionContainer .markdown-body .image.break { margin: 0px; display: inline-block; vertical-align: middle } /\* 让引用上下间距调小,避免内容显示过于稀疏 \*/ .unionContainer .markdown-body blockquote { margin: 4px 0; } .aliyun-docs-content table.qwen blockquote { border-left: none; /\* 添加这一行来移除表格里的引用文字的左侧边框 \*/ padding-left: 5px; /\* 左侧内边距 \*/ margin: 4px 0; } .gray-text { color: gray !important; font-size: 0.75em !important; /\* 字体减小一半 \*/ } + +/\* 调整 table 宽度 \*/ .aliyun-docs-content table.medium-width { max-width: 1018px; width: 100%; } .aliyun-docs-content table.table-no-border tr td:first-child { padding-left: 0; } .aliyun-docs-content table.table-no-border tr td:last-child { padding-right: 0; } /\* 支持吸顶 \*/ div:has(.aliyun-docs-content), .aliyun-docs-content .markdown-body { overflow: visible; } .stick-top { position: sticky; top: 46px; } /\*\*代码块字体\*\*/ /\* 减少表格中的代码块 margin,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body table .help-code-block { margin: 0 !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre { font-size: 12px !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre code { font-size: 12px !important; } /\*\* API Reference 表格 \*\*/ .aliyun-docs-content table.api-reference tr td:first-child { margin: 0px; border-bottom: 1px solid #d8d8d8; } .aliyun-docs-content table.api-reference tr:last-child td:first-child { border-bottom: none; } .aliyun-docs-content table.api-reference p { color: #6e6e80; } .aliyun-docs-content table.api-reference b, i { color: #181818; } .aliyun-docs-content table.api-reference .collapse { border: none; margin-top: 4px; margin-bottom: 4px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse .expandable-title i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse.expanded .expandable-content { padding: 10px 14px 10px 14px !important; margin: 0; border: 1px solid #e9e9e9; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .collapse .expandable-title b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .tabbed-content-box { border: none; } .aliyun-docs-content table.api-reference .tabbed-content-box section { padding: 8px 0 !important; } .aliyun-docs-content table.api-reference .tabbed-content-box.mini .tab-box { /\* position: absolute; left: 40px; right: 0; \*/ } .aliyun-docs-content .margin-top-33 { margin-top: 33px !important; } .aliyun-docs-content .two-codeblocks pre { max-height: calc(50vh - 136px) !important; height: auto; } .expandable-content section { border-bottom: 1px solid #e9e9e9; padding-top: 6px; padding-bottom: 4px; } .expandable-content section:last-child { border-bottom: none; } .expandable-content section:first-child { padding-top: 0; } \ No newline at end of file diff --git a/docs/协议文档/阿里系列/图像-千问-图像翻译.md b/docs/协议文档/阿里系列/图像-千问-图像翻译.md new file mode 100644 index 0000000..75f4852 --- /dev/null +++ b/docs/协议文档/阿里系列/图像-千问-图像翻译.md @@ -0,0 +1,174 @@ +千问-图像翻译模型(Qwen-MT-Image)可精准翻译图像中的文字,并保留原始排版。该模型还支持领域提示、敏感词过滤、术语干预等自定义功能。 + +**重要** + +本文档描述的功能仅在华北2(北京)地域可用,必须使用该地域的[API Key](https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key)。 + +**重要** + +阿里云百炼为华北2(北京)地域推出了业务空间专属域名 `https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com`,**能够为推理请求提供卓越的性能和更高的稳定性**,建议从 `https://dashscope.aliyuncs.com` 迁移至新域名。 + +其中 `{WorkspaceId}` 为您的业务空间 ID,可在阿里云百炼控制台的**业务空间详情**页面查看。现有域名仍可正常使用。 + +## **模型概览** + +| ![1](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/6962921671/p1007156.webp) 源语种:中文 | ![2](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/6962921671/p1006389.webp) 英文 | ![3](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/6962921671/p1006396.webp) 日文 | +| --- | --- | --- | +| ![4](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/6962921671/p1006438.webp) 韩语 | ![es](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/6962921671/p1006451.webp) 西班牙语 | ![fr](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=) 法语 | + +| **模型名称** | **模型简介** | **输出图像规格** | +| --- | --- | --- | +| qwen-mt-image | 千问-图像翻译模型 支持中/英文与其他语种之间的互译,但不支持在非中/英语种之间直接翻译(例如,从日语翻译为韩语)。详情请参见[支持的语种](#d2aa4b03d2kco)。 | 图片格式:JPG。 | + +## 前提条件 + +您需要已[获取API Key](https://help.aliyun.com/zh/model-studio/get-api-key)并[配置API Key到环境变量](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables)。 + +## HTTP调用 + +`POST https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/image2image/image-synthesis`,调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +HTTP API 采用异步模式,调用流程分两步: + +1. **创建任务获取任务ID**:发送一个请求创建任务,该请求会返回**任务ID(task\_id)**。 + +2. **根据任务ID查询结果**:使用task\_id轮询任务状态,直到任务完成并获得图像URL。 + + +### 步骤1:创建任务获取任务ID + +**说明** + +- 创建成功后,使用接口返回的 `task_id` 查询结果,task\_id 有效期为 24 小时。**请勿重复创建任务**,轮询获取即可。 + +- 新手指引请参见[Postman](https://help.aliyun.com/zh/model-studio/first-call-to-image-and-video-api)。 + + +| #### 请求参数 | ## 图像翻译 ``` # 以下为华北2(北京)地域的URL。请将 {WorkspaceId} 替换为您的百炼业务空间ID,各地域的URL不同。 curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/image2image/image-synthesis' \\ --header 'X-DashScope-Async: enable' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --header 'Content-Type: application/json' \\ --data '{ "model": "qwen-mt-image", "input": { "image_url": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250916/ordhsk/1.webp", "source_lang": "zh", "target_lang": "en", "ext": { "config": { "imageSegment": false } } } }' ``` | +| --- | --- | +| ##### 请求头(Headers) | +| **Content-Type** `*string*` **(必选)** 请求内容类型。此参数必须设置为`application/json`。 | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| **X-DashScope-Async** `*string*` **(必选)** 异步处理配置参数。HTTP请求只支持异步,**必须设置为**`**enable**`。 **重要** 缺少此请求头将报错:“current user api does not support synchronous calls”。 | +| ##### 请求体(Request Body) | +| **model** `*string*` **(必选)** 模型名称,必须设置为`qwen-mt-image`。 | +| **input** `*object*` **(必选)** 输入参数对象,包含以下字段: **属性** **image\\_url** `*string*` **(必选)** 图像的公网可访问的URL,支持 HTTP 和 HTTPS 协议。如需获取本地文件的公网URL,请参见[上传文件获取临时URL](https://help.aliyun.com/zh/model-studio/get-temporary-file-url) 。 - **格式限制**:JPG、JPEG、PNG、BMP、PNM、PPM、TIFF、WEBP - **尺寸限制**:图像的宽度和高度均需在15-8192像素范围内,宽高比在1:10至10:1范围内。 - **大小限制**:不超过100MB - URL地址中不能包含中文字符。 - **示例**:`https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250916/ordhsk/1.webp` **source\\_lang** `*string*` **(必选)** [源语种](#d2aa4b03d2kco)。 - **支持值**:语种全称、语种编码或`auto`(自动检测),对大小写不敏感 - **限制**:与`target_lang`不同,且至少有一项为中文或英文 - **示例**:`Chinese`、`en`、`auto` **target\\_lang** `*string*` **(必选)** [目标语种](#d2aa4b03d2kco)。 - **支持值**:语种全称或语种编码,对大小写不敏感 - **限制**:与`source_lang`不同,且至少有一项为中文或英文 - **示例**:`Chinese`、`en` **ext** `*object*` (可选) 可选拓展字段。 **属性** **domainHint** `*string*` (可选) 领域提示,为使译文风格更贴合特定领域,可以使用英文描述使用场景、译文风格等需求。 为确保翻译效果,建议不超过200个英文单词。 **重要** 领域提示语句当前**只支持英文**。 **示例:**These sentences are from seller-buyer conversations on a B2C ecommerce platform. Translate them into clear, engaging customer service language, ensuring the translation is appropriate for handling potential issues or disputes. **sensitives** `*array*` (可选) 配置敏感词,以在翻译前过滤图片中**完全匹配**的文本,**对大小写敏感**。 敏感词的语种可与源语种不一致,支持全部的[源语种](#ffcebd6c57dfn)和[目标语种](#d2aa4b03d2kco)。为确保翻译效果,建议单次请求添加的敏感词不超过50个。 **示例:**\\["全场9折", "七天无理由退换"\\] **terminologies** `*array*` (可选) 术语干预,为特定术语设定译文,以满足特定领域的翻译需求,术语对的语种需要与`source_lang`和`target_lang`对应。 **属性** **src** `*string*` **(必选)** 术语的源文本,语种需要与源语种`source_lang`一致。 **tgt** `*string*` **(必选)** 术语的目标文本,语种需要与目标语种`target_lang`一致。 **示例**:\\[{"src": "应用程序接口", "tgt": "API"}, {"src": "机器学习", "tgt": "ML"}\\] **config** `*object*` (可选) **属性** **imageSegment** `*bool*` (可选) 是否开启图像主体分割。开启后,将跳过对图像中主体(如人物、商品、Logo)上文字的翻译。 - `false`:(默认值)翻译图像中的所有文字。 - `true`:不翻译图像主体的文字。 > **注意**:旧版本参数名为`skipImgSegment`(是否跳过图像主体分割)。为保持兼容,该参数仍受支持,但建议使用新的 `imageSegment`参数。 | + +| #### 响应参数 | ### 成功响应 请保存 task\\_id,用于查询任务状态与结果。 ``` { "output": { "task_status": "PENDING", "task_id": "0385dc79-5ff8-4d82-bcb6-xxxxxx" }, "request_id": "4909100c-7b5a-9f92-bfe5-xxxxxx" } ``` ### 异常响应 创建任务失败,请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "code": "InvalidApiKey", "message": "No API-key provided.", "request_id": "7438d53d-6eb8-4596-8835-xxxxxx" } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | +| **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | +| **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | + +### 步骤2:根据任务ID查询结果 + +`GET https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/tasks/{task_id}` + +**说明** + +- `task_id` 有效期为**24小时**,若ID不存在或已过期,任务状态将返回 `UNKNOWN`。 + +- 任务成功后返回的 `url`有效期为**24小时**,请及时下载并保存图像。 + +- 此查询接口的默认RPS为1。如需更高频次的查询或事件通知,请[配置异步任务回调](https://help.aliyun.com/zh/model-studio/async-task-api)。 + +- 如需批量查询或取消任务,请参见[管理异步任务](https://help.aliyun.com/zh/model-studio/manage-asynchronous-tasks#f26499d72adsl)。 + + +| #### 请求参数 | ## 查询任务结果 您需要将`86ecf553-d340-4e21-xxxxxxxxx`替换为真实的task\\_id。 ``` # 以下为华北2(北京)地域的URL。请将 {WorkspaceId} 替换为您的百炼业务空间ID,各地域的URL不同。 curl -X GET https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/tasks/86ecf553-d340-4e21-xxxxxxxxx \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" ``` | +| --- | --- | +| ##### **请求头(Headers)** | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| ##### **URL路径参数(Path parameters)** | +| **task\\_id** `*string*`**(必选)** 任务ID。 | + +| #### **响应参数** | ## 任务执行成功-存在可翻译内容 任务数据(如任务状态、图像URL等)仅保留24小时,超时后会被自动清除。请您务必及时保存生成的图像。 ``` { "request_id": "5fec62eb-bf94-91f8-b9f4-f7f758e4e27e", "output": { "task_id": "72c52225-8444-4cab-ad0c-xxxxxx", "task_status": "SUCCEEDED", "submit_time": "2025-08-13 18:11:16.954", "scheduled_time": "2025-08-13 18:11:17.003", "end_time": "2025-08-13 18:11:23.860", "image_url": "http://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxx?Expires=xxx" }, "usage": { "image_count":1 } } ``` ## 任务执行成功-无可翻译内容 在图像中无可翻译文本(例如,在识别出图像主体后,其余部分无文字)时,**任务仍会成功并正常计费**,但会返回`No text detected for translation`的提示。 ``` { "request_id": "0ccb84aa-e034-431d-9d54-08e14fxxxxxx", "output": { "task_id": "34ec4208-97d6-498b-a390-9173f7xxxxxx", "task_status": "SUCCEEDED", "submit_time": "2026-01-12 17:56:23.625", "scheduled_time": "2026-01-12 17:56:23.656", "end_time": "2026-01-12 17:56:25.324", "image_url": "http://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxx.jpg?Expires=xxx", "message": "No text detected for translation" }, "usage": { "image_count": 1 } } ``` ## 任务执行失败 若任务执行失败,task\\_status将置为 FAILED,并提供错误码和信息。请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "request_id": "daad9007-6acd-9fb3-a6bc-d55902b9c9ff", "output": { "task_id": "fe8aa114-d9f1-4f76-b598-xxxxxx", "task_status": "FAILED", "submit_time": "2025-08-20 09:54:21.911", "scheduled_time": "2025-08-20 09:54:21.984", "end_time": "2025-08-20 12:55:00.818", "code": "InternalError", "message": "xxxxxx" } } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 **submit\\_time** `*string*` 任务提交时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **scheduled\\_time** `*string*` 任务执行时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **end\\_time** `*string*` 任务完成时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **image\\_url** `*string*` 模型生成图像的URL地址,与原图长宽相同,JPG格式。有效期为24小时,请及时下载并保存图像。 **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 **message** `*string*` 请求失败的详细信息,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 通常请求成功时不会返回此参数,仅在图像中无可翻译文本(例如,在分割图像主体后,其余部分无文字)时,**任务仍会成功并正常计费**,但会返回`No text detected for translation`的提示。 | +| **usage** `*object*` 输出信息统计。只对成功的结果计数。 **属性** **image\\_count** `*integer*` 模型生成图像的数量,固定为1。 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | + +## **支持的语种** + +进行图像翻译时,**源语种或目标语种必须至少有一种是中文或英文**。不支持在两个非中、英语种之间直接翻译(例如,从日语翻译为韩语)。若不确定源语种,可将 `source_lang` 设置为 `auto` 进行自动检测。 + +| **语种(中文名)** | **英文全称** | **编码** | **支持作为源语种** | **支持作为目标语种** | +| --- | --- | --- | --- | --- | +| 简体中文 | Chinese | zh | 支持 | 支持 | +| 英文 | English | en | 支持 | 支持 | +| 韩语 | Korean | ko | 支持 | 支持 | +| 日语 | Japanese | ja | 支持 | 支持 | +| 俄语 | Russian | ru | 支持 | 支持 | +| 西班牙语 | Spanish | es | 支持 | 支持 | +| 法语 | French | fr | 支持 | 支持 | +| 葡萄牙语 | Portuguese | pt | 支持 | 支持 | +| 意大利语 | Italian | it | 支持 | 支持 | +| 德语 | German | de | 支持 | 不支持 | +| 越南语 | Vietnamese | vi | 支持 | 支持 | +| 马来语 | Malay | ms | 不支持 | 支持 | +| 泰语 | Thai | th | 不支持 | 支持 | +| 印尼语 | Indonesian | id | 不支持 | 支持 | +| 阿拉伯语 | Arabian | ar | 不支持 | 支持 | + +## **计费与限流** + +- 模型免费额度和计费单价请参见[模型价格](https://help.aliyun.com/zh/model-studio/model-pricing#a3250eda6bgjx)。 + +- 模型限流请参见[限流](https://help.aliyun.com/zh/model-studio/rate-limit#f812e7c63axvx)。 + +- 计费说明:按成功生成的**图像张数**计费。模型调用失败或处理错误不产生任何费用,也不消耗[新人免费额度](https://help.aliyun.com/zh/model-studio/new-free-quota)。 + +- **注意**:如果图像中无可翻译文本,或在启用主体识别功能后,非主体部分无文字时,**任务仍记为成功**并**正常计费**,此时接口会返回`No text detected for translation`的提示。 + + +## **错误码** + +如果模型调用失败并返回报错信息,请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 + +## **常见问题** + +#### **Q:为什么图中的内容没有被翻译?** + +A:因为启用了[主体分割](#245ec24fe92ls)功能,模型不会翻译图片中人物、商品、Logo等主体上的文字。若需翻译所有文字,请将`ext.config.imgSegment`参数设置为`false`。 + +#### **Q:如何将临时的图像链接转为永久链接?** + +A:临时链接无法直接转为永久链接。需通过后端服务下载图像,再上传至对象存储服务(如阿里云 OSS)以生成新的永久链接。 + +**示例代码:下载图像到本地** + +``` +import requests + +def download_and_save_image(image_url, save_path): + try: + response = requests.get(image_url, stream=True, timeout=300) # 设置超时 + response.raise_for_status() # 如果HTTP状态码不是200,则引发异常 + with open(save_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + print(f"图像已成功下载到: {save_path}") + # 此处可以接上传到永久存储的逻辑 + except requests.exceptions.RequestException as e: + print(f"图像下载失败: {e}") + +if __name__ == '__main__': + image_url = "http://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxx?Expires=xxx" + save_path = "image-translation.jpg" + download_and_save_image(image_url, save_path) +``` + +### **Q: 如何查看模型调用量?** + +A: 模型调用完一小时后,请在[**模型监控**(北京)](https://bailian.console.aliyun.com/cn-beijing/?tab=model#/model-telemetry)或[**模型监控**(新加坡)](https://modelstudio.console.aliyun.com/ap-southeast-1?tab=dashboard#/model-telemetry) 页面,查看模型的调用次数、成功率等指标。详情请参见[账单查询与成本管理](https://help.aliyun.com/zh/model-studio/bill-query-and-cost-management)。 + +### **Q:如何获取图像存储的访问域名白名单?** + +A: 模型生成的图像存储于阿里云OSS,API将返回一个临时的公网URL。**若需要对该下载地址进行防火墙白名单配置**,请注意:由于底层存储会根据业务情况进行动态变更,为避免过期信息影响访问,文档不提供固定的OSS域名白名单。如有安全管控需求,请联系客户经理获取最新OSS域名列表。 + +/\*表格图片设置为块元素(独占一行),居中展示,鼠标放在图片上可以点击查看原图\*/ .unionContainer .markdown-body .image.break { margin: 0px; display: inline-block; vertical-align: middle } + +/\* 调整 table 宽度 \*/ .aliyun-docs-content table.medium-width { max-width: 1018px; width: 100%; } .aliyun-docs-content table.table-no-border tr td:first-child { padding-left: 0; } .aliyun-docs-content table.table-no-border tr td:last-child { padding-right: 0; } /\* 支持吸顶 \*/ div:has(.aliyun-docs-content), .aliyun-docs-content .markdown-body { overflow: visible; } .stick-top { position: sticky; top: 46px; } /\*\*代码块字体\*\*/ /\* 减少表格中的代码块 margin,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body table .help-code-block { margin: 0 !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre { font-size: 12px !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre code { font-size: 12px !important; } /\*\* API Reference 表格 \*\*/ .aliyun-docs-content table.api-reference tr td:first-child { margin: 0px; border-bottom: 1px solid #d8d8d8; } .aliyun-docs-content table.api-reference tr:last-child td:first-child { border-bottom: none; } .aliyun-docs-content table.api-reference p { color: #6e6e80; } .aliyun-docs-content table.api-reference b, i { color: #181818; } .aliyun-docs-content table.api-reference .collapse { border: none; margin-top: 4px; margin-bottom: 4px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse .expandable-title i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse.expanded .expandable-content { padding: 10px 14px 10px 14px !important; margin: 0; border: 1px solid #e9e9e9; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .collapse .expandable-title b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .tabbed-content-box { border: none; } .aliyun-docs-content table.api-reference .tabbed-content-box section { padding: 8px 0 !important; } .aliyun-docs-content table.api-reference .tabbed-content-box.mini .tab-box { /\* position: absolute; left: 40px; right: 0; \*/ } .aliyun-docs-content .margin-top-33 { margin-top: 33px !important; } .aliyun-docs-content .two-codeblocks pre { max-height: calc(50vh - 136px) !important; height: auto; } .expandable-content section { border-bottom: 1px solid #e9e9e9; padding-top: 6px; padding-bottom: 4px; } .expandable-content section:last-child { border-bottom: none; } .expandable-content section:first-child { padding-top: 0; } + +span.aliyun-docs-icon { color: transparent !important; font-size: 0 !important; } span.aliyun-docs-icon:before { color: black; font-size: 16px; } span.aliyun-docs-icon.icon-size-20:before { font-size: 20px; } span.aliyun-docs-icon.icon-size-22:before { font-size: 22px; } span.aliyun-docs-icon.icon-size-24:before { font-size: 24px; } span.aliyun-docs-icon.icon-size-26:before { font-size: 26px; } span.aliyun-docs-icon.icon-size-28:before { font-size: 28px; } \ No newline at end of file diff --git a/docs/协议文档/阿里系列/图像-千问-文生图.md b/docs/协议文档/阿里系列/图像-千问-文生图.md new file mode 100644 index 0000000..54f7a43 --- /dev/null +++ b/docs/协议文档/阿里系列/图像-千问-文生图.md @@ -0,0 +1,969 @@ +千问-文生图模型(Qwen-Image)是一款通用图像生成模型,支持多种艺术风格,尤其擅长**复杂文本渲染**。模型支持多行布局、段落级文本生成以及细粒度细节刻画,可实现复杂的图文混合布局设计。 + +| **快速入口:**[使用指南](https://help.aliyun.com/zh/model-studio/text-to-image) **\\|** 在线体验([北京](https://bailian.console.aliyun.com/cn-beijing/?tab=model#/efm/model_experience_center/vision?currentTab=imageGenerate&modelId=qwen-image-max) \\| [新加坡](https://modelstudio.console.aliyun.com/ap-southeast-1?tab=dashboard#/efm/model_experience_center/vision?currentTab=imageGenerate&modelId=qwen-image-max)) **\\|** [技术博客](https://qwen.ai/blog?id=9467b4bff9c638e847f08443802c6b96ab116a87&from=research.research-list) | +| --- | --- | --- | --- | + +## **效果展示** + +| **输入提示词** | **输出图像** | +| --- | --- | +| 冬日北京的都市街景,青灰瓦顶、朱红色外墙的两间相邻中式商铺比肩而立,檐下悬挂印有剪纸马的暖光灯笼,在阴天漫射光中投下柔和光晕,映照湿润鹅卵石路面泛起细腻反光。左侧为书法店:靛蓝色老旧的牌匾上以遒劲行书刻着“文字渲染”。店门口的玻璃上挂着一幅字,自上而下,用田英章硬笔写着“专业幻灯片 中英文海报 高级信息图”,落款印章为“1k token”朱砂印。店内的墙上,可以模糊的辨认有三幅竖排的书法作品,第一幅写着“阿里巴巴”,第二幅写着“通义千问”,第三幅写着“图像生成”。一位白发苍苍的老人背对着镜头观赏。右侧为花店,牌匾上以鲜花做成文字“真实质感”;店内多层花架陈列红玫瑰、粉洋牡丹和绿植,门上贴了一个圆形花边标识,标识上写着“2k resolution”,门口摆放了一个彩色霓虹灯,上面写着“细腻刻画 人物 自然 建筑”。两家店中间堆放了一个雪人,举了一老式小黑板,上面用粉笔字写着“Qwen-Image-2.0 正式发布”。街道左侧,年轻情侣依偎在一起,女孩是瘦脸,身穿米白色羊绒大衣,肉色光腿神器。女孩举着心形透明气球,气球印有白色的字:“生图编辑二合一”。里面有一个毛茸茸的卡皮巴拉玩偶。男孩身着剪裁合体的深灰色呢子外套,内搭浅色高领毛衣。街道右侧,一个后背上写着“更小模型,更快速度”的骑手疾驰而过。整条街光影交织、动静相宜。 | ![image (10)](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/9373463771/p1058343.png) | + +## 模型概览 + +| **模型名称** | **模型简介** | **输出图像规格** | +| --- | --- | --- | +| qwen-image-2.0-pro `**推荐**` > 当前与qwen-image-2.0-pro-2026-04-22能力相同 | 千问图像生成与编辑模型Pro系列。文字渲染、真实质感、语义遵循能力更强。 > 图像编辑请参考[千问-图像编辑](https://help.aliyun.com/zh/model-studio/qwen-image-edit-api)。 | 图像分辨率:支持自由设置宽高,输出图像总像素需在512\\*512至2048\\*2048之间。默认分辨率为2048\\*2048。 图像格式:png 图像张数:1-6张 | +| qwen-image-2.0-pro-2026-06-22 `**推荐**` | +| qwen-image-2.0-pro-2026-04-22 | +| qwen-image-2.0-pro-2026-03-03 | +| qwen-image-2.0 `**推荐**` > 当前与qwen-image-2.0-2026-03-03能力相同 | 千问图像生成与编辑模型加速版,兼顾效果与响应速度。 > 图像编辑请参考[千问-图像编辑](https://help.aliyun.com/zh/model-studio/qwen-image-edit-api)。 | +| qwen-image-2.0-2026-03-03 `**推荐**` | +| qwen-image-max > 当前与qwen-image-max-2025-12-30能力相同 | 千问图像生成模型Max系列。真实感、自然度更强,AI合成痕迹更低。 | 图像分辨率:可选分辨率及对应宽高比例请参见[size参数设置](#1c7b41f2d13sv) 图像格式:png 图像张数:固定1张 | +| qwen-image-max-2025-12-30 | +| qwen-image-plus > 当前与qwen-image能力相同 | 千问图像生成模型Plus系列,擅长多样化艺术风格与文字渲染。 | +| qwen-image-plus-2026-01-09 | +| qwen-image | + +各地域支持的模型请参见[百炼控制台](https://bailian.console.aliyun.com/cn-beijing?tab=model#/model-market/all)。 + +## 前提条件 + +在调用前,先[获取API Key](https://help.aliyun.com/zh/model-studio/get-api-key),再[配置API Key到环境变量](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables)。如需通过SDK进行调用,请[安装DashScope SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。 + +**重要** + +华北2(北京)和新加坡地域拥有独立的 **API Key** 与**请求地址**,不可混用,跨地域调用将导致鉴权失败或服务报错。 + +**重要** + +阿里云百炼为华北2(北京)、新加坡地域推出了业务空间专属域名,**能够为推理请求提供卓越的性能和更高的稳定性**,建议迁移至新域名: + +- 华北2(北京)地域:从 `https://dashscope.aliyuncs.com` 迁移至 `https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com` + +- 新加坡地域:从 `https://dashscope-intl.aliyuncs.com` 迁移至 `https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com` + + +其中 `{WorkspaceId}` 为您的业务空间 ID,可在阿里云百炼控制台的**业务空间详情**页面查看。现有域名仍可正常使用。 + +## **同步接口(推荐)** + +### **HTTP调用** + +千问图像模型支持同步接口,一次请求即可获得结果,调用流程简单,推荐用于多数场景。 + +**北京地域**:`POST https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` + +**新加坡地域**:`POST https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +| #### 请求参数 | ## **文生图** ``` curl --location 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \\ --header 'Content-Type: application/json' \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" \\ --data '{ "model": "qwen-image-2.0-pro", "input": { "messages": [ { "role": "user", "content": [ { "text": "冬日北京的都市街景,青灰瓦顶、朱红色外墙的两间相邻中式商铺比肩而立,檐下悬挂印有剪纸马的暖光灯笼,在阴天漫射光中投下柔和光晕,映照湿润鹅卵石路面泛起细腻反光。左侧为书法店:靛蓝色老旧的牌匾上以遒劲行书刻着“文字渲染”。店门口的玻璃上挂着一幅字,自上而下,用田英章硬笔写着“专业幻灯片 中英文海报 高级信息图”,落款印章为“1k token”朱砂印。店内的墙上,可以模糊的辨认有三幅竖排的书法作品,第一幅写着“阿里巴巴”,第二幅写着“通义千问”,第三幅写着“图像生成”。一位白发苍苍的老人背对着镜头观赏。右侧为花店,牌匾上以鲜花做成文字“真实质感”;店内多层花架陈列红玫瑰、粉洋牡丹和绿植,门上贴了一个圆形花边标识,标识上写着“2k resolution”,门口摆放了一个彩色霓虹灯,上面写着“细腻刻画 人物 自然 建筑”。两家店中间堆放了一个雪人,举了一老式小黑板,上面用粉笔字写着“Qwen-Image-2.0 正式发布”。街道左侧,年轻情侣依偎在一起,女孩是瘦脸,身穿米白色羊绒大衣,肉色光腿神器。女孩举着心形透明气球,气球印有白色的字:“生图编辑二合一”。里面有一个毛茸茸的卡皮巴拉玩偶。男孩身着剪裁合体的深灰色呢子外套,内搭浅色高领毛衣。街道右侧,一个后背上写着“更小模型,更快速度”的骑手疾驰而过。整条街光影交织、动静相宜。" } ] } ] }, "parameters": { "negative_prompt": "低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感。构图混乱。文字模糊,扭曲。", "prompt_extend": true, "watermark": false, "size": "2048*2048" } }' ``` | +| --- | --- | +| ##### 请求头(Headers) | +| **Content-Type** `*string*` **(必选)** 请求内容类型。此参数必须设置为`application/json`。 | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| ##### 请求体(Request Body) | +| **model** `*string*` **(必选)** 模型名称。示例值:`qwen-image-2.0-pro`。 | +| **input** `*object*` **(必选)** 输入的基本信息。 **属性** **messages** `*array*` **(必选)** 请求内容数组。**当前仅支持单轮对话**,数组内**有且只有一个元素**。 **属性** **role** `*string*` **(必选)** 消息的角色。此参数必须设置为`user`。 **content** `*array*` **(必选)** 消息内容数组。 **属性** **text** `*string*` **(必选)** 正向提示词用于描述您期望生成的图像内容、风格和构图。 支持中英文,qwen-image-2.0系列模型长度上限为 1300 Token,其他模型为 800 Token,超出部分将自动截断。 **注意**:仅支持传入一个text,不传或传入多个将报错。 | +| **parameters** `*object*` (可选) 图像处理参数。 **属性** **negative\\_prompt** `*string*` (可选) 反向提示词,用于描述不希望在图像中出现的内容,对画面进行限制。 支持中英文,长度不超过500个字符,超出部分将自动截断。 示例值:低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感。构图混乱。文字模糊,扭曲。 **size** `*string*` (可选) 输出图像的分辨率,格式为`宽*高`。 **qwen-image-2.0系列模型**:输出图像总像素需在`512*512`至`2048*2048`之间,默认分辨率为`2048*2048`。推荐分辨率: - `2688*1536` :16:9 - `1536*2688` :9:16 - `2048*2048`(**默认值)**:1:1 - `2368*1728` :4:3 - `1728*2368` :3:4 **qwen-image-max、qwen-image-plus系列模型**:默认分辨率为`1664*928`。**可选**的分辨率及其对应的图像宽高比例为: - `1664*928`(**默认值**):16:9 - `1472*1104`:4:3 - `1328*1328`:1:1 - `1104*1472`:3:4 - `928*1664`:9:16 **n** `*integer*` (可选) 输出图像的数量,默认值为1。 对于qwen-image-2.0系列模型,可选择输出1-6张图片。 对于qwen-image-max、qwen-image-plus系列模型,此参数固定为1,设置其他值将导致报错。 **prompt\\_extend** `*bool*` (可选) 是否开启 Prompt(提示词)智能改写功能。开启后模型将对正向提示词进行优化与润色。此功能不会修改反向提示词。 - `true`:**默认值**,开启智能改写。如果希望图像内容更多样化,由模型补充细节,建议开启此选项。 - `false`:关闭智能改写。如果图像细节更可控,建议关闭此选项,并参考[文生图Prompt指南](https://help.aliyun.com/zh/model-studio/text-to-image-prompt)进行优化, 点击查看改写示例 > 当前仅异步接口返回实际提示词。 **原始提示词(orig\\_prompt)**:一只坐着的橘黄色的猫,表情愉悦,活泼可爱,逼真准确。 **实际提示词(actual\\_prompt)**:一只坐着的橘黄色猫咪,毛发蓬松柔软,阳光透过窗户洒在它身上,呈现出温暖的光泽。猫咪体型匀称,四肢自然弯曲,稳稳地坐在木质地板上,尾巴轻轻卷曲在身侧,显得格外放松而优雅。它的大眼睛圆润明亮,瞳孔微微收缩,流露出愉悦而灵动的神情,嘴角微扬,仿佛正享受着美好的时光。耳朵微微向前倾斜,透露出活泼与好奇。背景是一间温馨的现代家居客厅,浅色木地板、一扇半开的窗户透进柔和的自然光,窗外可见绿意盎然的庭院,窗台上摆放着几盆绿植。画面采用真实摄影风格,细节逼真,光影层次丰富,突出猫咪的毛发质感、眼神神态与整体姿态的生动自然,整体氛围轻松愉快,充满生活气息。 **watermark** `*bool*` (可选) 是否在图像右下角添加 "Qwen-Image" 水印。默认值为 `false`。水印样式:![1](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/8972029571/p1012089.jpg) **seed** `*integer*` (可选) 随机数种子,取值范围`[0,2147483647]`。 使用相同的`seed`参数值可使生成内容保持相对稳定。若不提供,算法将自动使用随机数种子。 **注意**:模型生成过程具有概率性,即使使用相同的`seed`,也不能保证每次生成结果完全一致。 | + +| #### 响应参数 | ## 任务执行成功 图像URL仅保留24小时,超时后会被自动清除,请及时保存生成的图像。 ``` { "output": { "choices": [ { "finish_reason": "stop", "message": { "content": [ { "image": "https://dashscope-result-sh.oss-cn-shanghai.aliyuncs.com/xxx.png?Expires=xxx" } ], "role": "assistant" } } ] }, "usage": { "height": 2048, "image_count": 1, "width": 2048 }, "request_id": "d0250a3d-b07f-49e1-bdc8-6793f4929xxx" } ``` ## 任务执行异常 如果因为某种原因导致任务执行失败,将返回相关信息,可以通过code和message字段明确指示错误原因。请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "request_id": "a4d78a5f-655f-9639-8437-xxxxxx", "code": "InvalidParameter", "message": "num_images_per_prompt must be 1" } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **choices** `*array*` 模型生成的输出内容。此数组仅包含一个元素。 **属性** **finish\\_reason** `*string*` 任务停止原因,自然停止时为`stop`。 **message** `*object*` 模型返回的消息。 **属性** **role** `*string*` 消息的角色,固定为`assistant`。 **content** `*array*` **属性** **image** `*string*` 生成图像的 URL,图像格式为PNG。**链接有效期为24小时**,请及时下载并保存图像。 **task\\_metric** `*object*` 任务结果统计。使用qwen-image-2.0系列时无此返回值。 **属性** **TOTAL** `*integer*` 总的任务数。 **SUCCEEDED** `*integer*` 任务状态为成功的任务数。 **FAILED** `*integer*` 任务状态为失败的任务数。 | +| **usage** `*object*` 输出信息统计。只对成功的结果计数。 **属性** **image\\_count** `*integer*` 模型生成图像的数量,当前固定为1。 **width** `*integer*` 模型生成图像的宽度(像素)。 **height** `*integer*` 模型生成图像的高度(像素)。 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | +| **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | +| **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | + +* * * + +### **DashScope SDK调用** + +DashScope SDK目前已支持Python和Java。 + +SDK与HTTP接口的参数名基本一致,参数结构根据语言特性进行封装。同步调用参数说明可参考[HTTP调用](#90575c8228nmq)。 + +## Python + +**说明** + +请先确认已安装最新版DashScope Python SDK,否则可能运行报错:[安装SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。 + +##### **请求示例** + +``` +import json +import os +import dashscope +from dashscope import MultiModalConversation + +# 以下为华北2(北京)地域的URL,调用时请将 {WorkspaceId} 替换为真实的业务空间ID,各地域的URL不同。 +dashscope.base_http_api_url = 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1' + +messages = [ + { + "role": "user", + "content": [ + {"text": "冬日北京的都市街景,青灰瓦顶、朱红色外墙的两间相邻中式商铺比肩而立,檐下悬挂印有剪纸马的暖光灯笼,在阴天漫射光中投下柔和光晕,映照湿润鹅卵石路面泛起细腻反光。左侧为书法店:靛蓝色老旧的牌匾上以遒劲行书刻着“文字渲染”。店门口的玻璃上挂着一幅字,自上而下,用田英章硬笔写着“专业幻灯片 中英文海报 高级信息图”,落款印章为“1k token”朱砂印。店内的墙上,可以模糊的辨认有三幅竖排的书法作品,第一幅写着“阿里巴巴”,第二幅写着“通义千问”,第三幅写着“图像生成”。一位白发苍苍的老人背对着镜头观赏。右侧为花店,牌匾上以鲜花做成文字“真实质感”;店内多层花架陈列红玫瑰、粉洋牡丹和绿植,门上贴了一个圆形花边标识,标识上写着“2k resolution”,门口摆放了一个彩色霓虹灯,上面写着“细腻刻画 人物 自然 建筑”。两家店中间堆放了一个雪人,举了一老式小黑板,上面用粉笔字写着“Qwen-Image-2.0 正式发布”。街道左侧,年轻情侣依偎在一起,女孩是瘦脸,身穿米白色羊绒大衣,肉色光腿神器。女孩举着心形透明气球,气球印有白色的字:“生图编辑二合一”。里面有一个毛茸茸的卡皮巴拉玩偶。男孩身着剪裁合体的深灰色呢子外套,内搭浅色高领毛衣。街道右侧,一个后背上写着“更小模型,更快速度”的骑手疾驰而过。整条街光影交织、动静相宜。"} + ] + } +] + +# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx" +api_key = os.getenv("DASHSCOPE_API_KEY") + +response = MultiModalConversation.call( + api_key=api_key, + model="qwen-image-2.0-pro", + messages=messages, + result_format='message', + stream=False, + watermark=False, + prompt_extend=True, + negative_prompt="低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感。构图混乱。文字模糊,扭曲。", + size='2048*2048' +) + +if response.status_code == 200: + print(json.dumps(response, ensure_ascii=False)) +else: + print(f"HTTP返回码:{response.status_code}") + print(f"错误码:{response.code}") + print(f"错误信息:{response.message}") + print("请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code") +``` + +##### **响应示例** + +> 图像链接的有效期为24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "d2d1a8c0-325f-9b9d-8b90-xxxxxx", + "code": "", + "message": "", + "output": { + "text": null, + "finish_reason": null, + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-wlcb.oss-cn-wulanchabu.aliyuncs.com/xxx.png?Expires=xxx" + } + ] + } + } + ] + }, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "width": 2048, + "image_count": 1, + "height": 2048 + } +} +``` + +## Java + +**说明** + +请先确认已安装最新版DashScope Java SDK,否则可能运行报错:[安装SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。 + +##### **请求示例** + +``` +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam; +import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult; +import com.alibaba.dashscope.common.MultiModalMessage; +import com.alibaba.dashscope.common.Role; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.exception.UploadFileException; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class QwenImage { + + static { + // 以下为华北2(北京)地域的URL,调用时请将 {WorkspaceId} 替换为真实的业务空间ID,各地域的URL不同。 + Constants.baseHttpApiUrl = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"; + } + + // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + // 若没有配置环境变量,请用百炼API Key将下行替换为:static String apiKey ="sk-xxx" + static String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + public static void call() throws ApiException, NoApiKeyException, UploadFileException, IOException { + + MultiModalConversation conv = new MultiModalConversation(); + + MultiModalMessage userMessage = MultiModalMessage.builder().role(Role.USER.getValue()) + .content(Arrays.asList( + Collections.singletonMap("text", "冬日北京的都市街景,青灰瓦顶、朱红色外墙的两间相邻中式商铺比肩而立,檐下悬挂印有剪纸马的暖光灯笼,在阴天漫射光中投下柔和光晕,映照湿润鹅卵石路面泛起细腻反光。左侧为书法店:靛蓝色老旧的牌匾上以遒劲行书刻着“文字渲染”。店门口的玻璃上挂着一幅字,自上而下,用田英章硬笔写着“专业幻灯片 中英文海报 高级信息图”,落款印章为“1k token”朱砂印。店内的墙上,可以模糊的辨认有三幅竖排的书法作品,第一幅写着“阿里巴巴”,第二幅写着“通义千问”,第三幅写着“图像生成”。一位白发苍苍的老人背对着镜头观赏。右侧为花店,牌匾上以鲜花做成文字“真实质感”;店内多层花架陈列红玫瑰、粉洋牡丹和绿植,门上贴了一个圆形花边标识,标识上写着“2k resolution”,门口摆放了一个彩色霓虹灯,上面写着“细腻刻画 人物 自然 建筑”。两家店中间堆放了一个雪人,举了一老式小黑板,上面用粉笔字写着“Qwen-Image-2.0 正式发布”。街道左侧,年轻情侣依偎在一起,女孩是瘦脸,身穿米白色羊绒大衣,肉色光腿神器。女孩举着心形透明气球,气球印有白色的字:“生图编辑二合一”。里面有一个毛茸茸的卡皮巴拉玩偶。男孩身着剪裁合体的深灰色呢子外套,内搭浅色高领毛衣。街道右侧,一个后背上写着“更小模型,更快速度”的骑手疾驰而过。整条街光影交织、动静相宜。") + )).build(); + + Map parameters = new HashMap<>(); + parameters.put("watermark", false); + parameters.put("prompt_extend", true); + parameters.put("negative_prompt", "低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感。构图混乱。文字模糊,扭曲。"); + parameters.put("size", "2048*2048"); + + MultiModalConversationParam param = MultiModalConversationParam.builder() + .apiKey(apiKey) + .model("qwen-image-2.0-pro") + .messages(Collections.singletonList(userMessage)) + .parameters(parameters) + .build(); + + MultiModalConversationResult result = conv.call(param); + System.out.println(JsonUtils.toJson(result)); + } + + public static void main(String[] args) { + try { + call(); + } catch (ApiException | NoApiKeyException | UploadFileException | IOException e) { + System.out.println(e.getMessage()); + } + System.exit(0); + } +} +``` + +##### **响应示例** + +> 图像链接的有效期为24小时,请及时下载图像。 + +``` +{ + "requestId": "5b6f2d04-b019-40db-a5cc-xxxxxx", + "usage": { + "image_count": 1, + "width": 2048, + "height": 2048 + }, + "output": { + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [ + { + "image": "https://dashscope-result-wlcb.oss-cn-wulanchabu.aliyuncs.com/xxx.png?Expires=xxx" + } + ] + } + } + ] + } +} +``` + +## **异步接口** + +**重要** + +当前仅qwen-image-plus、qwen-image模型支持异步接口调用。 + +### **HTTP调用** + +调用流程分为两步: + +1. **创建任务获取任务ID**:发送一个请求创建任务,该请求会返回**任务ID(task\_id)**。 + +2. **根据任务ID查询结果**:使用task\_id轮询任务状态,直到任务完成并获得图像URL。 + + +#### **步骤1:创建任务获取任务ID** + +**北京地域**:`POST https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis` + +**新加坡地域**:`POST https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis` + +**说明** + +- 创建成功后,使用接口返回的 `task_id` 查询结果,task\_id 有效期为 24 小时。**请勿重复创建任务**,轮询获取即可。 + +- 新手指引请参见[Postman](https://help.aliyun.com/zh/model-studio/first-call-to-image-and-video-api)。 + + +| ##### **请求参数** | ## 文生图 当前仅`qwen-image-plus`、`qwen-image`模型支持异步接口调用。 ``` curl -X POST https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis \\ -H 'X-DashScope-Async: enable' \\ -H "Authorization: Bearer $DASHSCOPE_API_KEY" \\ -H 'Content-Type: application/json' \\ -d '{ "model": "qwen-image-plus", "input": { "prompt": "一副典雅庄重的对联悬挂于厅堂之中,房间是个安静古典的中式布置,桌子上放着一些青花瓷,对联上左书“义本生知人机同道善思新”,右书“通云赋智乾坤启数高志远”, 横批“智启千问”,字体飘逸,在中间挂着一幅中国风的画作,内容是岳阳楼。" }, "parameters": { "negative_prompt":" ", "size": "1664*928", "n": 1, "prompt_extend": true, "watermark": false } }' ``` | +| --- | --- | +| ###### **请求头(Headers)** | +| **Content-Type** `*string*` **(必选)** 请求内容类型。此参数必须设置为`application/json`。 | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| **X-DashScope-Async** `*string*` **(必选)** 异步处理配置参数。HTTP请求只支持异步,**必须设置为**`**enable**`。 **重要** 缺少此请求头将报错:“current user api does not support synchronous calls”。 | +| ###### **请求体(Request Body)** | +| **model** `*string*` **(必选)** 模型名称。当前仅`qwen-image-plus`、`qwen-image`模型支持异步接口调用。 示例值:`qwen-image-plus`。 | +| **input** `*object*` **(必选)** 输入的基本信息,如提示词等。 **属性** **prompt** `*string*` **(必选)** 正向提示词,用来描述生成图像中期望包含的元素和视觉特点。 支持中英文,长度不超过800个字符,每个汉字、字母、数字或符号计为一个字符,超出部分将自动截断。 示例值:一只坐着的橘黄色的猫,表情愉悦,活泼可爱,逼真准确。 **negative\\_prompt** `*string*` (可选) 反向提示词,用于描述不希望在图像中出现的内容,对画面进行限制。 支持中英文,长度不超过500个字符,超出部分将自动截断。 示例值:低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感。构图混乱。文字模糊,扭曲。 | +| **parameters** `*object*` (可选) 图像处理参数。 **属性** **size** `*string*` (可选) 输出图像的分辨率,格式为`宽*高`。 **qwen-image-2.0系列模型**:输出图像总像素需在`512*512`至`2048*2048`之间,默认分辨率为`2048*2048`。推荐分辨率: - `2688*1536` :16:9 - `1536*2688` :9:16 - `2048*2048`(**默认值)**:1:1 - `2368*1728` :4:3 - `1728*2368` :3:4 **qwen-image-max、qwen-image-plus系列模型**:默认分辨率为`1664*928`。**可选**的分辨率及其对应的图像宽高比例为: - `1664*928`(**默认值**):16:9 - `1472*1104`:4:3 - `1328*1328`:1:1 - `1104*1472`:3:4 - `928*1664`:9:16 **n** `*integer*` (可选) 生成图像的数量。**此参数当前固定为1,设置其他值将导致报错。** **prompt\\_extend** `*bool*` (可选) 是否开启 Prompt(提示词)智能改写功能。开启后模型将对正向提示词进行优化与润色。此功能不会修改反向提示词。 - `true`:**默认值**,开启智能改写。如果希望图像内容更多样化,由模型补充细节,建议开启此选项。 - `false`:关闭智能改写。如果图像细节更可控,建议关闭此选项,并参考[文生图Prompt指南](https://help.aliyun.com/zh/model-studio/text-to-image-prompt)进行优化, 点击查看改写示例 > 当前仅异步接口返回实际提示词。 **原始提示词(orig\\_prompt)**:一只坐着的橘黄色的猫,表情愉悦,活泼可爱,逼真准确。 **实际提示词(actual\\_prompt)**:一只坐着的橘黄色猫咪,毛发蓬松柔软,阳光透过窗户洒在它身上,呈现出温暖的光泽。猫咪体型匀称,四肢自然弯曲,稳稳地坐在木质地板上,尾巴轻轻卷曲在身侧,显得格外放松而优雅。它的大眼睛圆润明亮,瞳孔微微收缩,流露出愉悦而灵动的神情,嘴角微扬,仿佛正享受着美好的时光。耳朵微微向前倾斜,透露出活泼与好奇。背景是一间温馨的现代家居客厅,浅色木地板、一扇半开的窗户透进柔和的自然光,窗外可见绿意盎然的庭院,窗台上摆放着几盆绿植。画面采用真实摄影风格,细节逼真,光影层次丰富,突出猫咪的毛发质感、眼神神态与整体姿态的生动自然,整体氛围轻松愉快,充满生活气息。 **watermark** `*bool*` (可选) 是否在图像右下角添加 "Qwen-Image" 水印。默认值为 `false`。水印样式:![1](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/8972029571/p1012089.jpg) **seed** `*integer*` (可选) 随机数种子,取值范围`[0,2147483647]`。 使用相同的`seed`参数值可使生成内容保持相对稳定。若不提供,算法将自动使用随机数种子。 **注意**:模型生成过程具有概率性,即使使用相同的`seed`,也不能保证每次生成结果完全一致。 | + +| ##### **响应参数** | #### 成功响应 请保存 task\\_id,用于查询任务状态与结果。 ``` { "output": { "task_status": "PENDING", "task_id": "0385dc79-5ff8-4d82-bcb6-xxxxxx" }, "request_id": "4909100c-7b5a-9f92-bfe5-xxxxxx" } ``` #### 异常响应 创建任务失败,请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "code": "InvalidApiKey", "message": "No API-key provided.", "request_id": "7438d53d-6eb8-4596-8835-xxxxxx" } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | +| **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | +| **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | + +#### **步骤2:根据任务ID查询结果** + +##### **华北2(北京)** + +`GET https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/tasks/{task_id}` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +##### 新加坡 + +`GET https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/tasks/{task_id}` + +调用时请将`{WorkspaceId}`替换为真实的[Workspace ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 + +**说明** + +- **轮询建议**:图像生成过程耗时较长,建议采用**轮询**机制,并设置合理的查询间隔(如 10 秒)来获取结果。 + +- **任务状态流转**:PENDING(排队中)→ RUNNING(处理中)→ SUCCEEDED(成功)/ FAILED(失败)。 + +- **结果链接**:任务成功后返回图像链接,有效期为 **24 小时**。建议在获取链接后立即下载并转存至永久存储(如[阿里云 OSS](https://help.aliyun.com/zh/oss/user-guide/what-is-oss))。 + +- **RPS 限制**:查询接口默认RPS为20。如需更高频查询或事件通知,建议[配置异步任务回调](https://help.aliyun.com/zh/model-studio/async-task-api)。 + +- **更多操作**:如需批量查询、取消任务等操作,请参见[管理异步任务](https://help.aliyun.com/zh/model-studio/manage-asynchronous-tasks#f26499d72adsl)。 + + +| ##### **请求参数** | ## 查询任务结果 将`{task_id}`完整替换为上一步接口返回的`task_id`的值。`task_id`查询有效期为24小时,并请将`{WorkspaceId}`替换为真实的[业务空间ID](https://help.aliyun.com/zh/model-studio/obtain-the-app-id-and-workspace-id#d3eb3cd37b7fu)。 ``` curl -X GET https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/tasks/{task_id} \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" ``` | +| --- | --- | +| ###### **请求头(Headers)** | +| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API Key进行身份认证。示例值:Bearer sk-xxxx。 | +| ###### **URL路径参数(Path parameters)** | +| **task\\_id** `*string*`**(必选)** 任务ID。 | + +| ##### **响应参数** | ## 任务执行成功 任务数据(如任务状态、图像URL等)仅保留24小时,超时后会被自动清除。请您务必及时保存生成的图像。 ``` { "request_id": "cf4a3304-fa4d-97b6-bc72-xxxxxx", "output": { "task_id": "18e7cde0-8c17-42aa-afc5-xxxxxx", "task_status": "SUCCEEDED", "submit_time": "2025-09-05 11:33:20.542", "scheduled_time": "2025-09-05 11:33:20.581", "end_time": "2025-09-05 11:33:40.807", "results": [ { "orig_prompt": "一副典雅庄重的对联悬挂于厅堂之中,房间是个安静古典的中式布置,桌子上放着一些青花瓷,对联上左书“义本生知人机同道善思新”,右书“通云赋智乾坤启数高志远”, 横批“智启千问”,字体飘逸,在中间挂着一幅中国风的画作,内容是岳阳楼。", "actual_prompt": "一副典雅庄重的对联悬挂于中式厅堂之中,对联左侧书写“义本生知人机同道善思新”,右侧书写“通云赋智乾坤启数高志远”,横批为“智启千问”,字体为飘逸洒脱的书法体,墨色浓淡相宜,展现出浓厚的文化气息与艺术美感。对联中央悬挂一幅中国风画作,描绘的是著名的岳阳楼景观,楼阁飞檐翘角,依水而建,远处山水氤氲,云雾缭绕,展现出古典诗意之美。\\n\\n整个画面背景为一个安静、布置典雅的中式房间,室内木质结构古朴,光线柔和,营造出宁静庄重的氛围。对联悬挂于房间正中墙面,下方为一长案几,案上摆放数件青花瓷器,器型古雅,纹饰精美,蓝白相间,与整体环境和谐统一。整体画面风格为中国水墨风,线条流畅,色彩淡雅,富有传统美学韵味。", "url": "https://dashscope-result-sz.oss-cn-shenzhen.aliyuncs.com/7d/xxx.png?Expires=xxxx" } ] }, "usage": { "image_count": 1 } } ``` ## 任务执行失败 若任务执行失败,task\\_status将置为 FAILED,并提供错误码和信息。请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "request_id": "c61fe158-c0de-40f0-b4d9-964625119ba4", "output": { "task_id": "86ecf553-d340-4e21-xxxxxxxxx", "task_status": "FAILED", "submit_time": "2025-11-11 11:46:28.116", "scheduled_time": "2025-11-11 11:46:28.154", "end_time": "2025-11-11 11:46:28.255", "code": "InvalidParameter", "message": "xxxxxxxx" } } ``` | +| --- | --- | +| **output** `*object*` 任务输出信息。 **属性** **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 **submit\\_time** `*string*` 任务提交时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **scheduled\\_time** `*string*` 任务执行时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **end\\_time** `*string*` 任务完成时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **results** `*array*` 任务结果列表,包括图像URL、prompt、部分任务执行失败报错信息等。 **属性** **orig\\_prompt** `*string*` 原始输入的prompt,对应请求参数`prompt`。 **actual\\_prompt** `*string*` 开启 prompt 智能改写后,返回实际使用的优化后 prompt。若未开启该功能,则不返回此字段。 **url** `*string*` 模型生成图像的URL地址。有效期为24小时,请及时下载并保存图像。 **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)。 | +| **usage** `*object*` 输出信息统计。只对成功的结果计数。 **属性** **image\\_count** `*integer*` 模型生成图像的数量,当前固定为1。 | +| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | + +### **DashScope SDK调用** + +DashScope SDK目前已支持[Python](#a3ad9a3b6d9if)和[Java](#589b80853e6rn)。 + +SDK与HTTP接口的参数名基本一致,参数结构根据不同语言的SDK封装而定。异步调用参数说明可参考[HTTP调用](#42703589880ts)。 + +由于图像模型处理时间较长,底层服务采用异步方式。SDK在此基础上封装了两种调用模式: + +- **同步调用(阻塞模式)**: SDK会自动等待任务完成,然后直接返回最终结果,调用体验与常规同步调用一致。 + +- **异步调用(非阻塞模式)**: 调用后将立即返回任务ID,需要用户根据该ID自行查询任务状态和最终结果。 + + +#### **Python SDK调用** + +**说明** + +请先确认已安装最新版DashScope Python SDK,否则可能运行报错:[安装SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。 + +## 同步调用 + +##### **请求示例** + +``` +from http import HTTPStatus +from urllib.parse import urlparse, unquote +from pathlib import PurePosixPath +import requests +from dashscope import ImageSynthesis +import os +import dashscope + +# 以下为华北2(北京)地域的URL,调用时请将 {WorkspaceId} 替换为真实的业务空间ID,各地域的URL不同。 +dashscope.base_http_api_url = 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1' + +prompt = "一副典雅庄重的对联悬挂于厅堂之中,房间是个安静古典的中式布置,桌子上放着一些青花瓷,对联上左书“义本生知人机同道善思新”,右书“通云赋智乾坤启数高志远”, 横批“智启千问”,字体飘逸,在中间挂着一幅中国风的画作,内容是岳阳楼。" + +# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx" +api_key = os.getenv("DASHSCOPE_API_KEY") + +print('----同步调用,请等待任务执行----') +rsp = ImageSynthesis.call(api_key=api_key, + model="qwen-image-plus", # 当前仅qwen-image-plus、qwen-image模型支持异步接口 + prompt=prompt, + negative_prompt=" ", + n=1, + size='1664*928', + prompt_extend=True, + watermark=False) +print(f'response: {rsp}') +if rsp.status_code == HTTPStatus.OK: + # 在当前目录下保存图像 + for result in rsp.output.results: + file_name = PurePosixPath(unquote(urlparse(result.url).path)).parts[-1] + with open('./%s' % file_name, 'wb+') as f: + f.write(requests.get(result.url).content) +else: + print(f'同步调用失败, status_code: {rsp.status_code}, code: {rsp.code}, message: {rsp.message}') +``` + +##### 响应示例 + +> url 有效期24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "03b1ef03-480d-4ea5-ba52-xxxxxx", + "code": null, + "message": "", + "output": { + "task_id": "3cefd9bc-fcb2-4de9-a8bc-xxxxxx", + "task_status": "SUCCEEDED", + "results": [ + { + "url": "https://dashscope-result-sz.oss-cn-shenzhen.aliyuncs.com/xxx.png?Expires=xxxxxx", + "orig_prompt": "一副典雅庄重的对联悬挂于厅堂之中,房间是个安静古典的中式布置,桌子上放着一些青花瓷,对联上左书“义本生知人机同道善思新”,右书“通云赋智乾坤启数高志远”, 横批“智启千问”,字体飘逸,在中间挂着一幅中国风的画作,内容是岳阳楼。", + "actual_prompt": "一副典雅庄重的对联悬挂于中式厅堂正中,整体空间为安静、古色古香的中国传统布置。厅堂内木质家具沉稳大气,墙面为淡色仿古纸张质感,地面铺设深色木质地板,营造出宁静而庄重的氛围。对联以飘逸的书法字体书写,左侧上联为“义本生知人机同道善思新”,右侧下联为“通云赋智乾坤启数高志远”,横批“智启千问”,文字排列对称,墨色深邃,书法流畅有力,体现出浓厚的文化气息与哲思内涵。\n\n对联中央悬挂一幅中国风画作,内容为岳阳楼,楼阁依水而建,背景为浩渺洞庭湖,远处山峦起伏,云雾缭绕,画面采用传统水墨技法绘制,笔触细腻,意境悠远。画作下方为一张中式红木长桌,桌上错落摆放着几件青花瓷器,包括花瓶与茶具,瓷器釉色清透,纹饰典雅,与整体环境风格和谐统一。整体画面风格为中国古典水墨风,空间布局层次分明,氛围宁静雅致,展现出浓厚的东方文化底蕴。" + } + ], + "submit_time": "2025-09-09 13:41:54.041", + "scheduled_time": "2025-09-09 13:41:54.087", + "end_time": "2025-09-09 13:42:22.596" + }, + "usage": { + "image_count": 1 + } +} +``` + +## 异步调用 + +##### 请求示例 + +``` +from http import HTTPStatus +from urllib.parse import urlparse, unquote +from pathlib import PurePosixPath +import requests +from dashscope import ImageSynthesis +import os +import dashscope +import time + +# 以下为华北2(北京)地域的URL,调用时请将 {WorkspaceId} 替换为真实的业务空间ID,各地域的URL不同。 +dashscope.base_http_api_url = 'https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1' + +prompt = "一副典雅庄重的对联悬挂于厅堂之中,房间是个安静古典的中式布置,桌子上放着一些青花瓷,对联上左书“义本生知人机同道善思新”,右书“通云赋智乾坤启数高志远”, 横批“智启千问”,字体飘逸,在中间挂着一幅中国风的画作,内容是岳阳楼。" + +# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx" +# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key +api_key = os.getenv("DASHSCOPE_API_KEY") + +def async_call(): + print('----创建任务----') + task_info = create_async_task() + print('----轮询任务状态----') + poll_task_status(task_info) + + +# 创建异步任务 +def create_async_task(): + rsp = ImageSynthesis.async_call(api_key=api_key, + model="qwen-image-plus", # 当前仅qwen-image-plus、qwen-image模型支持异步接口 + prompt=prompt, + negative_prompt=" ", + n=1, + size='1664*928', + prompt_extend=True, + watermark=False) + print(rsp) + if rsp.status_code == HTTPStatus.OK: + print(rsp.output) + else: + print(f'创建任务失败, status_code: {rsp.status_code}, code: {rsp.code}, message: {rsp.message}') + return rsp + + +# 轮询异步任务状态,每5秒查询一次,最多轮询1分钟 +def poll_task_status(task): + start_time = time.time() + timeout = 60 # 1分钟超时 + + while True: + # 检查是否超时 + if time.time() - start_time > timeout: + print('轮询超时(1分钟),任务未完成') + return + + # 获取任务状态 + status_rsp = ImageSynthesis.fetch(task) + print(f'任务状态查询结果: {status_rsp}') + + if status_rsp.status_code != HTTPStatus.OK: + print(f'获取任务状态失败, status_code: {status_rsp.status_code}, code: {status_rsp.code}, message: {status_rsp.message}') + return + task_status = status_rsp.output.task_status + print(f'当前任务状态: {task_status}') + + if task_status == 'SUCCEEDED': + print('任务已完成,正在下载图像...') + for result in status_rsp.output.results: + file_name = PurePosixPath(unquote(urlparse(result.url).path)).parts[-1] + with open(f'./{file_name}', 'wb+') as f: + f.write(requests.get(result.url).content) + print(f'图像已保存为: {file_name}') + break + elif task_status == 'FAILED': + print(f'任务执行失败, status: {task_status}, code: {status_rsp.code}, message: {status_rsp.message}') + break + elif task_status == 'PENDING' or task_status == 'RUNNING': + print('任务正在进行中,5秒后继续查询...') + time.sleep(5) + elif task_status == 'CANCELED': + print('任务已被取消。') + break + else: + print(f'未知任务状态: {task_status},5秒后继续查询...') + time.sleep(5) + +# 取消异步任务,只有处于PENDING状态的任务才可以取消 +def cancel_task(task): + rsp = ImageSynthesis.cancel(task) + print(rsp) + if rsp.status_code == HTTPStatus.OK: + print(rsp.output.task_status) + else: + print(f'取消任务失败, status_code: {rsp.status_code}, code: {rsp.code}, message: {rsp.message}') + + +if __name__ == '__main__': + async_call() +``` + +##### **响应示例** + +1、创建任务的响应示例 + +``` +{ + "status_code": 200, + "request_id": "31b04171-011c-96bd-ac00-xxxxxx", + "code": "", + "message": "", + "output": { + "task_id": "4f90cf14-a34e-4eae-xxxxxxxx", + "task_status": "PENDING", + "results": [] + }, + "usage": null +} +``` + +2、查询任务结果的响应示例 + +> url 有效期24小时,请及时下载图像。 + +``` +{ + "status_code": 200, + "request_id": "03b1ef03-480d-4ea5-ba52-xxxxxx", + "code": null, + "message": "", + "output": { + "task_id": "3cefd9bc-fcb2-4de9-a8bc-xxxxxx", + "task_status": "SUCCEEDED", + "results": [ + { + "url": "https://dashscope-result-sz.oss-cn-shenzhen.aliyuncs.com/xxx.png?Expires=xxxxxx", + "orig_prompt": "一副典雅庄重的对联悬挂于厅堂之中,房间是个安静古典的中式布置,桌子上放着一些青花瓷,对联上左书“义本生知人机同道善思新”,右书“通云赋智乾坤启数高志远”, 横批“智启千问”,字体飘逸,在中间挂着一幅中国风的画作,内容是岳阳楼。", + "actual_prompt": "一副典雅庄重的对联悬挂于中式厅堂正中,整体空间为安静、古色古香的中国传统布置。厅堂内木质家具沉稳大气,墙面为淡色仿古纸张质感,地面铺设深色木质地板,营造出宁静而庄重的氛围。对联以飘逸的书法字体书写,左侧上联为“义本生知人机同道善思新”,右侧下联为“通云赋智乾坤启数高志远”,横批“智启千问”,文字排列对称,墨色深邃,书法流畅有力,体现出浓厚的文化气息与哲思内涵。\n\n对联中央悬挂一幅中国风画作,内容为岳阳楼,楼阁依水而建,背景为浩渺洞庭湖,远处山峦起伏,云雾缭绕,画面采用传统水墨技法绘制,笔触细腻,意境悠远。画作下方为一张中式红木长桌,桌上错落摆放着几件青花瓷器,包括花瓶与茶具,瓷器釉色清透,纹饰典雅,与整体环境风格和谐统一。整体画面风格为中国古典水墨风,空间布局层次分明,氛围宁静雅致,展现出浓厚的东方文化底蕴。" + } + ], + "submit_time": "2025-09-09 13:41:54.041", + "scheduled_time": "2025-09-09 13:41:54.087", + "end_time": "2025-09-09 13:42:22.596" + }, + "usage": { + "image_count": 1 + } +} +``` + +#### **Java SDK调用** + +**说明** + +请先确认已安装最新版DashScope Java SDK,否则可能运行报错:[安装SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。 + +## 同步调用 + +##### 请求示例 + +``` +// Copyright (c) Alibaba, Inc. and its affiliates. + +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisListResult; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.task.AsyncTaskListParam; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; +import java.util.HashMap; +import java.util.Map; + +public class Text2Image { + static { + // 以下为华北2(北京)地域的URL,调用时请将 {WorkspaceId} 替换为真实的业务空间ID,各地域的URL不同。 + Constants.baseHttpApiUrl = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"; + } + + // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + // 若没有配置环境变量,请用百炼API Key将下行替换为:static String apiKey = "sk-xxx" + static String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + public static void basicCall() throws ApiException, NoApiKeyException { + String prompt = "一副典雅庄重的对联悬挂于厅堂之中,房间是个安静古典的中式布置,桌子上放着一些青花瓷,对联上左书“义本生知人机同道善思新”,右书“通云赋智乾坤启数高志远”, 横批“智启千问”,字体飘逸,在中间挂着一幅中国风的画作,内容是岳阳楼。"; + Map parameters = new HashMap<>(); + parameters.put("prompt_extend", true); + parameters.put("watermark", false); + parameters.put("negative_prompt", " "); + ImageSynthesisParam param = + ImageSynthesisParam.builder() + .apiKey(apiKey) + // 当前仅qwen-image-plus、qwen-image模型支持异步接口 + .model("qwen-image-plus") + .prompt(prompt) + .n(1) + .size("1664*928") + .parameters(parameters) + .build(); + + ImageSynthesis imageSynthesis = new ImageSynthesis(); + ImageSynthesisResult result = null; + try { + System.out.println("---同步调用,请等待任务执行----"); + result = imageSynthesis.call(param); + } catch (ApiException | NoApiKeyException e){ + throw new RuntimeException(e.getMessage()); + } + System.out.println(JsonUtils.toJson(result)); + } + + public static void main(String[] args){ + try{ + basicCall(); + }catch(ApiException|NoApiKeyException e){ + System.out.println(e.getMessage()); + } + } +} +``` + +##### **响应示例** + +> url 有效期24小时,请及时下载图像。 + +``` +{ + "request_id": "f2153409-3950-9b73-9980-xxxxxx", + "output": { + "task_id": "2fc2e1de-0245-442d-b664-xxxxxx", + "task_status": "SUCCEEDED", + "results": [ + { + "orig_prompt": "一副典雅庄重的对联悬挂于厅堂之中,房间是个安静古典的中式布置,桌子上放着一些青花瓷,对联上左书“义本生知人机同道善思新”,右书“通云赋智乾坤启数高志远”, 横批“智启千问”,字体飘逸,在中间挂着一幅中国风的画作,内容是岳阳楼。", + "actual_prompt": "一副典雅庄重的对联悬挂于中式厅堂中央,对联左侧书写“义本生知人机同道善思新”,右侧书写“通云赋智乾坤启数高志远”,横批为“智启千问”,整体采用飘逸洒脱的书法字体,墨色浓淡相宜,展现出浓厚的传统韵味。对联中间悬挂一幅中国风画作,描绘的是著名的岳阳楼景观:楼阁飞檐翘角,依水而建,远处湖光潋滟,烟波浩渺,天空中有几缕轻云缭绕,营造出诗意盎然的意境。背景房间为安静古典的中式布置,木质家具线条流畅,桌上摆放着数件青花瓷器,纹饰精美,釉色莹润。整体空间光线柔和,营造出庄重、宁静的文化氛围。画面风格为传统中国水墨风,笔触细腻,层次分明,充满古典美感。", + "url": "https://dashscope-result-sz.oss-cn-shenzhen.aliyuncs.com/xxx.png?Expires=xxxx" + } + ] + }, + "usage": { + "image_count": 1 + } +} +``` + +## 异步调用 + +##### 请求示例 + +``` +// Copyright (c) Alibaba, Inc. and its affiliates. + +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; +import java.util.HashMap; +import java.util.Map; + +public class Text2Image { + + static { + // 以下为华北2(北京)地域的URL,调用时请将 {WorkspaceId} 替换为真实的业务空间ID,各地域的URL不同。 + Constants.baseHttpApiUrl = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"; + } + + // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key + // 若没有配置环境变量,请用百炼API Key将下行替换为:static String apiKey = "sk-xxx" + static String apiKey = System.getenv("DASHSCOPE_API_KEY"); + + public void asyncCall() { + System.out.println("---创建任务----"); + String taskId = this.createAsyncTask(); + System.out.println("--等待任务结束返回图像url----"); + this.waitAsyncTask(taskId); + } + + public String createAsyncTask() { + String prompt = "一副典雅庄重的对联悬挂于厅堂之中,房间是个安静古典的中式布置,桌子上放着一些青花瓷,对联上左书“义本生知人机同道善思新”,右书“通云赋智乾坤启数高志远”, 横批“智启千问”,字体飘逸,在中间挂着一幅中国风的画作,内容是岳阳楼。"; + Map parameters = new HashMap<>(); + parameters.put("prompt_extend", true); + parameters.put("watermark", false); + parameters.put("negative_prompt", " "); + ImageSynthesisParam param = + ImageSynthesisParam.builder() + .apiKey(apiKey) + // 当前仅qwen-image-plus、qwen-image模型支持异步接口 + .model("qwen-image-plus") + .prompt(prompt) + .n(1) + .size("1664*928") + .parameters(parameters) + .build(); + + try { + ImageSynthesisResult result = new ImageSynthesis().asyncCall(param); + System.out.println(JsonUtils.toJson(result)); + String taskId = result.getOutput().getTaskId(); + System.out.println("task_id=" + taskId); + return taskId; + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + + public void waitAsyncTask(String taskId) { + ImageSynthesis imageSynthesis = new ImageSynthesis(); + long startTime = System.currentTimeMillis(); + int timeout = 60 * 1000; // 1分钟超时 + int interval = 5 * 1000; // 5秒轮询间隔 + + while (true) { + if (System.currentTimeMillis() - startTime > timeout) { + System.out.println("轮询超时(1分钟),任务未完成"); + return; + } + + try { + ImageSynthesisResult result = imageSynthesis.fetch(taskId, apiKey); + System.out.println("任务状态查询结果: " + JsonUtils.toJson(result)); + if (result.getOutput() == null) { + System.out.println("获取任务状态失败,输出结果为空"); + return; + } + String taskStatus = result.getOutput().getTaskStatus(); + System.out.println("当前任务状态: " + taskStatus); + switch (taskStatus) { + case "SUCCEEDED": + System.out.println("任务已完成"); + System.out.println(JsonUtils.toJson(result)); + return; + case "FAILED": + System.out.println("任务执行失败, status: " + taskStatus); + return; + case "PENDING": + case "RUNNING": + System.out.println("任务正在进行中,5秒后继续查询..."); + Thread.sleep(interval); + break; + default: + System.out.println("未知任务状态: " + taskStatus + ",5秒后继续查询..."); + Thread.sleep(interval); + break; + } + } catch (ApiException | NoApiKeyException e) { + System.err.println("API调用异常: " + e.getMessage()); + return; + } catch (InterruptedException e) { + System.err.println("线程中断异常: " + e.getMessage()); + Thread.currentThread().interrupt(); + return; + } + } + } + + public static void main(String[] args){ + Text2Image text2Image = new Text2Image(); + text2Image.asyncCall(); + } +} +``` + +##### 响应示例 + +1、创建任务的响应示例 + +``` +{ + "request_id": "5dbf9dc5-4f4c-9605-85ea-542f97709ba8", + "output": { + "task_id": "7277e20e-aa01-4709-xxxxxxxx", + "task_status": "PENDING" + } +} +``` + +2、查询任务结果的响应示例 + +> url 有效期24小时,请及时下载图像。 + +``` +{ + "request_id": "f2153409-3950-9b73-9980-xxxxxx", + "output": { + "task_id": "2fc2e1de-0245-442d-b664-xxxxxx", + "task_status": "SUCCEEDED", + "results": [ + { + "orig_prompt": "一副典雅庄重的对联悬挂于厅堂之中,房间是个安静古典的中式布置,桌子上放着一些青花瓷,对联上左书“义本生知人机同道善思新”,右书“通云赋智乾坤启数高志远”, 横批“智启千问”,字体飘逸,在中间挂着一幅中国风的画作,内容是岳阳楼。", + "actual_prompt": "一副典雅庄重的对联悬挂于中式厅堂中央,对联左侧书写“义本生知人机同道善思新”,右侧书写“通云赋智乾坤启数高志远”,横批为“智启千问”,整体采用飘逸洒脱的书法字体,墨色浓淡相宜,展现出浓厚的传统韵味。对联中间悬挂一幅中国风画作,描绘的是著名的岳阳楼景观:楼阁飞檐翘角,依水而建,远处湖光潋滟,烟波浩渺,天空中有几缕轻云缭绕,营造出诗意盎然的意境。背景房间为安静古典的中式布置,木质家具线条流畅,桌上摆放着数件青花瓷器,纹饰精美,釉色莹润。整体空间光线柔和,营造出庄重、宁静的文化氛围。画面风格为传统中国水墨风,笔触细腻,层次分明,充满古典美感。", + "url": "https://dashscope-result-sz.oss-cn-shenzhen.aliyuncs.com/xxx.png?Expires=xxxx" + } + ] + }, + "usage": { + "image_count": 1 + } +} +``` + +## 结合 OpenAI Agents SDK 使用 + +千问-文生图(Qwen-Image)等图像生成模型通过 DashScope 原生接口调用,不支持 OpenAI 兼容(compatible-mode)模式,因此无法直接作为 OpenAI Agents SDK 中 Agent 的推理模型。 + +在 OpenAI Agents SDK 等智能体框架中,可将图像生成能力封装为工具(function tool),由支持 OpenAI 兼容模式的文本对话模型(如 qwen-plus)作为 Agent 的推理核心进行调度。示例如下: + +运行前请先安装依赖:`pip install openai-agents dashscope`。 + +``` +import os +import asyncio +import dashscope +from openai import AsyncOpenAI +from dashscope import MultiModalConversation +from agents import Agent, Runner, function_tool, OpenAIChatCompletionsModel + +# 从环境变量读取百炼 API Key(若未配置,可直接替换为 api_key="sk-xxx") +API_KEY = os.getenv("DASHSCOPE_API_KEY") + +# 以下为华北2(北京)地域的URL,调用时请将 {WorkspaceId} 替换为真实的业务空间ID,各地域的URL不同。 +dashscope.base_http_api_url = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1" +# 文本对话模型(Agent 推理核心)通过 OpenAI 兼容模式接入 +client = AsyncOpenAI(api_key=API_KEY, base_url="https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/compatible-mode/v1") + + +# 将图像生成封装为工具(function tool),供 Agent 调用 +@function_tool +def generate_image(prompt: str) -> str: + """根据文本描述生成一张图片并返回图片 URL。prompt:图片内容的文字描述。""" + rsp = MultiModalConversation.call( + api_key=API_KEY, + model="qwen-image", + messages=[{"role": "user", "content": [{"text": prompt}]}], + result_format="message", + ) + if rsp.status_code != 200: + return f"生成失败:{rsp.code} {rsp.message}" + return rsp.output.choices[0].message.content[0]["image"] + + +agent = Agent( + name="图像生成助手", + instructions="你是图像生成助手。当用户想要图片时,调用 generate_image 工具,并把返回的图片 URL 告诉用户。", + model=OpenAIChatCompletionsModel(model="qwen-plus", openai_client=client), + tools=[generate_image], +) + + +async def main(): + result = await Runner.run(agent, "帮我画一只在草地上奔跑的柯基犬") + print(result.final_output) + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## **计费与限流** + +- 模型免费额度和计费单价请参见[模型价格](https://help.aliyun.com/zh/model-studio/model-pricing#11a4ac6ea62wt)。 + +- 模型限流请参见[千问(Qwen-Image)](https://help.aliyun.com/zh/model-studio/rate-limit#f812e7c63axvx)。 + +- 计费说明:按成功生成的 **图像张数** 计费。模型调用失败或处理错误不产生任何费用,也不消耗[新人免费额度](https://help.aliyun.com/zh/model-studio/new-free-quota)。 + + +## **错误码** + +如果模型调用失败并返回报错信息,请参见[错误码](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 + +## **常见问题** + +#### **Q:prompt\_extend参数应该开启还是关闭?** + +A:如果希望图像内容更多样化,由模型补充细节,建议开启此选项(默认)。如果图像细节更可控,建议关闭此选项,并参考[文生图Prompt指南](https://help.aliyun.com/zh/model-studio/text-to-image-prompt)进行优化, + +#### **Q:qwen-image、qwen-image-plus、qwen-image-max、qwen-image-2.0、qwen-image-edit 等模型的区别是什么?** + +A: + +- **图像生成与编辑融合模型:**同时支持文生图和图像编辑。 + + - `qwen-image-2.0-pro`、`qwen-image-2.0-pro-2026-03-03`:当前两者能力相同,Pro系列具备更专业的文字渲染能力、更细腻的真实质感,细腻刻画写实场景,以及更强的语义遵循能力。仅支持同步接口。 + + - `qwen-image-2.0`、`qwen-image-2.0-2026-03-03`:当前两者能力相同,加速版有效实现了模型效果和性能的最佳平衡。仅支持同步接口。 + +- **文生图模型:**根据文本描述生成图像。 + + - `qwen-image-max`、`qwen-image-max-2025-12-30`:当前两者能力相同,相较于`qwen-image-plus`提升了生成图像的真实感与自然度,在人物质感、纹理细节和文字渲染等方面效果更佳。 + + - `qwen-image`、`qwen-image-plus`:当前两者能力相同,但`qwen-image-plus`的价格更优惠。 + + - `qwen-image-plus-2026-01-09`:千问图像生成的全新快照版模型,为`qwen-image-max`的蒸馏加速版,支持快速生成高质量图像。 + +- **图像编辑模型**: + `qwen-image-edit`:根据输入的图像和文本指令,执行图生图、局部修改等操作,详情请参见[千问-图像编辑](https://help.aliyun.com/zh/model-studio/qwen-image-edit-api)。 + + + +### **Q:如何获取图像存储的访问域名白名单?** + +A: 模型生成的图像存储于阿里云OSS,API将返回一个临时的公网URL。**若需要对该下载地址进行防火墙白名单配置**,请注意:由于底层存储会根据业务情况进行动态变更,为避免过期信息影响访问,文档不提供固定的OSS域名白名单。如有安全管控需求,请联系客户经理获取最新OSS域名列表。 + +/\* 调整 table 宽度 \*/ .aliyun-docs-content table.medium-width { max-width: 1018px; width: 100%; } .aliyun-docs-content table.table-no-border tr td:first-child { padding-left: 0; } .aliyun-docs-content table.table-no-border tr td:last-child { padding-right: 0; } /\* 支持吸顶 \*/ div:has(.aliyun-docs-content), .aliyun-docs-content .markdown-body { overflow: visible; } .stick-top { position: sticky; top: 46px; } /\*\*代码块字体\*\*/ /\* 减少表格中的代码块 margin,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body table .help-code-block { margin: 0 !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre { font-size: 12px !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre code { font-size: 12px !important; } /\*\* API Reference 表格 \*\*/ .aliyun-docs-content table.api-reference tr td:first-child { margin: 0px; border-bottom: 1px solid #d8d8d8; } .aliyun-docs-content table.api-reference tr:last-child td:first-child { border-bottom: none; } .aliyun-docs-content table.api-reference p { color: #6e6e80; } .aliyun-docs-content table.api-reference b, i { color: #181818; } .aliyun-docs-content table.api-reference .collapse { border: none; margin-top: 4px; margin-bottom: 4px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title { padding: 0; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title .title { margin-left: 16px; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse .expandable-title i.icon { position: absolute; color: #777; font-weight: 100; } .aliyun-docs-content table.api-reference .collapse.expanded .expandable-content { padding: 10px 14px 10px 14px !important; margin: 0; border: 1px solid #e9e9e9; } .aliyun-docs-content table.api-reference .collapse .expandable-title-bold b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .collapse .expandable-title b { font-size: 13px; font-weight: normal; color: #6e6e80; } .aliyun-docs-content table.api-reference .tabbed-content-box { border: none; } .aliyun-docs-content table.api-reference .tabbed-content-box section { padding: 8px 0 !important; } .aliyun-docs-content table.api-reference .tabbed-content-box.mini .tab-box { /\* position: absolute; left: 40px; right: 0; \*/ } .aliyun-docs-content .margin-top-33 { margin-top: 33px !important; } .aliyun-docs-content .two-codeblocks pre { max-height: calc(50vh - 136px) !important; height: auto; } .expandable-content section { border-bottom: 1px solid #e9e9e9; padding-top: 6px; padding-bottom: 4px; } .expandable-content section:last-child { border-bottom: none; } .expandable-content section:first-child { padding-top: 0; } + +/\* 让引用上下间距调小,避免内容显示过于稀疏 \*/ .unionContainer .markdown-body blockquote { margin: 4px 0; } .aliyun-docs-content table.qwen blockquote { border-left: none; /\* 添加这一行来移除表格里的引用文字的左侧边框 \*/ padding-left: 5px; /\* 左侧内边距 \*/ margin: 4px 0; } /\* 让表格显示成类似钉钉文档的分栏卡片 \*/ table.help-table-card td { border: 10px solid #FFF !important; background: #F4F6F9; padding: 16px !important; vertical-align: top; } /\* 减少表格中的代码块 margin,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body table .help-code-block { margin: 0 !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre { font-size: 12px !important; } /\* 减少表格中的代码块字号,让表格信息显示更紧凑 \*/ .unionContainer .markdown-body .help-code-block pre code { font-size: 12px !important; } /\* 表格中的引用上下间距调小,避免内容显示过于稀疏 \*/ .unionContainer .markdown-body table blockquote { margin: 4px 0 0 0; } /\*表格图片设置为块元素(独占一行),居中展示,鼠标放在图片上可以点击查看原图\*/ .unionContainer .markdown-body .image.break { margin: 0px; display: inline-block; vertical-align: middle } \ No newline at end of file diff --git a/go.mod b/go.mod index 19ac5b5..56f6f4f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/qianfree/team-api go 1.25.7 require ( + github.com/Masterminds/semver/v3 v3.5.0 github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible github.com/aws/aws-sdk-go-v2 v1.41.6 github.com/aws/aws-sdk-go-v2/config v1.32.16 @@ -33,7 +34,6 @@ require ( require ( github.com/BurntSushi/toml v1.5.0 // indirect - github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 // indirect diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index d6c0059..e081600 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -310,19 +310,28 @@ var ( task.InitActiveCount(ctx) task.StartAsyncPolling(ctx) + // Start sync-image async worker pool (wraps synchronous image providers as async tasks) + task.StartSyncImageWorkers(ctx) + // Start webhook dispatcher (event-driven delivery) tenant.InitWebhookDispatcher(ctx) // 注入 webhook 发布函数到 billing 包(解耦循环依赖) billing.SetPublishWebhookEventFn(tenant.PublishWebhookEvent) - // Flush usage log writer on shutdown (s.Run blocks until server stops) + // Graceful shutdown order (defers run LIFO after s.Run returns): + // 1) 先排空任务池(StopSyncImageWorkers / StopAsyncPolling)——它们收尾时会写 + // 用量日志 / 审计 / 结算;此时异步 Writer 必须仍存活。 + // 2) 再 flush 关闭异步 Writer。 + // 3) 最后关 webhook / plugin。 + // 因此把两个 task.Stop* 注册在最后(最先执行),Writer 关闭注册在前(后执行)。 defer plugin.Shutdown(ctx) defer tenant.ShutdownWebhookDispatcher() - defer task.StopAsyncPolling() defer common.CloseChannelErrorWriter() defer common.CloseUsageLogWriter() defer response.CloseErrorLogWriter() + defer task.StopAsyncPolling() + defer task.StopSyncImageWorkers() s.Run() return nil diff --git a/internal/controller/admin/admin_v1_file.go b/internal/controller/admin/admin_v1_file.go new file mode 100644 index 0000000..21bcfc9 --- /dev/null +++ b/internal/controller/admin/admin_v1_file.go @@ -0,0 +1,28 @@ +package admin + +import ( + "context" + + "github.com/qianfree/team-api/api/admin/v1" + "github.com/qianfree/team-api/internal/service" +) + +func (c *ControllerV1) FileList(ctx context.Context, req *v1.FileListReq) (res *v1.FileListRes, err error) { + return service.Admin().FileList(ctx, req) +} + +func (c *ControllerV1) FileStats(ctx context.Context, req *v1.FileStatsReq) (res *v1.FileStatsRes, err error) { + return service.Admin().FileStats(ctx, req) +} + +func (c *ControllerV1) FileDownload(ctx context.Context, req *v1.FileDownloadReq) (res *v1.FileDownloadRes, err error) { + return service.Admin().FileDownload(ctx, req) +} + +func (c *ControllerV1) FileDelete(ctx context.Context, req *v1.FileDeleteReq) (res *v1.FileDeleteRes, err error) { + return service.Admin().FileDelete(ctx, req) +} + +func (c *ControllerV1) FileCleanup(ctx context.Context, req *v1.FileCleanupReq) (res *v1.FileCleanupRes, err error) { + return service.Admin().FileCleanup(ctx, req) +} diff --git a/internal/handler/relay/sync_image.go b/internal/handler/relay/sync_image.go new file mode 100644 index 0000000..19d6bf8 --- /dev/null +++ b/internal/handler/relay/sync_image.go @@ -0,0 +1,232 @@ +package relay + +import ( + crand "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "net" + "net/http" + "strings" + "time" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + + "github.com/qianfree/team-api/internal/logic/task" + relay_common "github.com/qianfree/team-api/relay/common" + relay_constant "github.com/qianfree/team-api/relay/constant" + relay_handler "github.com/qianfree/team-api/relay/handler" +) + +// HandleSyncImageSubmit 同步图片厂商的「异步化」提交管线。 +// +// 由 HandleAliImageSubmit 在识别到同步厂商(ProviderTypeToTaskPlatform 未命中)时调用: +// 执行 gate → 强制非流式 → 估价 → 预扣 → 建 QUEUED 任务行 → 入 worker 队列, +// 客户端毫秒级拿到 task_id 后即释放连接,真正的上游长连接由 task 包的 worker 池后台持有。 +// +// 与 relay_handler.HandleTaskSubmit 的区别:不在提交阶段调用上游(同步厂商无上游任务 ID), +// 渠道也不在提交时绑定(channel_id=0,worker dispatch 时按最新健康度/容量重选)。 +func HandleSyncImageSubmit(r *ghttp.Request, body []byte, rc *relay_handler.TaskRelayContext, channelMeta *relay_common.ChannelMeta) { + ctx := r.Context() + w := rc.Writer + + modelName := extractModelName(body) + if modelName == "" { + writeSyncImageError(w, http.StatusBadRequest, "model is required") + return + } + + // 1. gate(复刻 HandleTaskSubmit 的 scope / IP 白名单 / 限流 / key 并发) + if rc.Scope == "read_only" { + writeSyncImageError(w, http.StatusForbidden, "API key scope denied") + return + } + if !checkSyncImageIPWhitelist(rc.KeyIpWhitelist, rc.ClientIP) { + writeSyncImageError(w, http.StatusForbidden, "IP address is not allowed") + return + } + if allowed, level, _, _, _ := taskBillingProvider.CheckRateLimit(ctx, rc.TenantID, rc.UserID, rc.ApiKeyID, rc.KeyRateLimitQps); !allowed { + writeSyncImageError(w, http.StatusTooManyRequests, fmt.Sprintf("rate limit exceeded at %s level", level)) + return + } + if !taskBillingProvider.AcquireApiKeyConcurrent(ctx, rc.ApiKeyID, rc.KeyConcurrency) { + writeSyncImageError(w, http.StatusTooManyRequests, "API key concurrent request limit exceeded") + return + } + // 提交很快即返回,并发槽在提交结束即释放(上游长连接由 worker 持有,不占此槽)。 + defer taskBillingProvider.ReleaseApiKeyConcurrent(ctx, rc.ApiKeyID) + + // 2. 强制非流式:剥离 stream 字段,防 DoResponse 走 SSE 分支 + cleanBody := stripStreamField(body) + + // 3. 估价(图片走 per_request 价) + estimatedCost, err := taskBillingProvider.EstimateTaskCost(ctx, rc.TenantID, modelName, nil) + if err != nil { + writeSyncImageError(w, http.StatusInternalServerError, "estimate cost failed: "+err.Error()) + return + } + + // 4. 配额 + 预扣 + if err := taskBillingProvider.CheckApiKeyQuota(ctx, rc.ApiKeyID, estimatedCost); err != nil { + writeSyncImageError(w, http.StatusPaymentRequired, "API key quota exceeded") + return + } + preDeduct, err := taskBillingProvider.PreDeductTask(ctx, rc.TenantID, rc.RequestID, estimatedCost, modelName) + if err != nil { + writeSyncImageError(w, http.StatusPaymentRequired, "insufficient balance") + return + } + + // 5. 建 QUEUED 任务行(channel_id=0,dispatch 时绑定) + publicTaskID := generateSyncImagePublicID() + now := time.Now() + privateData, _ := json.Marshal(map[string]any{ + "task_type": string(relay_constant.TaskPlatformSyncImage), + "billing_context": map[string]any{ + "ratios": nil, + "model_name": modelName, + "pre_deduct": preDeduct, + }, + }) + asyncTask := &relay_common.AsyncTask{ + PublicTaskID: publicTaskID, + RequestID: rc.RequestID, + Platform: string(relay_constant.TaskPlatformSyncImage), + Action: "generate", + Status: "QUEUED", + Progress: "0%", + TenantID: rc.TenantID, + UserID: rc.UserID, + ApiKeyID: rc.ApiKeyID, + ChannelID: 0, + ModelName: modelName, + PreDeductAmount: preDeduct, + PrivateData: privateData, + SubmitTime: &now, + } + if err := taskDataProvider.CreateTask(ctx, asyncTask); err != nil { + _ = taskBillingProvider.SettleTaskFailed(ctx, rc.TenantID, rc.RequestID, preDeduct) + writeSyncImageError(w, http.StatusInternalServerError, "create task record failed: "+err.Error()) + return + } + + // CreateTask 不回填自增 ID,回查取任务行主键供 worker CAS 使用。 + created, err := taskDataProvider.GetTaskByPublicID(ctx, publicTaskID) + if err != nil || created == nil { + _ = taskBillingProvider.SettleTaskFailed(ctx, rc.TenantID, rc.RequestID, preDeduct) + g.Log().Errorf(ctx, "sync_image: load created task %s failed: %v", publicTaskID, err) + writeSyncImageError(w, http.StatusInternalServerError, "create task record failed") + return + } + rc.TaskID = publicTaskID + + // 6. 入队(非阻塞);队列满 → 退款 + CAS→FAILURE + DecrActive + 429 + job := &task.SyncImageJob{ + TaskID: created.ID, + PublicTaskID: publicTaskID, + RequestID: rc.RequestID, + TenantID: rc.TenantID, + UserID: rc.UserID, + ApiKeyID: rc.ApiKeyID, + ProjectID: rc.ProjectID, + Model: modelName, + RequestBody: cleanBody, + PreDeductAmount: preDeduct, + Ratios: nil, + SubmitTime: now, + } + if !task.EnqueueSyncImageJob(job) { + _ = taskBillingProvider.SettleTaskFailed(ctx, rc.TenantID, rc.RequestID, preDeduct) + _ = taskDataProvider.UpdateTaskCAS(ctx, &relay_common.AsyncTask{ + ID: created.ID, + Status: "FAILURE", + FailReason: "sync image worker queue full", + BillingSettled: true, + FinishTime: &now, + }, "QUEUED") + task.DecrActiveTask() + rc.TaskID = "" + writeSyncImageError(w, http.StatusTooManyRequests, "server busy, please retry later") + return + } + + // 7. 返回 QUEUED(客户端连接在此释放) + writeSyncImageJSON(w, http.StatusOK, map[string]any{ + "id": publicTaskID, + "status": "QUEUED", + "model": modelName, + "created_at": now.Unix(), + }) +} + +// stripStreamField 移除请求体中的 stream 字段(强制非流式)。 +func stripStreamField(body []byte) []byte { + var m map[string]json.RawMessage + if err := json.Unmarshal(body, &m); err != nil { + return body + } + if _, ok := m["stream"]; !ok { + return body + } + delete(m, "stream") + cleaned, err := json.Marshal(m) + if err != nil { + return body + } + return cleaned +} + +// checkSyncImageIPWhitelist 复刻 HandleTaskSubmit 的 IP 白名单校验(支持精确 IP 与 CIDR)。 +func checkSyncImageIPWhitelist(whitelist, clientIP string) bool { + if whitelist == "" { + return true + } + host, _, err := net.SplitHostPort(clientIP) + if err != nil { + host = clientIP + } + host = strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[") + parsedIP := net.ParseIP(host) + if parsedIP == nil { + return false + } + for _, item := range strings.Split(whitelist, ",") { + item = strings.TrimSpace(item) + if item == "" { + continue + } + if item == host { + return true + } + if strings.Contains(item, "/") { + if _, cidr, err := net.ParseCIDR(item); err == nil && cidr.Contains(parsedIP) { + return true + } + } + } + return false +} + +func generateSyncImagePublicID() string { + b := make([]byte, 16) + if _, err := crand.Read(b); err != nil { + panic("crypto/rand failed: " + err.Error()) + } + return "task_" + hex.EncodeToString(b) +} + +func writeSyncImageError(w http.ResponseWriter, statusCode int, message string) { + writeSyncImageJSON(w, statusCode, map[string]any{ + "error": map[string]any{ + "type": "invalid_request_error", + "message": message, + }, + }) +} + +func writeSyncImageJSON(w http.ResponseWriter, statusCode int, data any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + _ = json.NewEncoder(w).Encode(data) +} diff --git a/internal/handler/relay/sync_image_test.go b/internal/handler/relay/sync_image_test.go new file mode 100644 index 0000000..3714880 --- /dev/null +++ b/internal/handler/relay/sync_image_test.go @@ -0,0 +1,84 @@ +package relay + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestStripStreamField(t *testing.T) { + // stream=true 应被移除,其余字段保留 + in := []byte(`{"model":"dall-e-3","prompt":"a cat","stream":true}`) + out := stripStreamField(in) + var m map[string]json.RawMessage + if err := json.Unmarshal(out, &m); err != nil { + t.Fatalf("output not valid json: %v", err) + } + if _, ok := m["stream"]; ok { + t.Fatal("stream field should be removed") + } + if _, ok := m["model"]; !ok { + t.Fatal("model field should be preserved") + } + if _, ok := m["prompt"]; !ok { + t.Fatal("prompt field should be preserved") + } +} + +func TestStripStreamField_NoStream(t *testing.T) { + in := []byte(`{"model":"dall-e-3","prompt":"a cat"}`) + out := stripStreamField(in) + var m map[string]json.RawMessage + if err := json.Unmarshal(out, &m); err != nil { + t.Fatalf("output not valid json: %v", err) + } + if len(m) != 2 { + t.Fatalf("field count = %d, want 2", len(m)) + } +} + +func TestStripStreamField_InvalidJSON(t *testing.T) { + in := []byte(`not json`) + if got := stripStreamField(in); string(got) != string(in) { + t.Fatal("invalid json should be returned unchanged") + } +} + +func TestCheckSyncImageIPWhitelist(t *testing.T) { + cases := []struct { + name string + whitelist string + clientIP string + want bool + }{ + {"empty allows all", "", "1.2.3.4:5678", true}, + {"exact match", "1.2.3.4", "1.2.3.4:5678", true}, + {"exact no host:port", "1.2.3.4", "1.2.3.4", true}, + {"non-match", "1.2.3.4", "5.6.7.8:1000", false}, + {"cidr match", "10.0.0.0/8", "10.1.2.3:9", true}, + {"cidr non-match", "10.0.0.0/8", "11.1.2.3:9", false}, + {"multi list match", "9.9.9.9, 10.0.0.0/8", "10.5.5.5", true}, + {"invalid ip", "1.2.3.4", "not-an-ip", false}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if got := checkSyncImageIPWhitelist(c.whitelist, c.clientIP); got != c.want { + t.Errorf("checkSyncImageIPWhitelist(%q, %q) = %v, want %v", c.whitelist, c.clientIP, got, c.want) + } + }) + } +} + +func TestGenerateSyncImagePublicID(t *testing.T) { + id := generateSyncImagePublicID() + if !strings.HasPrefix(id, "task_") { + t.Fatalf("id %q should have task_ prefix", id) + } + if len(id) != len("task_")+32 { + t.Fatalf("id %q length = %d, want %d", id, len(id), len("task_")+32) + } + // 唯一性(极小概率碰撞) + if generateSyncImagePublicID() == id { + t.Fatal("two ids should differ") + } +} diff --git a/internal/handler/relay/task.go b/internal/handler/relay/task.go index 39f53bd..5e43fb0 100644 --- a/internal/handler/relay/task.go +++ b/internal/handler/relay/task.go @@ -10,6 +10,7 @@ import ( "github.com/gogf/gf/v2/net/ghttp" "github.com/qianfree/team-api/internal/logic/billing" + lcommon "github.com/qianfree/team-api/internal/logic/common" "github.com/qianfree/team-api/internal/logic/monitor" "github.com/qianfree/team-api/internal/logic/relay" "github.com/qianfree/team-api/internal/logic/task" @@ -295,10 +296,35 @@ func HandleAliImageSubmit(r *ghttp.Request) { modelName := extractModelName(body) registerAsyncTask(rc.RequestID, rc.TenantID, rc.UserID, rc.ProjectID, modelName, channelMeta, r.URL.Path) - relay_handler.HandleTaskSubmit( - r.Context(), body, r.URL.Path, r.Header, - rc, taskDataProvider, taskBillingProvider, channelMeta, - ) + // 异步图片端点分流。判「是否真有异步图片上游」用 constant.IsAsyncImageModel(唯一真相源, + // 与 sync gate / async_image 标记同源)——目前仅阿里 image-synthesis(wanx / qwen-image / + // wan2.2-t2i 等)为 true,走 taskchannel 提交拿 task_id。 + // + // 其余图片模型上游都是同步阻塞返回,交 sync_image worker 池包装成可轮询任务(worker 直连各自 + // 同步 channel.Adaptor):阿里 multimodal(qwen-image-2.x/z-image/wan2.6-t2i/wan2.7-image)恒走; + // OpenAI / Gemini(imagen) / 火山(seedream) 等同步厂商受「同步图片异步化」总开关约束。 + // + // 不能用 ProviderTypeToTaskPlatform 笼统判「异步厂商」——Gemini/火山的 taskchannel 是视频 + //(veo/seedance)专用,把它们的图片模型送进去会被当视频构造请求而失败。 + providerType := relay_constant.ProviderType(channelMeta.ChannelType) + isAliSyncMultimodal := providerType == relay_constant.ProviderAli && + relay_constant.IsAliSyncMultimodalImageModel(channelMeta.UpstreamModelName) + syncImageEnabled := lcommon.Config().GetBool(r.Context(), "sync_image_async_enabled") + + switch { + case relay_constant.IsAsyncImageModel(providerType, channelMeta.UpstreamModelName): + // 真·异步图片上游(阿里 image-synthesis):taskchannel 提交 + 轮询 + relay_handler.HandleTaskSubmit( + r.Context(), body, r.URL.Path, r.Header, + rc, taskDataProvider, taskBillingProvider, channelMeta, + ) + case isAliSyncMultimodal || syncImageEnabled: + // 同步阻塞图片:worker 池异步化(阿里 multimodal 恒走;其余同步厂商受开关约束) + HandleSyncImageSubmit(r, body, rc, channelMeta) + default: + // 同步厂商 + 「同步图片异步化」关闭:异步端点不支持,提示改用同步端点 + writeSyncImageError(rc.Writer, 400, "async image generation is disabled for this model; call POST /v1/images/generations instead") + } if rc.TaskID != "" { monitor.SwitchToTaskID(rc.RequestID, rc.TaskID) diff --git a/internal/logic/admin/audit.go b/internal/logic/admin/audit.go index eb8fd21..342f45c 100644 --- a/internal/logic/admin/audit.go +++ b/internal/logic/admin/audit.go @@ -88,11 +88,11 @@ func (s *sAdmin) UpdateAuditConfig(ctx context.Context, req *v1.AuditConfigUpdat } // queryPage 使用 GoFrame ORM 分页查询,返回 []map[string]any。 -// 注意:该函数使用审计数据库连接(common.GetAuditDB())查询审计表。 +// 使用主库查询审计表(aud_request_logs 之外的审计表数据量小,无需独立库)。 // 分开调用 Count 和 All 避免 ScanAndCount 对 map[string]any 的 bug。 func queryPage(ctx context.Context, table, fields, where, orderBy string, page, pageSize int, args ...any) ([]map[string]any, int, error) { - auditDB := common.GetAuditDB() - m := auditDB.Ctx(ctx).Model(table).Safe() + mainDB := g.DB() + m := mainDB.Ctx(ctx).Model(table).Safe() if where != "" { m = m.Where(where, args...) } @@ -106,7 +106,7 @@ func queryPage(ctx context.Context, table, fields, where, orderBy string, page, } // 重新构建查询(Count 会消耗 Model 状态) - q := auditDB.Ctx(ctx).Model(table).Safe() + q := mainDB.Ctx(ctx).Model(table).Safe() if fields != "" { q = q.Fields(fields) } @@ -456,7 +456,7 @@ func (s *sAdmin) ExportOperationLogs(ctx context.Context, req *v1.OperationLogEx offset := 0 for { where, args := buildOpLogWhere() - q := common.GetAuditDB().Ctx(ctx).Model("aud_operation_logs").Fields(selectFields).OrderDesc("created_at").Offset(offset).Limit(1000) + q := g.DB().Ctx(ctx).Model("aud_operation_logs").Fields(selectFields).OrderDesc("created_at").Offset(offset).Limit(1000) if where != "" { q = q.Where(where, args...) } @@ -546,11 +546,11 @@ func (s *sAdmin) ContentFilterLogList(ctx context.Context, req *v1.ContentFilter where := strings.Join(conditions, " AND ") - // Step 1: 从审计库查询审计记录(不带 JOIN) - auditDB := common.GetAuditDB() + // Step 1: 从主库查询内容过滤日志(数据量小,无需独立库) + mainDB := g.DB() fields := "id, tenant_id, user_id, api_key_id, project_id, request_id, method, path, client_ip, filter_mode, matched_words, original_snippet, blocked, created_at" - countM := auditDB.Ctx(ctx).Model("aud_content_filter_logs").Safe() + countM := mainDB.Ctx(ctx).Model("aud_content_filter_logs").Safe() if where != "" { countM = countM.Where(where, args...) } @@ -564,7 +564,7 @@ func (s *sAdmin) ContentFilterLogList(ctx context.Context, req *v1.ContentFilter }, nil } - dataM := auditDB.Ctx(ctx).Model("aud_content_filter_logs").Safe(). + dataM := mainDB.Ctx(ctx).Model("aud_content_filter_logs").Safe(). Fields(fields). OrderDesc("created_at"). Page(page, pageSize) diff --git a/internal/logic/admin/data_governance.go b/internal/logic/admin/data_governance.go index af2b30a..b3b5de0 100644 --- a/internal/logic/admin/data_governance.go +++ b/internal/logic/admin/data_governance.go @@ -145,40 +145,132 @@ func CleanupExpiredData(ctx context.Context) error { return nil } -// CleanupExpiredExportFiles 清理过期导出文件 +// FileCleanupResult 报告一次文件保留期清理删除的文件数量。 +type FileCleanupResult struct { + ExportsDeleted int `json:"exports_deleted"` + ImagesDeleted int `json:"images_deleted"` +} + +// deleteExpiredFiles 删除 created_at 早于 cutoff 的 fil_files 行及其存储对象。 +// exports=true 匹配 storage_path LIKE 'exports/%'(导出文件);exports=false 匹配 +// 其余(AI re-host 图片等 provider 上传的文件)。删除经 FileService.Delete 完成, +// 会一并删除桶中对象(而非仅删库行,避免存储泄漏)。fileSvc 为 nil 时(对象存储未 +// 配置)退化为仅删库行。分批处理以限制内存与单次事务规模。 +func deleteExpiredFiles(ctx context.Context, fileSvc *common.FileService, cutoff time.Time, exports bool) (int, error) { + const batchSize = 500 + total := 0 + for { + m := dao.FilFiles.Ctx(ctx).Where("created_at < ?", cutoff) + if exports { + m = m.Where("storage_path LIKE ?", "exports/%") + } else { + m = m.Where("storage_path NOT LIKE ?", "exports/%") + } + + var batch []struct { + ID int64 `json:"id"` + } + if err := m.Fields("id").OrderAsc("id").Limit(batchSize).Scan(&batch); err != nil { + return total, err + } + if len(batch) == 0 { + break + } + + deletedInBatch := 0 + for _, f := range batch { + if fileSvc != nil { + // FileService.Delete 删对象(失败仅告警)后删行;返回硬错误表示行未删。 + if err := fileSvc.Delete(ctx, f.ID); err != nil { + g.Log().Warningf(ctx, "file retention: delete file %d failed: %v", f.ID, err) + continue + } + } else if _, err := dao.FilFiles.Ctx(ctx).Where("id", f.ID).Delete(); err != nil { + g.Log().Warningf(ctx, "file retention: delete row %d failed: %v", f.ID, err) + continue + } + total++ + deletedInBatch++ + } + + // 整批未能删除任何一行(删除持续失败)——停止,避免同一批被反复重查形成热循环。 + if deletedInBatch == 0 { + g.Log().Warningf(ctx, "file retention: no progress in batch, stopping (check storage/db)") + break + } + if len(batch) < batchSize { + break + } + } + if total > 0 { + g.Log().Infof(ctx, "file retention: removed %d files (exports=%v, before %s)", total, exports, cutoff.Format("2006-01-02")) + } + return total, nil +} + +// buildFileService 从数据库配置构造 FileService;对象存储未配置时返回 nil(清理退化为仅删库行)。 +func buildFileService(ctx context.Context) *common.FileService { + fileSvc, err := common.NewFileServiceFromConfig(ctx) + if err != nil { + g.Log().Warningf(ctx, "file retention: object storage unavailable, pruning db rows only: %v", err) + return nil + } + return fileSvc +} + +// CleanupExpiredExportFiles 清理过期导出文件(含存储对象)。由 export_file_cleanup cron 调用。 func CleanupExpiredExportFiles(ctx context.Context) error { expiryDays := common.Config().GetInt(ctx, "data_export_expiry_days") if expiryDays <= 0 { return nil } cutoff := time.Now().AddDate(0, 0, -expiryDays) - - var expiredFiles []struct { - ID int64 `json:"id"` - StoragePath string `json:"storage_path"` - } - err := dao.FilFiles.Ctx(ctx). - Where("created_at < ?", cutoff). - Where("storage_path LIKE ?", "exports/%"). - Fields("id, storage_path"). - Scan(&expiredFiles) - if err != nil { - return err - } - for _, f := range expiredFiles { - if _, err := dao.FilFiles.Ctx(ctx).Where("id", f.ID).Delete(); err != nil { - g.Log().Warningf(ctx, "cleanup file %d failed: %v", f.ID, err) - } - } - return nil + _, err := deleteExpiredFiles(ctx, buildFileService(ctx), cutoff, true) + return err } -// CheckFileRetention 检查文件保留期 +// CleanupExpiredImages 清理过期的 AI re-host 图片及其它 provider 上传文件(含存储对象)。 +// file_image_retention_days<=0 表示关闭(默认),直接跳过。 +func CleanupExpiredImages(ctx context.Context) (int, error) { + retentionDays := common.Config().GetInt(ctx, "file_image_retention_days") + if retentionDays <= 0 { + return 0, nil + } + cutoff := time.Now().AddDate(0, 0, -retentionDays) + return deleteExpiredFiles(ctx, buildFileService(ctx), cutoff, false) +} + +// CheckFileRetention 由 file_retention_check cron 调用:受 file_retention_enabled 开关控制, +// 清理过期图片文件(导出文件由独立的 export_file_cleanup cron 负责)。 func CheckFileRetention(ctx context.Context) error { if !common.Config().GetBool(ctx, "file_retention_enabled") { return nil } - return CleanupExpiredExportFiles(ctx) + _, err := CleanupExpiredImages(ctx) + return err +} + +// RunFileRetentionNow 立即执行一次完整保留期清理(导出 + 图片),返回各自删除数量。 +// 供管理后台「手动触发清理」端点调用;不受 file_retention_enabled 开关限制。 +func RunFileRetentionNow(ctx context.Context) (*FileCleanupResult, error) { + res := &FileCleanupResult{} + fileSvc := buildFileService(ctx) + + if days := common.Config().GetInt(ctx, "data_export_expiry_days"); days > 0 { + n, err := deleteExpiredFiles(ctx, fileSvc, time.Now().AddDate(0, 0, -days), true) + if err != nil { + return res, err + } + res.ExportsDeleted = n + } + if days := common.Config().GetInt(ctx, "file_image_retention_days"); days > 0 { + n, err := deleteExpiredFiles(ctx, fileSvc, time.Now().AddDate(0, 0, -days), false) + if err != nil { + return res, err + } + res.ImagesDeleted = n + } + return res, nil } func cleanupTableByDate(ctx context.Context, table, dateColumn string, days int) error { @@ -209,8 +301,14 @@ func cleanupTableByDate(ctx context.Context, table, dateColumn string, days int) return fmt.Errorf("invalid table for cleanup: %s", table) } + // aud_request_logs 走独立库,其余审计表走主库 + db := g.DB() + if table == "aud_request_logs" { + db = common.GetAuditDB() + } + for { - result, err := common.GetAuditDB().Ctx(ctx).Exec(ctx, deleteSQL, cutoff, batchSize) + result, err := db.Ctx(ctx).Exec(ctx, deleteSQL, cutoff, batchSize) if err != nil { return err } diff --git a/internal/logic/admin/file.go b/internal/logic/admin/file.go new file mode 100644 index 0000000..57a2426 --- /dev/null +++ b/internal/logic/admin/file.go @@ -0,0 +1,212 @@ +package admin + +import ( + "context" + "strings" + + v1 "github.com/qianfree/team-api/api/admin/v1" + "github.com/qianfree/team-api/internal/dao" + "github.com/qianfree/team-api/internal/logic/common" +) + +// deriveFileCategory 由存储路径与 MIME 类型推导文件分类:export / image / other。 +func deriveFileCategory(storagePath, mimeType string) string { + if strings.HasPrefix(storagePath, "exports/") { + return "export" + } + if strings.HasPrefix(mimeType, "image/") { + return "image" + } + return "other" +} + +// adminFileService 从数据库配置构造 FileService;对象存储未配置时返回业务错误。 +func adminFileService(ctx context.Context) (*common.FileService, error) { + svc, err := common.NewFileServiceFromConfig(ctx) + if err != nil { + return nil, common.NewBadRequestError("对象存储未配置,请先在系统设置中配置存储") + } + return svc, nil +} + +// FileList 文件列表(分页 + 筛选)。 +func (s *sAdmin) FileList(ctx context.Context, req *v1.FileListReq) (*v1.FileListRes, error) { + page, pageSize := common.NormalizePagination(req.Page, req.PageSize) + + query := dao.FilFiles.Ctx(ctx) + if req.TenantId > 0 { + query = query.Where("tenant_id", req.TenantId) + } + if req.UserId > 0 { + query = query.Where("user_id", req.UserId) + } + if req.Provider != "" { + query = query.Where("storage_provider", req.Provider) + } + switch req.Category { + case "export": + query = query.Where("storage_path LIKE ?", "exports/%") + case "image": + query = query.Where("mime_type LIKE ?", "image/%").Where("storage_path NOT LIKE ?", "exports/%") + case "other": + query = query.Where("COALESCE(mime_type,'') NOT LIKE ?", "image/%").Where("storage_path NOT LIKE ?", "exports/%") + } + if req.Keyword != "" { + kw := "%" + strings.TrimSpace(req.Keyword) + "%" + query = query.Where("(original_name LIKE ? OR storage_path LIKE ?)", kw, kw) + } + if req.StartDate != "" { + query = query.Where("created_at >= ?", req.StartDate+" 00:00:00") + } + if req.EndDate != "" { + query = query.Where("created_at <= ?", req.EndDate+" 23:59:59") + } + + var total int + rows := make([]*v1.FileItem, 0) + err := query.OrderDesc("created_at"). + Page(page, pageSize). + ScanAndCount(&rows, &total, false) + if err != nil { + return nil, err + } + for _, r := range rows { + r.Category = deriveFileCategory(r.StoragePath, r.MimeType) + } + + return &v1.FileListRes{List: rows, Total: total, Page: page, PageSize: pageSize}, nil +} + +// FileStats 存储占用统计(KPI)。 +func (s *sAdmin) FileStats(ctx context.Context, req *v1.FileStatsReq) (*v1.FileStatsRes, error) { + res := &v1.FileStatsRes{ + ByProvider: make([]v1.FileProviderStat, 0), + ByCategory: make([]v1.FileCategoryStat, 0), + TopTenants: make([]v1.FileTenantStat, 0), + } + + // 总量 + var totals struct { + Count int64 `json:"count"` + Bytes int64 `json:"bytes"` + } + if err := dao.FilFiles.Ctx(ctx). + Fields("COUNT(*) AS count, COALESCE(SUM(size),0) AS bytes"). + Scan(&totals); err != nil { + return nil, err + } + res.TotalCount = totals.Count + res.TotalBytes = totals.Bytes + + // 按供应商 + if err := dao.FilFiles.Ctx(ctx). + Fields("storage_provider AS provider, COUNT(*) AS count, COALESCE(SUM(size),0) AS bytes"). + Group("storage_provider"). + Scan(&res.ByProvider); err != nil { + return nil, err + } + + // 按分类(export / image / other) + for _, cat := range []string{"export", "image", "other"} { + count, bytes, err := s.fileCategoryStat(ctx, cat) + if err != nil { + return nil, err + } + res.ByCategory = append(res.ByCategory, v1.FileCategoryStat{Category: cat, Count: count, Bytes: bytes}) + } + + // Top 租户占用 + topN := req.TopN + if topN <= 0 { + topN = 10 + } + if err := dao.FilFiles.Ctx(ctx). + Fields("tenant_id, COUNT(*) AS count, COALESCE(SUM(size),0) AS bytes"). + Group("tenant_id"). + Order("bytes DESC"). + Limit(topN). + Scan(&res.TopTenants); err != nil { + return nil, err + } + + return res, nil +} + +// fileCategoryStat 统计单个分类的文件数与字节数。 +func (s *sAdmin) fileCategoryStat(ctx context.Context, category string) (int64, int64, error) { + m := dao.FilFiles.Ctx(ctx) + switch category { + case "export": + m = m.Where("storage_path LIKE ?", "exports/%") + case "image": + m = m.Where("mime_type LIKE ?", "image/%").Where("storage_path NOT LIKE ?", "exports/%") + default: // other + m = m.Where("COALESCE(mime_type,'') NOT LIKE ?", "image/%").Where("storage_path NOT LIKE ?", "exports/%") + } + var r struct { + Count int64 `json:"count"` + Bytes int64 `json:"bytes"` + } + if err := m.Fields("COUNT(*) AS count, COALESCE(SUM(size),0) AS bytes").Scan(&r); err != nil { + return 0, 0, err + } + return r.Count, r.Bytes, nil +} + +// FileDownload 生成临时预览下载链接。 +func (s *sAdmin) FileDownload(ctx context.Context, req *v1.FileDownloadReq) (*v1.FileDownloadRes, error) { + count, err := dao.FilFiles.Ctx(ctx).Where("id", req.Id).Count() + if err != nil { + return nil, err + } + if count == 0 { + return nil, common.NewNotFoundError("文件") + } + + svc, err := adminFileService(ctx) + if err != nil { + return nil, err + } + var url string + if req.Variant == "thumb" { + url, err = svc.GetThumbnailURL(ctx, req.Id, req.Width) + } else { + url, err = svc.GetDownloadURL(ctx, req.Id) + } + if err != nil { + return nil, err + } + return &v1.FileDownloadRes{Url: url}, nil +} + +// FileDelete 删除单个文件(存储对象 + 记录)。 +func (s *sAdmin) FileDelete(ctx context.Context, req *v1.FileDeleteReq) (*v1.FileDeleteRes, error) { + count, err := dao.FilFiles.Ctx(ctx).Where("id", req.Id).Count() + if err != nil { + return nil, err + } + if count == 0 { + return nil, common.NewNotFoundError("文件") + } + + svc, err := adminFileService(ctx) + if err != nil { + return nil, err + } + if err := svc.Delete(ctx, req.Id); err != nil { + return nil, err + } + return &v1.FileDeleteRes{}, nil +} + +// FileCleanup 手动触发一次保留期清理(导出 + 图片)。 +func (s *sAdmin) FileCleanup(ctx context.Context, req *v1.FileCleanupReq) (*v1.FileCleanupRes, error) { + result, err := RunFileRetentionNow(ctx) + if err != nil { + return nil, err + } + return &v1.FileCleanupRes{ + ExportsDeleted: result.ExportsDeleted, + ImagesDeleted: result.ImagesDeleted, + }, nil +} diff --git a/internal/logic/admin/permission.go b/internal/logic/admin/permission.go index 364b7f1..1a55b15 100644 --- a/internal/logic/admin/permission.go +++ b/internal/logic/admin/permission.go @@ -142,6 +142,13 @@ var predefinedPermissionGroups = []v1.PermissionGroup{ "redemption:view", "redemption:create", "redemption:edit", }, }, + { + Name: "file", + Label: "文件管理", + Permissions: []string{ + "file:view", "file:delete", "file:cleanup", + }, + }, } // GetUserPermissions returns permission points and data scopes for an admin user. diff --git a/internal/logic/common/audit_db.go b/internal/logic/common/audit_db.go index e0ef029..3217293 100644 --- a/internal/logic/common/audit_db.go +++ b/internal/logic/common/audit_db.go @@ -38,11 +38,16 @@ func IsAuditDBConfigured() bool { return auditDBConfigured } -// AuditModelCtx 返回指定审计表的 Model(带 context,绑定到审计库连接)。 +// AuditModelCtx 返回指定审计表的 Model(带 context)。 +// 仅 aud_request_logs(大模型请求审计)使用独立审计库(配置了 database.audit 时); +// 其余审计表(aud_operation_logs、aud_login_history、aud_sensitive_access_logs、 +// aud_content_filter_logs)始终使用主库,数据量小无需分离。 // 使用示例: // // common.AuditModelCtx(ctx, "aud_request_logs").Data(data).Insert() -// common.AuditModelCtx(ctx, "aud_operation_logs").Where("id", id).Scan(&record) func AuditModelCtx(ctx context.Context, table string) *gdb.Model { - return GetAuditDB().Model(table).Safe().Ctx(ctx) + if table == "aud_request_logs" { + return GetAuditDB().Model(table).Safe().Ctx(ctx) + } + return g.DB().Model(table).Safe().Ctx(ctx) } diff --git a/internal/logic/common/settings_registry.go b/internal/logic/common/settings_registry.go index 7583131..1ff60fb 100644 --- a/internal/logic/common/settings_registry.go +++ b/internal/logic/common/settings_registry.go @@ -221,16 +221,20 @@ var Registry = []SettingDef{ Label: "健康快照保留天数", Validation: "min:1,max:90"}, {Key: "channel_proxy_url", Type: SettingTypeString, Default: "", Category: "channel", Label: "代理地址", Description: "全局代理 URL,支持 http:// 和 socks5://,如 http://127.0.0.1:7890。启用代理的渠道会通过此代理转发请求"}, + {Key: "sync_image_async_enabled", Type: SettingTypeBool, Default: "true", Category: "channel", + Label: "同步图片厂商异步化", Description: "开启后,同步阻塞返回的图片厂商(如 OpenAI/DALL·E)走 /v1/images/generations/async 时由后台 worker 池异步处理,客户端提交即拿 task_id 后轮询取图;关闭则该端点对同步厂商返回不支持"}, + {Key: "sync_image_rehost_url", Type: SettingTypeBool, Default: "false", Category: "channel", + Label: "同步图片 URL 转存对象存储", Description: "开启后,上游返回图片 URL 时下载并转存对象存储(返回 24h 稳定链接,需已配置存储);关闭则直接透传上游 URL(部分厂商约 1h 过期)。b64_json 始终转存"}, // ── Storage ── {Key: "storage_provider", Type: SettingTypeString, Default: "minio", Category: "storage", - Label: "存储供应商", Validation: "enum:s3,minio,oss,cos", + Label: "存储供应商", Validation: "enum:s3,minio,r2,oss,cos", Description: "对象存储供应商类型"}, {Key: "storage_endpoint", Type: SettingTypeString, Default: "", Category: "storage", Label: "存储端点", - Description: "S3/MinIO: https://s3.amazonaws.com, OSS: https://oss-cn-hangzhou.aliyuncs.com, COS: https://cos.ap-guangzhou.myqcloud.com"}, + Description: "S3/MinIO: https://s3.amazonaws.com, OSS: https://oss-cn-hangzhou.aliyuncs.com, COS: https://cos.ap-guangzhou.myqcloud.com, R2: https://.r2.cloudflarestorage.com"}, {Key: "storage_region", Type: SettingTypeString, Default: "", Category: "storage", - Label: "存储区域", Description: "AWS Region / OSS Region / COS Region"}, + Label: "存储区域", Description: "AWS Region / OSS Region / COS Region(R2 固定填 auto)"}, {Key: "storage_bucket", Type: SettingTypeString, Default: "", Category: "storage", Label: "存储桶名称"}, {Key: "storage_access_key_id", Type: SettingTypeString, Default: "", Category: "storage", @@ -257,6 +261,8 @@ var Registry = []SettingDef{ Label: "GDPR删除请求完成天数", Validation: "min:7,max:90"}, {Key: "file_retention_enabled", Type: SettingTypeBool, Default: "true", Category: "data_governance", Label: "启用文件保留期检查"}, + {Key: "file_image_retention_days", Type: SettingTypeInt, Default: "0", Category: "data_governance", + Label: "AI图片保留天数(0=不清理)", Description: "AI re-host 图片超过该天数后自动清理,0 表示不自动删除", Validation: "min:0,max:3650"}, // ── Agreement (用户协议) ── {Key: "agreement_enabled", Type: SettingTypeBool, Default: "false", Category: "agreement", diff --git a/internal/logic/common/storage.go b/internal/logic/common/storage.go index ea5ed88..993dcea 100644 --- a/internal/logic/common/storage.go +++ b/internal/logic/common/storage.go @@ -6,10 +6,12 @@ import ( "github.com/qianfree/team-api/internal/dao" do "github.com/qianfree/team-api/internal/model/do" "io" + "strings" "time" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" + "github.com/google/uuid" ) // StorageProvider defines the interface for file storage backends. @@ -22,6 +24,11 @@ type StorageProvider interface { Delete(ctx context.Context, key string) error // PresignedURL generates a temporary URL for downloading. PresignedURL(ctx context.Context, key string, expires time.Duration) (string, error) + // PresignedThumbnailURL generates a temporary URL that returns a resized + // thumbnail (width in pixels, height auto). Providers with native + // server-side image processing (OSS/COS) apply it in the signed URL; + // providers without it (S3/MinIO/R2) return the original-object URL. + PresignedThumbnailURL(ctx context.Context, key string, width int, expires time.Duration) (string, error) } // FileService provides file storage operations with metadata tracking. @@ -66,12 +73,20 @@ type FileRecord struct { // Upload uploads a file, stores it via the provider, and records metadata. func (s *FileService) Upload(ctx context.Context, upload *FileUpload) (*FileRecord, error) { - // Generate storage key - key := fmt.Sprintf("%d/%d/%s", upload.TenantID, time.Now().Unix(), upload.Filename) + // Generate storage key (relative to the provider's configured path prefix): + // {date}/{uuid}{ext} — date-based dirs for lifecycle policies, UUID for + // uniqueness and non-guessability, original extension retained. + ext := extFromFilename(upload.Filename) + uuidStr := uuid.New().String() + key := fmt.Sprintf("%s/%s%s", time.Now().Format("2006-01-02"), uuidStr, ext) + fileName := uuidStr + ext - // Upload to storage provider - storagePath, err := s.provider.Upload(ctx, upload.Reader, key, upload.ContentType) - if err != nil { + // Upload to storage provider. The provider applies the configured path + // prefix internally, so we persist the RAW key (not the returned full key): + // Download/Delete/PresignedURL pass this key back through the provider, + // which re-applies the prefix exactly once. Storing the prefixed key here + // would cause the prefix to be applied twice on retrieval. + if _, err := s.provider.Upload(ctx, upload.Reader, key, upload.ContentType); err != nil { return nil, gerror.Wrapf(err, "upload to storage") } @@ -79,12 +94,12 @@ func (s *FileService) Upload(ctx context.Context, upload *FileUpload) (*FileReco result, err := dao.FilFiles.Ctx(ctx).Data(do.FilFiles{ TenantId: upload.TenantID, UserId: upload.UserID, - Filename: storagePath, + Filename: fileName, OriginalName: upload.Filename, MimeType: upload.ContentType, Size: upload.Size, StorageProvider: s.providerName, - StoragePath: storagePath, + StoragePath: key, VirusScanStatus: "pending", }).Insert() if err != nil { @@ -99,12 +114,12 @@ func (s *FileService) Upload(ctx context.Context, upload *FileUpload) (*FileReco ID: id, TenantID: upload.TenantID, UserID: upload.UserID, - Filename: storagePath, + Filename: fileName, OriginalName: upload.Filename, MimeType: upload.ContentType, Size: upload.Size, StorageProvider: s.providerName, - StoragePath: storagePath, + StoragePath: key, VirusScanStatus: "pending", }, nil } @@ -125,6 +140,31 @@ func (s *FileService) GetDownloadURL(ctx context.Context, fileID int64) (string, return s.provider.PresignedURL(ctx, record.StoragePath, 24*time.Hour) } +// GetThumbnailURL generates a presigned URL for previewing a downscaled +// thumbnail. Only images get server-side resizing; non-image files (and +// providers without native image processing) fall back to the original object. +func (s *FileService) GetThumbnailURL(ctx context.Context, fileID int64, width int) (string, error) { + var record *FileRecord + err := dao.FilFiles.Ctx(ctx). + Where("id", fileID). + Scan(&record) + if err != nil { + return "", gerror.Wrapf(err, "query file %d", fileID) + } + if record == nil { + return "", gerror.Newf("file not found: %d", fileID) + } + + if width <= 0 { + width = 400 + } + // Non-image objects cannot be resized; return the original presigned URL. + if !strings.HasPrefix(record.MimeType, "image/") { + return s.provider.PresignedURL(ctx, record.StoragePath, 24*time.Hour) + } + return s.provider.PresignedThumbnailURL(ctx, record.StoragePath, width, 24*time.Hour) +} + // Delete deletes a file from storage and marks it as deleted. func (s *FileService) Delete(ctx context.Context, fileID int64) error { var record *FileRecord @@ -153,3 +193,12 @@ func (s *FileService) Delete(ctx context.Context, fileID int64) error { return nil } + +// extFromFilename extracts the file extension (including the leading dot) +// from a filename. Returns empty string if no extension is found. +func extFromFilename(name string) string { + if idx := strings.LastIndex(name, "."); idx > 0 { + return name[idx:] + } + return "" +} diff --git a/internal/logic/common/storage_cos.go b/internal/logic/common/storage_cos.go index 3f648b1..b66dc22 100644 --- a/internal/logic/common/storage_cos.go +++ b/internal/logic/common/storage_cos.go @@ -109,3 +109,21 @@ func (s *COSStorageProvider) PresignedURL(ctx context.Context, key string, expir return presignedURL.String(), nil } + +// PresignedThumbnailURL signs a URL with a COS CI imageMogr2/thumbnail op so COS +// returns a downscaled thumbnail (width px, height auto). The process param is +// injected into the signed query so the signature covers it. +func (s *COSStorageProvider) PresignedThumbnailURL(ctx context.Context, key string, width int, expires time.Duration) (string, error) { + fullKey := s.fullKey(key) + + q := &url.Values{} + q.Add(fmt.Sprintf("imageMogr2/thumbnail/%dx", width), "") + opt := &cos.PresignedURLOptions{Query: q} + + presignedURL, err := s.client.Object.GetPresignedURL(ctx, http.MethodGet, fullKey, s.secretID, s.secretKey, expires, opt) + if err != nil { + return "", fmt.Errorf("cos presign thumbnail url: %w", err) + } + + return presignedURL.String(), nil +} diff --git a/internal/logic/common/storage_factory.go b/internal/logic/common/storage_factory.go index fd8cb7e..73d65df 100644 --- a/internal/logic/common/storage_factory.go +++ b/internal/logic/common/storage_factory.go @@ -38,7 +38,7 @@ func GetStorageConfig(ctx context.Context) *StorageConfig { // NewStorageProvider creates a StorageProvider based on the given configuration. func NewStorageProvider(cfg *StorageConfig) (StorageProvider, error) { switch cfg.Provider { - case "s3", "minio": + case "s3", "minio", "r2": return NewS3Provider(cfg) case "oss": return NewOSSProvider(cfg) diff --git a/internal/logic/common/storage_oss.go b/internal/logic/common/storage_oss.go index b170f40..4272401 100644 --- a/internal/logic/common/storage_oss.go +++ b/internal/logic/common/storage_oss.go @@ -97,3 +97,17 @@ func (s *OSSStorageProvider) PresignedURL(ctx context.Context, key string, expir return signedURL, nil } + +// PresignedThumbnailURL signs a URL with an x-oss-process image/resize op so OSS +// returns a downscaled thumbnail (width px, height auto, no upscaling). +func (s *OSSStorageProvider) PresignedThumbnailURL(ctx context.Context, key string, width int, expires time.Duration) (string, error) { + fullKey := s.fullKey(key) + + process := fmt.Sprintf("image/resize,w_%d", width) + signedURL, err := s.bucket.SignURL(fullKey, oss.HTTPGet, int64(expires.Seconds()), oss.Process(process)) + if err != nil { + return "", fmt.Errorf("oss sign thumbnail url: %w", err) + } + + return signedURL, nil +} diff --git a/internal/logic/common/storage_s3.go b/internal/logic/common/storage_s3.go index bc263bb..7742b78 100644 --- a/internal/logic/common/storage_s3.go +++ b/internal/logic/common/storage_s3.go @@ -45,9 +45,14 @@ func NewS3Provider(cfg *StorageConfig) (*S3StorageProvider, error) { if cfg.Region != "" { o.Region = cfg.Region } - if cfg.Provider == "minio" && cfg.Endpoint != "" { + // Cloudflare R2 is S3-compatible but requires an account-level + // BaseEndpoint. Unlike MinIO it uses virtual-hosted addressing, so + // path-style must NOT be forced. + if (cfg.Provider == "minio" || cfg.Provider == "r2") && cfg.Endpoint != "" { o.BaseEndpoint = aws.String(cfg.Endpoint) - o.UsePathStyle = true + if cfg.Provider == "minio" { + o.UsePathStyle = true + } } }) @@ -128,3 +133,9 @@ func (s *S3StorageProvider) PresignedURL(ctx context.Context, key string, expire return req.URL, nil } + +// PresignedThumbnailURL falls back to the original object: S3/MinIO/R2 have no +// native server-side image processing, so a true thumbnail is not available. +func (s *S3StorageProvider) PresignedThumbnailURL(ctx context.Context, key string, width int, expires time.Duration) (string, error) { + return s.PresignedURL(ctx, key, expires) +} diff --git a/internal/logic/common/storage_test.go b/internal/logic/common/storage_test.go new file mode 100644 index 0000000..3d3f176 --- /dev/null +++ b/internal/logic/common/storage_test.go @@ -0,0 +1,54 @@ +package common + +import "testing" + +// TestProviderFullKeyAppliesPrefixOnce guards the invariant behind the +// storage_path fix: every provider prepends the configured path prefix exactly +// once to whatever key it is given. FileService therefore MUST persist the RAW +// key (relative to the prefix) — if it stored the already-prefixed key returned +// by Upload, Download/Delete/PresignedURL would prefix it a second time and +// target a non-existent object (e.g. "team-api/team-api/…"). +func TestProviderFullKeyAppliesPrefixOnce(t *testing.T) { + const prefix = "team-api" + const rawKey = "42/1700000000/task_abc.png" + const want = prefix + "/" + rawKey + + cases := []struct { + name string + fullKey func(string) string + }{ + {"s3", (&S3StorageProvider{prefix: prefix}).fullKey}, + {"oss", (&OSSStorageProvider{prefix: prefix}).fullKey}, + {"cos", (&COSStorageProvider{prefix: prefix}).fullKey}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + // Raw key -> single prefix (the correct, fixed behavior). + if got := c.fullKey(rawKey); got != want { + t.Fatalf("fullKey(raw) = %q, want %q", got, want) + } + // An already-prefixed key would be double-prefixed — this is exactly + // the bug we avoid by storing the raw key in fil_files.storage_path. + if got := c.fullKey(want); got != prefix+"/"+want { + t.Fatalf("fullKey(prefixed) = %q, want double-prefixed %q (documents why raw key must be stored)", got, prefix+"/"+want) + } + }) + } +} + +// TestProviderFullKeyNoPrefix verifies that with an empty prefix the key is +// returned unchanged, so keys round-trip identically when no prefix is set. +func TestProviderFullKeyNoPrefix(t *testing.T) { + const rawKey = "exports/tenant_7/20240101_000000.json" + providers := map[string]func(string) string{ + "s3": (&S3StorageProvider{}).fullKey, + "oss": (&OSSStorageProvider{}).fullKey, + "cos": (&COSStorageProvider{}).fullKey, + } + for name, fullKey := range providers { + if got := fullKey(rawKey); got != rawKey { + t.Fatalf("%s: fullKey(%q) with empty prefix = %q, want unchanged", name, rawKey, got) + } + } +} diff --git a/internal/logic/common/tenant_model.go b/internal/logic/common/tenant_model.go index 239453e..a93dab1 100644 --- a/internal/logic/common/tenant_model.go +++ b/internal/logic/common/tenant_model.go @@ -5,6 +5,7 @@ import ( "github.com/gogf/gf/v2/frame/g" "github.com/qianfree/team-api/internal/dao" + "github.com/qianfree/team-api/relay/constant" ) // AvailableModelItem 租户可用模型基本信息(公共结构体,不含价格) @@ -136,3 +137,65 @@ func GetTenantAvailableModels(ctx context.Context, tenantID int64, category, sea return result, nil } + +// GetAsyncImageModelSet 判定给定图片模型(按 model_id 字符串)是否「走异步图片端点」。 +// +// 真相源是服务该模型的渠道 provider 类型:桥接 mdl_models.model_id = chn_abilities.model_name +// → chn_channels.type,用 constant.IsAsyncImageModel 分类(与同步端点拦截 gate 同源)。 +// - 当某模型的全部在役服务渠道都是异步图片 provider(如 DashScope wanx/qwen-image)时,同步 +// /v1/images/generations 必失败,必须走 /v1/images/generations/async 提交+轮询 → true。 +// - 此外若「同步图片厂商异步化」总开关(sync_image_async_enabled,默认开启)打开,同步阻塞 +// 返回的图片厂商(OpenAI/DALL·E 等)也由 worker 池在异步端点处理,故所有有渠道的图片模型 +// 一并走异步端点 → true。开关关闭时,同步厂商回落同步端点 → false。 +// +// 一次批量查询(非 N+1);仅对图片分类模型调用。modelIDs 为空时返回空 map。 +func GetAsyncImageModelSet(ctx context.Context, modelIDs []string) (map[string]bool, error) { + result := make(map[string]bool) + if len(modelIDs) == 0 { + return result, nil + } + + var rows []struct { + ModelName string `json:"model_name"` + Type int `json:"type"` + } + err := dao.ChnAbilities.Ctx(ctx).As("a"). + LeftJoin("chn_channels c ON a.channel_id = c.id"). + WhereIn("a.model_name", modelIDs). + Where("a.enabled", true). + Where("c.status", "active"). + Fields("DISTINCT a.model_name, c.type"). + Scan(&rows) + if err != nil { + return nil, err + } + + // 聚合每个模型的服务渠道 provider 类型:有渠道且全部为异步图片模型 → true。 + // 判定按 provider + 模型名(constant.IsAsyncImageModel),与同步端点拦截 gate 同源, + // 因此 qwen-image-2.x 等同步 multimodal 模型即使渠道为 Ali 也判为非异步。 + type aggState struct { + hasChannel bool + allAsync bool + } + agg := make(map[string]*aggState, len(modelIDs)) + for _, r := range rows { + st := agg[r.ModelName] + if st == nil { + st = &aggState{allAsync: true} + agg[r.ModelName] = st + } + st.hasChannel = true + if !constant.IsAsyncImageModel(constant.ProviderType(r.Type), r.ModelName) { + st.allAsync = false + } + } + // 「同步图片厂商异步化」总开关(默认开启):开启时,同步阻塞返回的图片厂商(OpenAI/DALL·E + // 等)也能走 /v1/images/generations/async 由 worker 池异步处理,因此任何有在役渠道的图片 + // 模型都应走异步端点(在线体验用提交+轮询)。关闭时,仅「全部在役渠道均为异步图片 provider」 + // 的模型才异步(此时同步端点必失败);同步厂商回落同步端点。 + syncImageEnabled := Config().GetBool(ctx, "sync_image_async_enabled") + for name, st := range agg { + result[name] = st.hasChannel && (st.allAsync || syncImageEnabled) + } + return result, nil +} diff --git a/internal/logic/docs/openapi.go b/internal/logic/docs/openapi.go index 618e47b..66943a7 100644 --- a/internal/logic/docs/openapi.go +++ b/internal/logic/docs/openapi.go @@ -155,6 +155,53 @@ func buildOpenAPISpec() map[string]any { }, }, }, + "/v1/images/generations/async": map[string]any{ + "post": map[string]any{ + "summary": "Image Generations Async (Submit)", + "description": "异步图像生成任务提交。阿里云百炼(DashScope)等异步图像上游无法经同步接口一次性返回,需提交任务拿到 task_id 后轮询 /v1/images/generations/async/{task_id} 取图。", + "operationId": "createImageAsync", + "security": []map[string]any{{"BearerAuth": []any{}}}, + "requestBody": map[string]any{ + "required": true, + "content": map[string]any{ + "application/json": map[string]any{ + "schema": map[string]any{ + "type": "object", + "required": []string{"model", "prompt"}, + "properties": map[string]any{ + "model": map[string]any{"type": "string", "example": "wanx2.1-t2i-turbo"}, + "prompt": map[string]any{"type": "string"}, + "n": map[string]any{"type": "integer", "default": 1}, + "size": map[string]any{"type": "string", "example": "1024x1024"}, + }, + }, + }, + }, + }, + "responses": map[string]any{ + "200": map[string]any{"description": "任务已提交,返回 task_id 与初始状态"}, + }, + }, + }, + "/v1/images/generations/async/{task_id}": map[string]any{ + "get": map[string]any{ + "summary": "Image Generations Async (Fetch)", + "description": "查询异步图像生成任务结果。轮询直到 status 为 SUCCESS(含图片 url)或 FAILURE(含 error),建议间隔 2~3 秒。", + "operationId": "getImageAsync", + "security": []map[string]any{{"BearerAuth": []any{}}}, + "parameters": []map[string]any{ + { + "name": "task_id", + "in": "path", + "required": true, + "schema": map[string]any{"type": "string"}, + }, + }, + "responses": map[string]any{ + "200": map[string]any{"description": "任务状态与结果"}, + }, + }, + }, "/v1/messages": map[string]any{ "post": map[string]any{ "summary": "Claude Messages", diff --git a/internal/logic/monitor/request_tracker.go b/internal/logic/monitor/request_tracker.go index 3ef0be8..25597ea 100644 --- a/internal/logic/monitor/request_tracker.go +++ b/internal/logic/monitor/request_tracker.go @@ -54,6 +54,7 @@ type RealtimeData struct { ByModel map[string]int `json:"by_model"` ByChannel map[string]int `json:"by_channel"` ByTenant map[int64]int `json:"by_tenant"` + SyncImagePool SyncImagePoolSnapshot `json:"sync_image_pool"` } // snapshotRing is a fixed-size circular buffer for arbitrary snapshot types. @@ -266,6 +267,7 @@ func GetRealtimeData() *RealtimeData { ByModel: byModel, ByChannel: byChannel, ByTenant: byTenant, + SyncImagePool: GetSyncImagePoolSnapshot(), } } diff --git a/internal/logic/monitor/sync_image_pool.go b/internal/logic/monitor/sync_image_pool.go new file mode 100644 index 0000000..c4c0d5b --- /dev/null +++ b/internal/logic/monitor/sync_image_pool.go @@ -0,0 +1,35 @@ +package monitor + +// SyncImagePoolSnapshot 是同步图片「异步化」worker 池的一次性状态快照。 +// 由 internal/logic/task 在启动 worker 池时通过 RegisterSyncImagePoolProvider 注入取数函数, +// monitor 反向持有该函数即可,避免 monitor → task 的导入环(task 已 import monitor)。 +type SyncImagePoolSnapshot struct { + WorkerTotal int `json:"worker_total"` // 池大小(固定) + WorkerBusy int `json:"worker_busy"` // 忙碌 worker 数(瞬时) + QueueDepth int `json:"queue_depth"` // 当前排队深度 + QueueCap int `json:"queue_cap"` // 队列容量 + Enqueued int64 `json:"enqueued"` // 累计入队成功 + Rejected int64 `json:"rejected"` // 累计拒绝(队列满 → 429 退款) + Succeeded int64 `json:"succeeded"` // 累计 worker 处理成功 + Failed int64 `json:"failed"` // 累计 worker 处理失败 + ChannelInflight map[int64]int `json:"channel_inflight"` // 每渠道在途数(层② per-channel 容量) + Enabled bool `json:"enabled"` // 池是否已启动(未注册 provider 时 false) +} + +// syncImagePoolProvider 由 task 包在启动 worker 池时注册。 +var syncImagePoolProvider func() SyncImagePoolSnapshot + +// RegisterSyncImagePoolProvider 注册同步图片 worker 池状态取数函数。 +func RegisterSyncImagePoolProvider(f func() SyncImagePoolSnapshot) { + syncImagePoolProvider = f +} + +// GetSyncImagePoolSnapshot 返回当前 worker 池状态;未注册(池未启动)时返回 Enabled=false。 +func GetSyncImagePoolSnapshot() SyncImagePoolSnapshot { + if syncImagePoolProvider == nil { + return SyncImagePoolSnapshot{Enabled: false} + } + s := syncImagePoolProvider() + s.Enabled = true + return s +} diff --git a/internal/logic/relay/provider.go b/internal/logic/relay/provider.go index 073572b..a01c7d6 100644 --- a/internal/logic/relay/provider.go +++ b/internal/logic/relay/provider.go @@ -104,12 +104,13 @@ func (p *DataProviderImpl) tryAffinityChannel(ctx context.Context, tenantID int6 } type channelRow struct { - ChannelID int64 `json:"channel_id"` - ChannelName string `json:"channel_name"` - ChannelType int `json:"channel_type"` - BaseURL string `json:"base_url"` - UpstreamModel string `json:"upstream_model"` - Settings string `json:"settings"` + ChannelID int64 `json:"channel_id"` + ChannelName string `json:"channel_name"` + ChannelType int `json:"channel_type"` + BaseURL string `json:"base_url"` + UpstreamModel string `json:"upstream_model"` + MaxConcurrency int `json:"max_concurrency"` + Settings string `json:"settings"` } var ch *channelRow @@ -119,7 +120,7 @@ func (p *DataProviderImpl) tryAffinityChannel(ctx context.Context, tenantID int6 Where("c.status", "active"). Where("a.model_name", modelName). Where("a.enabled", true). - Fields("c.id as channel_id, c.name as channel_name, c.type as channel_type, c.base_url, a.upstream_model, c.settings"). + Fields("c.id as channel_id, c.name as channel_name, c.type as channel_type, c.base_url, a.upstream_model, c.max_concurrency, c.settings"). Scan(&ch) if err != nil || ch == nil { return nil, common.ErrChannelUnavailable @@ -144,6 +145,7 @@ func (p *DataProviderImpl) tryAffinityChannel(ctx context.Context, tenantID int6 ApiKey: key, UpstreamModelName: upstreamModel, IsModelMapped: ch.UpstreamModel != "" && ch.UpstreamModel != modelName, + MaxConcurrency: ch.MaxConcurrency, Settings: settings, }, nil } @@ -158,6 +160,7 @@ func (p *DataProviderImpl) selectChannelFromDB(ctx context.Context, tenantID int UpstreamModel string `json:"upstream_model"` Priority int `json:"priority"` Weight int `json:"weight"` + MaxConcurrency int `json:"max_concurrency"` Settings string `json:"settings"` HealthScore *float64 `json:"health_score"` ConsecutiveFailures int `json:"consecutive_failures"` @@ -169,7 +172,7 @@ func (p *DataProviderImpl) selectChannelFromDB(ctx context.Context, tenantID int Where("a.model_name", modelName). Where("a.enabled", true). Where("c.status", "active"). - Fields("c.id as channel_id, c.name as channel_name, c.type as channel_type, c.base_url, a.upstream_model, c.priority, c.weight, c.settings, h.health_score, h.consecutive_failures"). + Fields("c.id as channel_id, c.name as channel_name, c.type as channel_type, c.base_url, a.upstream_model, c.priority, c.weight, c.max_concurrency, c.settings, h.health_score, h.consecutive_failures"). OrderDesc("c.priority"). OrderDesc("c.weight") @@ -211,6 +214,7 @@ func (p *DataProviderImpl) selectChannelFromDB(ctx context.Context, tenantID int ApiKey: key, UpstreamModelName: upstreamModel, IsModelMapped: ch.UpstreamModel != "" && ch.UpstreamModel != modelName, + MaxConcurrency: ch.MaxConcurrency, Settings: settings, }, nil } @@ -620,14 +624,37 @@ func (p *DataProviderImpl) UpdateTaskAudit(ctx context.Context, record *common.A updateData.LatencyMs = record.LatencyMs } - _, err := lcommon.AuditModelCtx(bgCtx, "aud_request_logs"). - Where("task_id", record.TaskID). - Data(updateData). - Update() - if err != nil { - g.Log().Errorf(bgCtx, - "update task audit failed: task_id=%s err=%v", - record.TaskID, err) + // 提交阶段的审计 INSERT 是异步的(RecordAudit 内 go func 落库)。对 sync_image 这类 + // 提交后立即入队、worker 可能极快出终态的任务,完成回调有可能抢在 INSERT 之前到达, + // 导致按 task_id 更新命中 0 行、终态(SUCCESS/FAILURE)丢失,审计里的任务状态一直停在 + // 「已提交」。这里在命中 0 行时短暂重试,兜住这个竞态窗口。 + // 审计关闭时提交阶段本就不写行,无需重试,直接返回避免空转。 + auditDisabled := globalLevel == lcommon.AuditLevelNone && tenantLevel == lcommon.AuditLevelNone + const maxAttempts = 6 + for attempt := 1; ; attempt++ { + res, err := lcommon.AuditModelCtx(bgCtx, "aud_request_logs"). + Where("task_id", record.TaskID). + Data(updateData). + Update() + if err != nil { + g.Log().Errorf(bgCtx, + "update task audit failed: task_id=%s attempt=%d err=%v", + record.TaskID, attempt, err) + return + } + if affected, _ := res.RowsAffected(); affected > 0 { + return + } + if auditDisabled || attempt >= maxAttempts { + if !auditDisabled { + g.Log().Warningf(bgCtx, + "update task audit affected 0 rows after %d attempts: task_id=%s (submit audit not persisted yet?)", + maxAttempts, record.TaskID) + } + return + } + // 线性退避:150ms、300ms……最长约 2.25s,足够覆盖异步 INSERT 落库延迟。 + time.Sleep(time.Duration(attempt) * 150 * time.Millisecond) } }() } diff --git a/internal/logic/task/async_polling.go b/internal/logic/task/async_polling.go index 2f59222..aa6cc5d 100644 --- a/internal/logic/task/async_polling.go +++ b/internal/logic/task/async_polling.go @@ -86,6 +86,12 @@ func pollOnce(ctx context.Context) { // 3. 按 platform 分组处理 platformGroups := make(map[string][]*common.AsyncTask) for _, t := range tasks { + // sync_image 无上游任务 ID 可轮询,由 worker 池自行推进;此处跳过, + // 避免 processPlatformTasks → taskchannel.GetAdaptor("sync_image") 报错刷日志。 + // 其超时/未结算仍由 handleTimedOutTasks / handleUnsettledTasks 兜底。 + if t.Platform == string(constant.TaskPlatformSyncImage) { + continue + } platformGroups[t.Platform] = append(platformGroups[t.Platform], t) } @@ -94,6 +100,19 @@ func pollOnce(ctx context.Context) { } } +// buildTimeoutFailure 把超时任务标记为失败,并返回**覆盖前**的真实状态供 CAS 谓词使用。 +// +// 独立成纯函数以便回归测试。历史 bug:调用方先 `t.Status = "FAILURE"` 再把 `t.Status` +// 当作 UpdateTaskCAS 的 oldStatus 传入,使 CAS 变成 `WHERE status='FAILURE' SET status='FAILURE'`, +// 对任何非终态行匹配 0 行 → 超时兜底网从未生效(超时任务不退款、active 计数不减)。 +func buildTimeoutFailure(t *common.AsyncTask, now time.Time) (oldStatus string) { + oldStatus = t.Status + t.Status = "FAILURE" + t.FailReason = "task timed out" + t.FinishTime = &now + return oldStatus +} + // handleTimedOutTasks 处理超时任务 func handleTimedOutTasks(ctx context.Context) { cutoff := time.Now().Add(-taskTimeout).Unix() @@ -104,12 +123,11 @@ func handleTimedOutTasks(ctx context.Context) { } for _, t := range tasks { - t.Status = "FAILURE" - t.FailReason = "task timed out" now := time.Now() - t.FinishTime = &now + // 必须用覆盖前的真实状态(IN_PROGRESS/QUEUED/SUBMITTED…)作为 CAS 谓词。 + oldStatus := buildTimeoutFailure(t, now) - if err := DefaultAsyncProvider.UpdateTaskCAS(ctx, t, t.Status); err != nil { + if err := DefaultAsyncProvider.UpdateTaskCAS(ctx, t, oldStatus); err != nil { g.Log().Warningf(ctx, "poll: mark timeout task %s: %v", t.PublicTaskID, err) continue } @@ -147,6 +165,13 @@ func handleUnsettledTasks(ctx context.Context) { } for _, t := range tasks { + // sync_image 无 upstream_task_id,不能落入下面「UpstreamTaskID=="" → 退款」的分支, + // 否则崩溃窗口内的成功任务会被误退款(用户出图却全额退回,资损)。单独处理。 + if t.Platform == string(constant.TaskPlatformSyncImage) { + handleUnsettledSyncImage(ctx, t) + continue + } + var pd privateData if err := json.Unmarshal(t.PrivateData, &pd); err != nil || pd.UpstreamTaskID == "" { // 无法恢复,直接退还预扣 @@ -196,6 +221,44 @@ func handleUnsettledTasks(ctx context.Context) { } } +// handleUnsettledSyncImage 结算重试网中 sync_image 任务的专用处理。 +// +// sync_image 无 upstream_task_id,通用分支会把它当「不可恢复」直接退款——但对 worker 已判 +// SUCCESS、仅 billing_settled 未落库的崩溃窗口任务而言,退款即资损(用户出图却被全额退回)。 +// 因此这里按终态区分:SUCCESS 用 private_data 的 billing_context 重放结算,FAILURE 才退款。 +func handleUnsettledSyncImage(ctx context.Context, t *common.AsyncTask) { + taskBilling := billing.NewTaskBillingProvider() + + if t.Status == "SUCCESS" { + var pd privateData + _ = json.Unmarshal(t.PrivateData, &pd) + actualCost := t.ActualCost + if actualCost <= 0 { + actualCost = t.PreDeductAmount + } + if _, err := taskBilling.SettleTaskSuccess(ctx, t.TenantID, t.UserID, t.ApiKeyID, t.ChannelID, + t.ModelName, t.RequestID, actualCost, t.PreDeductAmount, 0, 0, pd.BillingContext.Ratios, t.PublicTaskID); err != nil { + g.Log().Warningf(ctx, "poll: retry settle sync_image task %s: %v", t.PublicTaskID, err) + return + } + t.BillingSettled = true + t.ActualCost = actualCost + taskBilling.IncrApiKeyQuotaUsed(ctx, t.ApiKeyID, actualCost) + DefaultAsyncProvider.UpdateTask(ctx, t) + g.Log().Infof(ctx, "poll: retried settlement for sync_image task %s", t.PublicTaskID) + return + } + + // FAILURE:退还预扣 + if err := taskBilling.SettleTaskFailed(ctx, t.TenantID, t.RequestID, t.PreDeductAmount); err != nil { + g.Log().Warningf(ctx, "poll: retry refund sync_image task %s: %v", t.PublicTaskID, err) + return + } + t.BillingSettled = true + DefaultAsyncProvider.UpdateTask(ctx, t) + billing.CleanupPreDeduct(ctx, t.TenantID, t.RequestID+"_adjust") +} + // processPlatformTasks 处理同一平台的任务 func processPlatformTasks(ctx context.Context, platform string, tasks []*common.AsyncTask) { // 按 channel_id 分组 diff --git a/internal/logic/task/async_polling_test.go b/internal/logic/task/async_polling_test.go new file mode 100644 index 0000000..9bb67f1 --- /dev/null +++ b/internal/logic/task/async_polling_test.go @@ -0,0 +1,35 @@ +package task + +import ( + "testing" + "time" + + "github.com/qianfree/team-api/relay/common" +) + +// TestBuildTimeoutFailure_UsesOriginalStatusAsCASPredicate 锁定超时兜底网的回归 bug: +// UpdateTaskCAS 的 oldStatus 必须是覆盖前的真实状态,而不是已改写的 "FAILURE"。 +func TestBuildTimeoutFailure_UsesOriginalStatusAsCASPredicate(t *testing.T) { + for _, orig := range []string{"IN_PROGRESS", "QUEUED", "SUBMITTED", "NOT_START"} { + task := &common.AsyncTask{PublicTaskID: "task_x", Status: orig} + now := time.Now() + + oldStatus := buildTimeoutFailure(task, now) + + if oldStatus != orig { + t.Fatalf("oldStatus = %q, want %q (must be the pre-mutation status, never \"FAILURE\")", oldStatus, orig) + } + if oldStatus == "FAILURE" { + t.Fatal("regression: oldStatus must not be the mutated FAILURE value") + } + if task.Status != "FAILURE" { + t.Fatalf("task.Status = %q, want FAILURE", task.Status) + } + if task.FailReason != "task timed out" { + t.Fatalf("task.FailReason = %q, want 'task timed out'", task.FailReason) + } + if task.FinishTime == nil || !task.FinishTime.Equal(now) { + t.Fatal("task.FinishTime must be set to now") + } + } +} diff --git a/internal/logic/task/mem_writer.go b/internal/logic/task/mem_writer.go new file mode 100644 index 0000000..a619b5a --- /dev/null +++ b/internal/logic/task/mem_writer.go @@ -0,0 +1,48 @@ +package task + +import ( + "bytes" + "net/http" +) + +// memResponseWriter 是一个纯内存的 http.ResponseWriter 实现。 +// +// sync_image worker 复刻 relay 管线时没有真实的客户端 HTTP 连接, +// adaptor.DoResponse 会把上游响应写入传入的 writer。使用本类型捕获响应体和状态码, +// 供 worker 后续解析(b64_json / url),而不会像 ResponseCaptureWriter 那样 +// 因内嵌 nil 的真实 writer 而 panic。 +type memResponseWriter struct { + header http.Header + buf bytes.Buffer + status int +} + +// newMemResponseWriter 创建内存 writer,默认状态码 200(与 net/http 语义一致)。 +func newMemResponseWriter() *memResponseWriter { + return &memResponseWriter{ + header: make(http.Header), + status: http.StatusOK, + } +} + +func (w *memResponseWriter) Header() http.Header { + return w.header +} + +func (w *memResponseWriter) WriteHeader(code int) { + w.status = code +} + +func (w *memResponseWriter) Write(b []byte) (int, error) { + return w.buf.Write(b) +} + +// Bytes 返回已捕获的响应体。 +func (w *memResponseWriter) Bytes() []byte { + return w.buf.Bytes() +} + +// StatusCode 返回捕获的状态码。 +func (w *memResponseWriter) StatusCode() int { + return w.status +} diff --git a/internal/logic/task/sync_image_worker.go b/internal/logic/task/sync_image_worker.go new file mode 100644 index 0000000..7066ecd --- /dev/null +++ b/internal/logic/task/sync_image_worker.go @@ -0,0 +1,762 @@ +package task + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "math/rand" + "net/http" + "sync" + "sync/atomic" + "time" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gctx" + + "github.com/qianfree/team-api/internal/logic/billing" + lcommon "github.com/qianfree/team-api/internal/logic/common" + "github.com/qianfree/team-api/internal/logic/monitor" + "github.com/qianfree/team-api/internal/logic/relay" + "github.com/qianfree/team-api/relay/channel" + "github.com/qianfree/team-api/relay/common" + "github.com/qianfree/team-api/relay/constant" + "github.com/qianfree/team-api/relay/dto" +) + +// 同步图片厂商「异步化」worker 池。 +// +// 背景:OpenAI/DALL·E 等图片厂商同步阻塞返回(10–60s)。走 /v1/images/generations/async +// 端点时,提交阶段立即建 QUEUED 任务并返回 task_id,真正持有上游长连接的动作交给这里的 +// 有界 worker 池(默认 100)在后台执行,客户端轮询 /fetch 取结果。 +// +// 三层并发控制:① 全局池(worker 数硬上限)② 单渠道(channelInflight vs MaxConcurrency) +// ③ 渠道间(GetChannelForModel 优先级/权重 + 饱和渠道 exclude 重选)。 + +const ( + syncImageWorkerCount = 100 // 全局 worker 池大小(层①) + syncImageQueueSize = 1000 // 有界队列容量,满则提交侧退款+429 + syncImageMaxAttempts = 16 // 单任务选渠道最大尝试次数(防全饱和时空转) + syncImageDownloadCap = 20 << 20 + + // syncImageShutdownWait 停机时等待在途任务收尾的最长时间。超过则强制停止, + // 把仍未完成的在途任务标记为超时(TIMEOUT)并退款,不再无限等待上游返回。 + syncImageShutdownWait = 3 * time.Minute +) + +var ( + syncImageQueue chan *SyncImageJob + syncImageStop chan struct{} // 关闭 → worker 不再领取新任务,处理完在途任务即退出 + syncImageWg sync.WaitGroup + syncImageShutdown atomic.Bool // 停机中:拒绝新任务入队 + + // 本实例在途任务登记表(TaskID -> job)。用于停机超时时,只对**本实例**正在执行的 + // 任务做超时+退款,避免误伤多实例部署下其他实例的 IN_PROGRESS 任务。 + syncImageRunningMu sync.Mutex + syncImageRunning = make(map[int64]*SyncImageJob) + + channelInflightMu sync.Mutex + channelInflight = make(map[int64]int) + + // 池状态计数器(进程内累计,重启归零;供实时监控面板展示)。 + syncImageBusy atomic.Int64 // 忙碌 worker 数(瞬时) + syncImageEnqueued atomic.Int64 // 累计入队成功 + syncImageRejected atomic.Int64 // 累计拒绝(队列满 → 429 退款) + syncImageSucceeded atomic.Int64 // 累计 worker 处理成功 + syncImageFailed atomic.Int64 // 累计 worker 处理失败 + + syncImageFileSvc *lcommon.FileService + syncImageBilling common.TaskBillingProvider + syncImageRelayProv common.DataProvider +) + +// SyncImageJob 同步图片任务的内存载荷(请求体只放内存,不落库,无需崩溃重放)。 +type SyncImageJob struct { + TaskID int64 // tsk_model_tasks 主键 + PublicTaskID string + RequestID string + TenantID int64 + UserID int64 + ApiKeyID int64 + ProjectID int64 + Model string + RequestBody []byte + PreDeductAmount float64 + Ratios map[string]float64 + SubmitTime time.Time +} + +// StartSyncImageWorkers 启动 worker 池,与 StartAsyncPolling 同期在 cmd.go 接线。 +func StartSyncImageWorkers(ctx context.Context) { + syncImageQueue = make(chan *SyncImageJob, syncImageQueueSize) + syncImageStop = make(chan struct{}) + syncImageShutdown.Store(false) + syncImageRunningMu.Lock() + syncImageRunning = make(map[int64]*SyncImageJob) + syncImageRunningMu.Unlock() + syncImageBilling = billing.NewTaskBillingProvider() + syncImageRelayProv = relay.NewDataProvider() + + // 对象存储在启动时构造一次共享(无状态,可安全共享)。未配置时置 nil, + // b64_json 任务在无存储时会走 FAILURE+退款(见 buildImageResult)。 + if fs, err := lcommon.NewFileServiceFromConfig(ctx); err != nil { + g.Log().Warningf(ctx, "sync_image: object storage not configured (%v); b64_json image tasks will fail until storage is set", err) + } else { + syncImageFileSvc = fs + } + + for i := 0; i < syncImageWorkerCount; i++ { + syncImageWg.Add(1) + go syncImageWorkerLoop(i) + } + g.Log().Infof(ctx, "sync_image: started %d workers (queue=%d)", syncImageWorkerCount, syncImageQueueSize) + + // 注册状态取数函数,供管理后台实时监控面板展示池状态。 + monitor.RegisterSyncImagePoolProvider(SyncImageWorkerStats) +} + +// StopSyncImageWorkers 停机: +// 1. 置停机标志,拒绝新任务入队,通知 worker 不再领取新任务; +// 2. 将队列中未开始的排队任务全部置为 FAILURE 并退款; +// 3. 有界等待在途任务收尾(最长 syncImageShutdownWait);超过则强制停止, +// 把仍未完成的在途任务标记为超时(TIMEOUT)并退款,不再无限等待上游返回。 +func StopSyncImageWorkers() { + if syncImageQueue == nil { + return + } + ctx := gctx.New() + + // 1. 拒绝新入队,并通知 worker 停止领取新任务。 + syncImageShutdown.Store(true) + // 幂等:避免重复 close panic。 + select { + case <-syncImageStop: + default: + close(syncImageStop) + } + + // 2. 将队列中未开始的排队任务全部失败并退款。 + // 此时 worker 收到 stop 后不再领取新任务;即便有个别 worker 在关闭瞬间抢走一条, + // channel 接收是原子的,该任务转为在途(由步骤 3 处理),不会与本次排干重复。 + failQueuedOnShutdown(ctx) + + // 3. 有界等待在途任务收尾。 + done := make(chan struct{}) + go func() { + syncImageWg.Wait() + close(done) + }() + + select { + case <-done: + g.Log().Info(ctx, "sync_image: 所有在途任务已完成,worker 池已停止") + case <-time.After(syncImageShutdownWait): + // 超过等待上限:强制停止,未完成的在途任务标记为超时并退款。 + // 残留 worker goroutine 仍卡在上游,随进程退出而终止;其后续收尾会因 CAS + // 谓词失配而 no-op,不会与这里的超时处理重复结算。 + g.Log().Warningf(ctx, "sync_image: 等待在途任务超过 %v,强制停止;未完成任务标记为超时并退款", syncImageShutdownWait) + timeoutRunningOnShutdown(ctx) + } +} + +// failQueuedOnShutdown 排干队列,把未开始的排队任务全部置为 FAILURE 并退款。 +func failQueuedOnShutdown(ctx context.Context) { + failed := 0 + for { + select { + case job := <-syncImageQueue: + if job == nil { + continue + } + failQueuedSyncImageJob(ctx, job, "server shutting down") + failed++ + default: + if failed > 0 { + g.Log().Infof(ctx, "sync_image: 停机将 %d 个未开始的排队任务置为失败并退款", failed) + } + return + } + } +} + +// timeoutRunningOnShutdown 停机超时:把本实例所有仍未完成的在途任务标记为超时并退款。 +func timeoutRunningOnShutdown(ctx context.Context) { + syncImageRunningMu.Lock() + jobs := make([]*SyncImageJob, 0, len(syncImageRunning)) + for _, j := range syncImageRunning { + jobs = append(jobs, j) + } + syncImageRunningMu.Unlock() + + timedOut := 0 + for _, job := range jobs { + if timeoutRunningSyncImageJob(ctx, job) { + timedOut++ + } + } + if timedOut > 0 { + g.Log().Warningf(ctx, "sync_image: 停机强制将 %d 个在途任务标记为超时并退款", timedOut) + } +} + +// timeoutRunningSyncImageJob 把单个在途任务标记为超时并退款(语义对齐 handleTimedOutTasks): +// DB status 置 FAILURE(task timed out)、审计 task_status 置 TIMEOUT。CAS 谓词 IN_PROGRESS +// 保护:若残留 worker 已抢先落终态,这里 no-op,避免重复退款/重复减 active 计数。 +func timeoutRunningSyncImageJob(ctx context.Context, job *SyncImageJob) bool { + now := time.Now() + const reason = "task timed out" + + if err := DefaultAsyncProvider.UpdateTaskCAS(ctx, &common.AsyncTask{ + ID: job.TaskID, + Status: "FAILURE", + FailReason: reason, + FinishTime: &now, + }, "IN_PROGRESS"); err != nil { + return false // 已被 worker 收尾或其他方抢占 + } + + settled := true + if job.PreDeductAmount > 0 { + if err := syncImageBilling.SettleTaskFailed(ctx, job.TenantID, job.RequestID, job.PreDeductAmount); err != nil { + g.Log().Warningf(ctx, "sync_image: timeout task %s refund failed (unsettled net will retry): %v", job.PublicTaskID, err) + settled = false + } + } + if settled { + _ = DefaultAsyncProvider.UpdateTask(ctx, &common.AsyncTask{ + ID: job.TaskID, + Status: "FAILURE", + FailReason: reason, + FinishTime: &now, + BillingSettled: true, + }) + } + + syncImageFailed.Add(1) + DecrActiveTask() + monitor.UnregisterRequestByTaskID(job.PublicTaskID) + + usageTask := buildUsageTask(job, nil, "FAILURE", 0, now) + recordTaskUsage(usageTask, nil, false, reason, nil) + // 闭环审计:把提交阶段的 SUBMITTED 审计记录更新为 TIMEOUT(与超时兜底网一致)。 + recordTaskCompletionAudit(usageTask, "TIMEOUT", reason, nil) + return true +} + +// EnqueueSyncImageJob 非阻塞入队;队列满返回 false,供提交侧退款+429。 +func EnqueueSyncImageJob(job *SyncImageJob) bool { + if syncImageQueue == nil || syncImageShutdown.Load() { + syncImageRejected.Add(1) + return false + } + select { + case syncImageQueue <- job: + syncImageEnqueued.Add(1) + return true + default: + syncImageRejected.Add(1) + return false + } +} + +// SyncImageWorkerStats 返回 worker 池当前状态快照,供管理后台实时监控面板展示。 +// 在 StartSyncImageWorkers 中注册进 monitor 包(Provider 注入,避免 monitor → task 导入环)。 +func SyncImageWorkerStats() monitor.SyncImagePoolSnapshot { + channelInflightMu.Lock() + inflight := make(map[int64]int, len(channelInflight)) + for k, v := range channelInflight { + inflight[k] = v + } + channelInflightMu.Unlock() + + qLen, qCap := 0, 0 + if syncImageQueue != nil { + qLen, qCap = len(syncImageQueue), cap(syncImageQueue) + } + + return monitor.SyncImagePoolSnapshot{ + WorkerTotal: syncImageWorkerCount, + WorkerBusy: int(syncImageBusy.Load()), + QueueDepth: qLen, + QueueCap: qCap, + Enqueued: syncImageEnqueued.Load(), + Rejected: syncImageRejected.Load(), + Succeeded: syncImageSucceeded.Load(), + Failed: syncImageFailed.Load(), + ChannelInflight: inflight, + } +} + +func syncImageWorkerLoop(workerID int) { + defer syncImageWg.Done() + for { + // 优先响应停机:一旦收到停机信号,不再领取新的排队任务。已在执行中的在途任务 + // 由 runSyncImageJob 同步跑完(syncImageWg 会等待),未开始的排队任务由 + // StopSyncImageWorkers 在 worker 全部退出后统一置为 FAILURE。 + select { + case <-syncImageStop: + return + default: + } + + select { + case <-syncImageStop: + return + case job := <-syncImageQueue: + if job == nil { + return + } + runSyncImageJob(workerID, job) + } + } +} + +// failQueuedSyncImageJob 停机时把仍处于 QUEUED(未开始)的任务置为 FAILURE 并退款。 +// 用 CAS QUEUED->FAILURE 保护:若该任务已被某 worker 抢走(IN_PROGRESS/终态),这里 no-op, +// 由该 worker 负责结算,避免重复退款。 +func failQueuedSyncImageJob(ctx context.Context, job *SyncImageJob, reason string) { + now := time.Now() + reason = truncateStr(reason, 500) + + if err := DefaultAsyncProvider.UpdateTaskCAS(ctx, &common.AsyncTask{ + ID: job.TaskID, + Status: "FAILURE", + FailReason: reason, + FinishTime: &now, + }, "QUEUED"); err != nil { + return + } + + settled := true + if job.PreDeductAmount > 0 { + if err := syncImageBilling.SettleTaskFailed(ctx, job.TenantID, job.RequestID, job.PreDeductAmount); err != nil { + g.Log().Warningf(ctx, "sync_image: shutdown-fail task %s refund failed (unsettled net will retry): %v", job.PublicTaskID, err) + settled = false + } + } + if settled { + _ = DefaultAsyncProvider.UpdateTask(ctx, &common.AsyncTask{ + ID: job.TaskID, + Status: "FAILURE", + FailReason: reason, + FinishTime: &now, + BillingSettled: true, + }) + } + + syncImageFailed.Add(1) + DecrActiveTask() + monitor.UnregisterRequestByTaskID(job.PublicTaskID) + + usageTask := buildUsageTask(job, nil, "FAILURE", 0, now) + recordTaskUsage(usageTask, nil, false, reason, nil) + // 闭环审计:把提交阶段的 SUBMITTED 审计记录更新为 FAILURE。 + recordTaskCompletionAudit(usageTask, "FAILURE", reason, nil) +} + +// runSyncImageJob 每任务 recover 兜 panic,防单任务崩溃拖垮 worker。 +func runSyncImageJob(workerID int, job *SyncImageJob) { + syncImageBusy.Add(1) + // 登记为本实例在途任务,供停机超时时定向处理;defer 保证 panic 也能摘除。 + syncImageRunningMu.Lock() + syncImageRunning[job.TaskID] = job + syncImageRunningMu.Unlock() + defer func() { + syncImageRunningMu.Lock() + delete(syncImageRunning, job.TaskID) + syncImageRunningMu.Unlock() + syncImageBusy.Add(-1) + if r := recover(); r != nil { + g.Log().Errorf(gctx.New(), "sync_image: worker %d panic on task %s: %v", workerID, job.PublicTaskID, r) + } + }() + processSyncImageJob(job) +} + +func processSyncImageJob(job *SyncImageJob) { + ctx := gctx.New() + + // 1. CAS QUEUED -> IN_PROGRESS(与超时网互斥) + now := time.Now() + if err := DefaultAsyncProvider.UpdateTaskCAS(ctx, &common.AsyncTask{ + ID: job.TaskID, + Status: "IN_PROGRESS", + Progress: "0%", + StartTime: &now, + }, "QUEUED"); err != nil { + g.Log().Warningf(ctx, "sync_image: task %s CAS QUEUED->IN_PROGRESS failed (already taken): %v", job.PublicTaskID, err) + return + } + + // 2. 选渠道循环 + 复刻管线(失败换渠道重试) + var exclude []int64 + lastErr := "no available channel" + for attempt := 0; attempt < syncImageMaxAttempts; attempt++ { + sel, err := syncImageRelayProv.GetChannelForModel(ctx, job.TenantID, job.UserID, job.Model, exclude) + if err != nil { + lastErr = fmt.Sprintf("select channel: %v", err) + break // 无更多候选 + } + + // 层②:per-channel 容量。MaxConcurrency<=0 视为不限。 + if !tryOccupyChannel(sel.ChannelID, sel.MaxConcurrency) { + exclude = append(exclude, sel.ChannelID) + syncImageJitter() + continue + } + + ok, memW, perr := runImagePipelineWithRelease(ctx, job, sel) + if ok { + settleSyncImageSuccess(ctx, job, sel, memW) + return + } + + // 失败:降健康度 + 排除该渠道 + 抖动退避重选 + syncImageRelayProv.UpdateChannelHealth(ctx, sel.ChannelID, false, 0) + syncImageRelayProv.IncrementConsecutiveFailure(ctx, sel.ChannelID) + exclude = append(exclude, sel.ChannelID) + lastErr = perr + syncImageJitter() + } + + // 3. 全饱和/无候选/重试耗尽 → FAILURE + 退款 + failSyncImageJob(ctx, job, nil, lastErr) +} + +// runImagePipelineWithRelease 保证无论成功/失败/panic 都释放 per-channel 槽(defer)。 +func runImagePipelineWithRelease(ctx context.Context, job *SyncImageJob, sel *common.ChannelSelection) (ok bool, memW *memResponseWriter, failReason string) { + defer decInflight(sel.ChannelID) + return runImagePipeline(ctx, job, sel) +} + +// runImagePipeline 复刻 RelayHandler 的最小管线,把上游响应捕获到内存 writer。 +func runImagePipeline(ctx context.Context, job *SyncImageJob, sel *common.ChannelSelection) (bool, *memResponseWriter, string) { + info := &common.RelayInfo{ + Context: ctx, + TenantID: job.TenantID, + UserID: job.UserID, + ApiKeyID: job.ApiKeyID, + ProjectID: job.ProjectID, + RequestID: job.RequestID, + RelayMode: int(constant.RelayModeImagesGenerations), + IsStream: false, + OriginModelName: job.Model, + BaseModelName: job.Model, + RequestURLPath: "/v1/images/generations", + RequestHeaders: http.Header{ + "Content-Type": []string{"application/json"}, + "Accept": []string{"application/json"}, + }, + StartTime: time.Now(), + StreamStatus: common.NewStreamStatus(), + InboundFormat: constant.RelayFormatOpenAI, + ClientFormat: constant.RelayFormatOpenAI, + ChannelMeta: &common.ChannelMeta{ + ChannelID: sel.ChannelID, + ChannelType: sel.ChannelType, + ChannelName: sel.ChannelName, + BaseURL: sel.BaseURL, + ApiKey: sel.ApiKey, + UpstreamModelName: sel.UpstreamModelName, + IsModelMapped: sel.IsModelMapped, + Settings: sel.Settings, + }, + } + + adaptor := channel.GetAdaptor(sel.ChannelType) + if adaptor == nil { + return false, nil, fmt.Sprintf("no adaptor for channelType %d", sel.ChannelType) + } + adaptor.Init(info) + + reader, err := adaptor.ConvertRequest(ctx, info, job.RequestBody) + if err != nil { + return false, nil, fmt.Sprintf("convert request: %v", err) + } + + // 上游长连接用后台 context(不随停机被斩断),阻塞 10–60s。 + resp, err := adaptor.DoRequest(context.WithoutCancel(ctx), info, reader) + if err != nil { + return false, nil, fmt.Sprintf("do request: %v", err) + } + + memW := newMemResponseWriter() + if _, err := adaptor.DoResponse(ctx, resp, info, memW); err != nil { + return false, memW, fmt.Sprintf("do response: %v", err) + } + if memW.StatusCode() != http.StatusOK { + return false, memW, fmt.Sprintf("upstream status %d: %s", memW.StatusCode(), truncateStr(string(memW.Bytes()), 300)) + } + return true, memW, "" +} + +// settleSyncImageSuccess 成功收尾:先 CAS 赢终态,再结算钱包,最后标记 billing_settled。 +// 崩溃窗口(终态已写、settled 未写)由 handleUnsettledTasks 的 sync_image 分支兜底。 +func settleSyncImageSuccess(ctx context.Context, job *SyncImageJob, sel *common.ChannelSelection, memW *memResponseWriter) { + resultURL, normalized, err := buildImageResult(ctx, job, memW.Bytes()) + if err != nil { + failSyncImageJob(ctx, job, sel, fmt.Sprintf("build result: %v", err)) + return + } + + now := time.Now() + // per_request 图片:实际费用 == 预扣(RecalculateByTokens 对 per_request 返回 0,无 token 重算)。 + actualCost := job.PreDeductAmount + + // 1. 赢得终态(billing_settled=false 先落) + if err := DefaultAsyncProvider.UpdateTaskCAS(ctx, &common.AsyncTask{ + ID: job.TaskID, + Status: "SUCCESS", + Progress: "100%", + ResultURL: resultURL, + Data: normalized, + FinishTime: &now, + }, "IN_PROGRESS"); err != nil { + // 已被超时网抢占,放弃结算(抢占方负责退款/DecrActiveTask) + g.Log().Warningf(ctx, "sync_image: task %s terminal CAS failed (preempted): %v", job.PublicTaskID, err) + return + } + + // 2. 结算钱包 + settleResult, serr := syncImageBilling.SettleTaskSuccess(ctx, job.TenantID, job.UserID, job.ApiKeyID, sel.ChannelID, + job.Model, job.RequestID, actualCost, job.PreDeductAmount, 0, 0, job.Ratios, job.PublicTaskID) + if serr != nil { + // 保留 billing_settled=false,由未结算兜底网重放结算 + g.Log().Warningf(ctx, "sync_image: task %s settle success failed (unsettled net will retry): %v", job.PublicTaskID, serr) + } else { + // 3. 标记已结算 + _ = DefaultAsyncProvider.UpdateTask(ctx, &common.AsyncTask{ + ID: job.TaskID, + Status: "SUCCESS", + Progress: "100%", + ResultURL: resultURL, + Data: normalized, + FinishTime: &now, + BillingSettled: true, + ActualCost: actualCost, + }) + syncImageBilling.IncrApiKeyQuotaUsed(ctx, job.ApiKeyID, actualCost) + } + + // 4. 收尾 + syncImageSucceeded.Add(1) + DecrActiveTask() + monitor.UnregisterRequestByTaskID(job.PublicTaskID) + syncImageRelayProv.UpdateChannelHealth(ctx, sel.ChannelID, true, 0) + + chBasic := &common.ChannelBasicInfo{ID: sel.ChannelID, Type: sel.ChannelType, Name: sel.ChannelName} + usageTask := buildUsageTask(job, sel, "SUCCESS", actualCost, now) + recordTaskUsage(usageTask, chBasic, true, "", settleResult) + // 闭环审计:把提交阶段写入的 SUBMITTED 审计记录更新为终态(与 pollSingleTask 一致), + // 否则请求审计日志里的任务状态会一直停留在「已提交」。 + recordTaskCompletionAudit(usageTask, "SUCCESS", string(normalized), nil) +} + +// failSyncImageJob 失败收尾:CAS 赢终态 → 退款 → 标记已结算 → 用量/计数。 +func failSyncImageJob(ctx context.Context, job *SyncImageJob, sel *common.ChannelSelection, reason string) { + now := time.Now() + reason = truncateStr(reason, 500) + + if err := DefaultAsyncProvider.UpdateTaskCAS(ctx, &common.AsyncTask{ + ID: job.TaskID, + Status: "FAILURE", + FailReason: reason, + FinishTime: &now, + }, "IN_PROGRESS"); err != nil { + g.Log().Warningf(ctx, "sync_image: task %s fail CAS failed (already finalized): %v", job.PublicTaskID, err) + return + } + + settled := true + if job.PreDeductAmount > 0 { + if err := syncImageBilling.SettleTaskFailed(ctx, job.TenantID, job.RequestID, job.PreDeductAmount); err != nil { + g.Log().Warningf(ctx, "sync_image: task %s refund failed (unsettled net will retry): %v", job.PublicTaskID, err) + settled = false + } + } + if settled { + _ = DefaultAsyncProvider.UpdateTask(ctx, &common.AsyncTask{ + ID: job.TaskID, + Status: "FAILURE", + FailReason: reason, + FinishTime: &now, + BillingSettled: true, + }) + } + + syncImageFailed.Add(1) + DecrActiveTask() + monitor.UnregisterRequestByTaskID(job.PublicTaskID) + + var chBasic *common.ChannelBasicInfo + if sel != nil { + chBasic = &common.ChannelBasicInfo{ID: sel.ChannelID, Type: sel.ChannelType, Name: sel.ChannelName} + } + usageTask := buildUsageTask(job, sel, "FAILURE", 0, now) + recordTaskUsage(usageTask, chBasic, false, reason, nil) + // 闭环审计:更新提交阶段的 SUBMITTED 审计记录为 FAILURE。 + recordTaskCompletionAudit(usageTask, "FAILURE", reason, nil) +} + +// buildImageResult 解析上游图片响应,返回结果 URL 与归一化后的响应体(供落库 Data)。 +// b64_json 无条件 re-host;url 按配置开关(默认透传,开启则下载 re-host)。 +func buildImageResult(ctx context.Context, job *SyncImageJob, body []byte) (resultURL string, normalized []byte, err error) { + var imgResp dto.ImageResponse + if e := json.Unmarshal(body, &imgResp); e != nil { + return "", nil, fmt.Errorf("parse image response: %w", e) + } + if len(imgResp.Data) == 0 { + return "", nil, fmt.Errorf("empty image data") + } + if len(imgResp.Data) > 1 { + g.Log().Warningf(ctx, "sync_image: task %s returned %d images; only the first is surfaced via ResultURL", job.PublicTaskID, len(imgResp.Data)) + } + + first := imgResp.Data[0] + switch { + case first.B64JSON != "": + data, e := base64.StdEncoding.DecodeString(first.B64JSON) + if e != nil { + return "", nil, fmt.Errorf("decode b64_json: %w", e) + } + resultURL, err = rehostImage(ctx, job, data, "image/png", ".png") + case first.URL != "": + if shouldRehostURL(ctx) { + resultURL, err = rehostFromURL(ctx, job, first.URL) + } else { + resultURL = first.URL + } + default: + return "", nil, fmt.Errorf("no url or b64_json in image response") + } + if err != nil { + return "", nil, err + } + + // 归一化落库:只存单张结果 URL,避免把大体积 b64 原文写进 data 列。 + normalized, _ = json.Marshal(dto.ImageResponse{ + Created: imgResp.Created, + Data: []dto.ImageData{{URL: resultURL, RevisedPrompt: first.RevisedPrompt}}, + }) + return resultURL, normalized, nil +} + +func rehostImage(ctx context.Context, job *SyncImageJob, data []byte, contentType, ext string) (string, error) { + if syncImageFileSvc == nil { + return "", fmt.Errorf("object storage not configured, cannot re-host image") + } + rec, err := syncImageFileSvc.Upload(ctx, &lcommon.FileUpload{ + Reader: bytes.NewReader(data), + Filename: job.PublicTaskID + ext, + ContentType: contentType, + Size: int64(len(data)), + TenantID: job.TenantID, + UserID: job.UserID, + }) + if err != nil { + return "", err + } + return syncImageFileSvc.GetDownloadURL(ctx, rec.ID) +} + +func rehostFromURL(ctx context.Context, job *SyncImageJob, url string) (string, error) { + if syncImageFileSvc == nil { + return "", fmt.Errorf("object storage not configured, cannot re-host image") + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("download image status %d", resp.StatusCode) + } + data, err := io.ReadAll(io.LimitReader(resp.Body, syncImageDownloadCap)) + if err != nil { + return "", err + } + contentType := resp.Header.Get("Content-Type") + if contentType == "" { + contentType = "image/png" + } + return rehostImage(ctx, job, data, contentType, extFromContentType(contentType)) +} + +func shouldRehostURL(ctx context.Context) bool { + return lcommon.Config().GetBool(ctx, "sync_image_rehost_url") +} + +// buildUsageTask 构造供 recordTaskUsage 使用的 AsyncTask(用量日志需要的字段)。 +func buildUsageTask(job *SyncImageJob, sel *common.ChannelSelection, status string, actualCost float64, finish time.Time) *common.AsyncTask { + submit := job.SubmitTime + t := &common.AsyncTask{ + ID: job.TaskID, + PublicTaskID: job.PublicTaskID, + RequestID: job.RequestID, + Platform: string(constant.TaskPlatformSyncImage), + Status: status, + TenantID: job.TenantID, + UserID: job.UserID, + ApiKeyID: job.ApiKeyID, + ModelName: job.Model, + PreDeductAmount: job.PreDeductAmount, + ActualCost: actualCost, + SubmitTime: &submit, + FinishTime: &finish, + } + if sel != nil { + t.ChannelID = sel.ChannelID + t.UpstreamModel = sel.UpstreamModelName + } + return t +} + +// tryOccupyChannel 层②:容量未满则占槽并返回 true。MaxConcurrency<=0 视为不限。 +func tryOccupyChannel(channelID int64, maxConcurrency int) bool { + channelInflightMu.Lock() + defer channelInflightMu.Unlock() + if maxConcurrency > 0 && channelInflight[channelID] >= maxConcurrency { + return false + } + channelInflight[channelID]++ + return true +} + +func decInflight(channelID int64) { + channelInflightMu.Lock() + defer channelInflightMu.Unlock() + if channelInflight[channelID] > 0 { + channelInflight[channelID]-- + } + if channelInflight[channelID] <= 0 { + delete(channelInflight, channelID) + } +} + +func syncImageJitter() { + time.Sleep(time.Duration(50+rand.Intn(150)) * time.Millisecond) +} + +func truncateStr(s string, max int) string { + if len(s) <= max { + return s + } + return s[:max] +} + +func extFromContentType(ct string) string { + switch { + case bytes.Contains([]byte(ct), []byte("jpeg")), bytes.Contains([]byte(ct), []byte("jpg")): + return ".jpg" + case bytes.Contains([]byte(ct), []byte("webp")): + return ".webp" + default: + return ".png" + } +} diff --git a/internal/logic/task/sync_image_worker_test.go b/internal/logic/task/sync_image_worker_test.go new file mode 100644 index 0000000..5b845f0 --- /dev/null +++ b/internal/logic/task/sync_image_worker_test.go @@ -0,0 +1,129 @@ +package task + +import ( + "context" + "encoding/base64" + "encoding/json" + "net/http" + "testing" +) + +func TestMemResponseWriter(t *testing.T) { + w := newMemResponseWriter() + if w.StatusCode() != http.StatusOK { + t.Fatalf("default status = %d, want 200", w.StatusCode()) + } + if _, err := w.Write([]byte("hello ")); err != nil { + t.Fatalf("write: %v", err) + } + if _, err := w.Write([]byte("world")); err != nil { + t.Fatalf("write: %v", err) + } + if got := string(w.Bytes()); got != "hello world" { + t.Fatalf("body = %q, want %q", got, "hello world") + } + w.WriteHeader(http.StatusBadGateway) + if w.StatusCode() != http.StatusBadGateway { + t.Fatalf("status after WriteHeader = %d, want 502", w.StatusCode()) + } + w.Header().Set("Content-Type", "application/json") + if w.Header().Get("Content-Type") != "application/json" { + t.Fatalf("header not stored") + } +} + +func TestTryOccupyChannel_Unlimited(t *testing.T) { + const ch = int64(9000001) + defer func() { channelInflight = map[int64]int{} }() + + // MaxConcurrency <= 0 视为不限:多次占用均成功。 + for i := 0; i < 5; i++ { + if !tryOccupyChannel(ch, 0) { + t.Fatalf("occupy #%d with maxConc=0 should succeed", i) + } + } + if !tryOccupyChannel(ch, -1) { + t.Fatalf("occupy with maxConc=-1 should succeed") + } +} + +func TestTryOccupyChannel_Saturation(t *testing.T) { + const ch = int64(9000002) + defer func() { channelInflight = map[int64]int{} }() + + if !tryOccupyChannel(ch, 2) { + t.Fatal("occupy #1 should succeed") + } + if !tryOccupyChannel(ch, 2) { + t.Fatal("occupy #2 should succeed") + } + if tryOccupyChannel(ch, 2) { + t.Fatal("occupy #3 should fail (saturated)") + } + // 释放一个槽后应能再次占用 + decInflight(ch) + if !tryOccupyChannel(ch, 2) { + t.Fatal("occupy after release should succeed") + } +} + +func TestDecInflight_CleansUpAndFloorsAtZero(t *testing.T) { + const ch = int64(9000003) + defer func() { channelInflight = map[int64]int{} }() + + tryOccupyChannel(ch, 5) + decInflight(ch) + if _, ok := channelInflight[ch]; ok { + t.Fatal("counter reaching 0 should be deleted from map") + } + // 多减不应变负 / panic + decInflight(ch) + if channelInflight[ch] != 0 { + t.Fatalf("counter = %d, want 0", channelInflight[ch]) + } +} + +func TestBuildImageResult_EmptyData(t *testing.T) { + job := &SyncImageJob{PublicTaskID: "task_x"} + body, _ := json.Marshal(map[string]any{"created": 1, "data": []any{}}) + if _, _, err := buildImageResult(context.Background(), job, body); err == nil { + t.Fatal("empty data should error") + } +} + +func TestBuildImageResult_B64WithoutStorageErrors(t *testing.T) { + // syncImageFileSvc 默认 nil(未启动 worker),b64_json 无法 re-host → 报错。 + syncImageFileSvc = nil + job := &SyncImageJob{PublicTaskID: "task_x", TenantID: 1, UserID: 2} + raw, _ := json.Marshal(map[string]any{ + "created": 1, + "data": []any{map[string]any{"b64_json": base64.StdEncoding.EncodeToString([]byte("PNGDATA"))}}, + }) + if _, _, err := buildImageResult(context.Background(), job, raw); err == nil { + t.Fatal("b64 without storage should error") + } +} + +func TestExtFromContentType(t *testing.T) { + cases := map[string]string{ + "image/png": ".png", + "image/jpeg": ".jpg", + "image/webp": ".webp", + "": ".png", + "text/plain": ".png", + } + for ct, want := range cases { + if got := extFromContentType(ct); got != want { + t.Errorf("extFromContentType(%q) = %q, want %q", ct, got, want) + } + } +} + +func TestTruncateStr(t *testing.T) { + if got := truncateStr("abcdef", 3); got != "abc" { + t.Fatalf("truncate = %q, want abc", got) + } + if got := truncateStr("ab", 5); got != "ab" { + t.Fatalf("truncate = %q, want ab", got) + } +} diff --git a/internal/logic/tenant/audit_config.go b/internal/logic/tenant/audit_config.go index 38e0d77..6632a74 100644 --- a/internal/logic/tenant/audit_config.go +++ b/internal/logic/tenant/audit_config.go @@ -106,12 +106,12 @@ func (s *sTenant) AuditLogs(ctx context.Context, req *v1.TenantAuditLogsReq) (*v // 使用原生 SQL 查询,绕过 GoFrame ScanAndCount 对 map[string]any 的 bug dataSQL := `SELECT id, tenant_id, user_id, user_type, action, resource_type, resource_id, detail, changes_json, ip_address, created_at FROM aud_operation_logs WHERE tenant_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?` - result, err := common.GetAuditDB().Query(ctx, dataSQL, tenantID, pageSize, (page-1)*pageSize) + result, err := g.DB().Query(ctx, dataSQL, tenantID, pageSize, (page-1)*pageSize) if err != nil { return nil, err } - countResult, err := common.GetAuditDB().Query(ctx, "SELECT COUNT(*) AS total FROM aud_operation_logs WHERE tenant_id = ?", tenantID) + countResult, err := g.DB().Query(ctx, "SELECT COUNT(*) AS total FROM aud_operation_logs WHERE tenant_id = ?", tenantID) if err != nil { return nil, err } diff --git a/internal/logic/tenant/model.go b/internal/logic/tenant/model.go index 4aa9a8f..5e65dd9 100644 --- a/internal/logic/tenant/model.go +++ b/internal/logic/tenant/model.go @@ -362,6 +362,27 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab } } + // 标记图片模型是否必须走异步端点(DashScope 等异步图片 provider)。 + // 判定与同步端点拦截 gate 同源(constant.IsAsyncImageProvider),保证在线体验示例 + // 与后端实际行为一致。仅对图片分类模型跑一次批量查询。 + imageModelIDs := make([]string, 0) + for _, item := range list { + if item.Category == "image" { + imageModelIDs = append(imageModelIDs, item.ModelId) + } + } + if len(imageModelIDs) > 0 { + asyncSet, err := lcommon.GetAsyncImageModelSet(ctx, imageModelIDs) + if err != nil { + return nil, err + } + for i := range list { + if list[i].Category == "image" && asyncSet[list[i].ModelId] { + list[i].AsyncImage = true + } + } + } + return &v1.TenantAvailableModelsRes{List: list}, nil } diff --git a/internal/middleware/rbac.go b/internal/middleware/rbac.go index 528c09a..4545c48 100644 --- a/internal/middleware/rbac.go +++ b/internal/middleware/rbac.go @@ -197,6 +197,13 @@ var adminPermissionRules = []permissionRule{ {method: "GET", path: "/api/admin/audit/sensitive-logs", perm: "audit:read_sensitive"}, {method: "GET", path: "/api/admin/audit/content-filter-logs", perm: "audit:view"}, + // ── file 文件管理 ── + {method: "GET", path: "/api/admin/files", perm: "file:view"}, + {method: "GET", path: "/api/admin/files/stats", perm: "file:view"}, + {method: "POST", path: "/api/admin/files/cleanup", perm: "file:cleanup"}, + {method: "GET", prefix: "/api/admin/files/", suffix: "/download", perm: "file:view"}, + {method: "DELETE", prefix: "/api/admin/files/", perm: "file:delete"}, + // ── monitor 监控告警 ── {method: "GET", prefix: "/api/admin/monitor/", perm: "monitor:view"}, {method: "GET", path: "/api/admin/alert/rules", perm: "monitor:view"}, diff --git a/internal/service/admin.go b/internal/service/admin.go index ef55be8..d60e2b2 100644 --- a/internal/service/admin.go +++ b/internal/service/admin.go @@ -203,6 +203,16 @@ type ( UpdateFeedbackStatus(ctx context.Context, req *v1.FeedbackUpdateStatusReq) (*v1.FeedbackUpdateStatusRes, error) // GetFeedbackStats 反馈统计 GetFeedbackStats(ctx context.Context, req *v1.FeedbackStatsReq) (*v1.FeedbackStatsRes, error) + // FileList 文件列表(分页 + 筛选)。 + FileList(ctx context.Context, req *v1.FileListReq) (*v1.FileListRes, error) + // FileStats 存储占用统计(KPI)。 + FileStats(ctx context.Context, req *v1.FileStatsReq) (*v1.FileStatsRes, error) + // FileDownload 生成临时预览下载链接。 + FileDownload(ctx context.Context, req *v1.FileDownloadReq) (*v1.FileDownloadRes, error) + // FileDelete 删除单个文件(存储对象 + 记录)。 + FileDelete(ctx context.Context, req *v1.FileDeleteReq) (*v1.FileDeleteRes, error) + // FileCleanup 手动触发一次保留期清理(导出 + 图片)。 + FileCleanup(ctx context.Context, req *v1.FileCleanupReq) (*v1.FileCleanupRes, error) // CreateHelpCategory 创建帮助分类 CreateHelpCategory(ctx context.Context, req *v1.HelpCategoryCreateReq) (*v1.HelpCategoryCreateRes, error) // UpdateHelpCategory 更新帮助分类 diff --git a/internal/service/tenant.go b/internal/service/tenant.go index 6f1f6c2..f97aaf4 100644 --- a/internal/service/tenant.go +++ b/internal/service/tenant.go @@ -226,6 +226,7 @@ type ( // GetOrgInfo returns tenant organization info. GetOrgInfo(ctx context.Context, req *v1.TenantOrgInfoReq) (*v1.TenantOrgInfoRes, error) // UpdateOrgInfo updates tenant organization info. + // 组织代码仅可在未启用团队功能时设置一次(设置即激活),启用后不可修改。 UpdateOrgInfo(ctx context.Context, req *v1.TenantOrgUpdateReq) (*v1.TenantOrgUpdateRes, error) // TransferOwnership transfers tenant ownership to another member. TransferOwnership(ctx context.Context, req *v1.TenantOrgTransferReq) (*v1.TenantOrgTransferRes, error) diff --git a/relay/channel/ali/adaptor.go b/relay/channel/ali/adaptor.go index dec6f38..9cb82bb 100644 --- a/relay/channel/ali/adaptor.go +++ b/relay/channel/ali/adaptor.go @@ -40,6 +40,11 @@ func (a *Adaptor) GetRequestURL(info *common.RelayInfo) (string, error) { case constant.RelayModeEmbeddings: return baseURL + "/compatible-mode/v1/embeddings", nil case constant.RelayModeImagesGenerations: + // qwen-image-2.x 等同步 multimodal 模型走 multimodal-generation 端点; + // 其余(wanx*、qwen-image 等异步族)用 image-synthesis(异步族一般已被 gate 引导到异步端点)。 + if isMultimodalImageMode(info) { + return baseURL + "/api/v1/services/aigc/multimodal-generation/generation", nil + } return baseURL + "/api/v1/services/aigc/text2image/image-synthesis", nil default: return "", fmt.Errorf("unsupported relay mode: %d", info.RelayMode) @@ -68,6 +73,11 @@ func (a *Adaptor) ConvertRequest(ctx context.Context, info *common.RelayInfo, re return convertClaudeRequest(requestBody, info) } + // 同步 multimodal 图片(qwen-image-2.x):OpenAI Images 请求体 → DashScope messages 格式 + if isMultimodalImageMode(info) { + return convertMultimodalImageRequest(requestBody, info) + } + // 非 OpenAI 格式先转换为 OpenAI if info.InboundFormat != "" && info.InboundFormat != constant.RelayFormatOpenAI { c, err := openai.ConvertToOpenAI(requestBody, info) @@ -129,6 +139,11 @@ func (a *Adaptor) DoRequest(ctx context.Context, info *common.RelayInfo, request // DoResponse 处理上游响应。 // Claude 入站委托 claude.Adaptor 原生直通;其他格式委托 openai.Adaptor。 func (a *Adaptor) DoResponse(ctx context.Context, resp *http.Response, info *common.RelayInfo, writer http.ResponseWriter) (*common.Usage, error) { + // 同步 multimodal 图片(qwen-image-2.x):DashScope multimodal 响应 → OpenAI Images 响应 + if isMultimodalImageMode(info) { + return handleMultimodalImageResponse(resp, writer) + } + if info.GetOriginalClientFormat() == constant.RelayFormatClaude { delegate := &claude.Adaptor{} delegate.Init(info) diff --git a/relay/channel/ali/image_multimodal.go b/relay/channel/ali/image_multimodal.go new file mode 100644 index 0000000..1d33513 --- /dev/null +++ b/relay/channel/ali/image_multimodal.go @@ -0,0 +1,169 @@ +package ali + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/qianfree/team-api/relay/common" + "github.com/qianfree/team-api/relay/constant" +) + +// 阿里云 DashScope「同步 multimodal」图片生成(qwen-image-2.x 系列)。 +// +// 与异步 wanx / qwen-image image-synthesis 端点不同,qwen-image-2.0 仅支持同步调用,走 +// /api/v1/services/aigc/multimodal-generation/generation,请求体为 messages 格式、响应从 +// output.choices[].message.content[].image 取图片 URL。这里做 OpenAI Images ↔ DashScope +// multimodal 的双向转换,使其能经同步 /v1/images/generations 一次性返回。 + +// isMultimodalImageMode 判断当前请求是否为阿里云同步 multimodal 图片生成。 +func isMultimodalImageMode(info *common.RelayInfo) bool { + return constant.RelayMode(info.RelayMode) == constant.RelayModeImagesGenerations && + constant.IsAliSyncMultimodalImageModel(info.ChannelMeta.UpstreamModelName) +} + +// ==================== 请求/响应结构体 ==================== + +type multimodalImageContent struct { + Text string `json:"text,omitempty"` + Image string `json:"image,omitempty"` +} + +type multimodalImageMessage struct { + Role string `json:"role"` + Content []multimodalImageContent `json:"content"` +} + +type multimodalImageInput struct { + Messages []multimodalImageMessage `json:"messages"` +} + +type multimodalImageRequest struct { + Model string `json:"model"` + Input multimodalImageInput `json:"input"` + Parameters map[string]any `json:"parameters,omitempty"` +} + +type multimodalImageResponse struct { + Output struct { + Choices []struct { + Message struct { + Content []multimodalImageContent `json:"content"` + } `json:"message"` + } `json:"choices"` + } `json:"output"` + Code string `json:"code"` + Message string `json:"message"` + RequestID string `json:"request_id"` +} + +// ==================== 转换实现 ==================== + +// convertMultimodalImageRequest 将 OpenAI Images 请求转换为 DashScope multimodal 请求体。 +func convertMultimodalImageRequest(requestBody []byte, info *common.RelayInfo) (io.Reader, error) { + var req map[string]any + if err := json.Unmarshal(requestBody, &req); err != nil { + return nil, fmt.Errorf("parse image request: %w", err) + } + + prompt, _ := req["prompt"].(string) + mm := multimodalImageRequest{ + Model: info.ChannelMeta.UpstreamModelName, + Input: multimodalImageInput{ + Messages: []multimodalImageMessage{ + {Role: "user", Content: []multimodalImageContent{{Text: prompt}}}, + }, + }, + Parameters: map[string]any{}, + } + + // size: 1024x1024 → 1024*1024 + if v, ok := req["size"].(string); ok && v != "" { + mm.Parameters["size"] = strings.ReplaceAll(v, "x", "*") + } + if v, ok := req["n"]; ok { + mm.Parameters["n"] = v + } + if v, ok := req["negative_prompt"].(string); ok && v != "" { + mm.Parameters["negative_prompt"] = v + } + if v, ok := req["seed"]; ok { + mm.Parameters["seed"] = v + } + if v, ok := req["prompt_extend"]; ok { + mm.Parameters["prompt_extend"] = v + } + if v, ok := req["watermark"]; ok { + mm.Parameters["watermark"] = v + } + if len(mm.Parameters) == 0 { + mm.Parameters = nil + } + + data, err := json.Marshal(mm) + if err != nil { + return nil, fmt.Errorf("marshal multimodal image request: %w", err) + } + return bytes.NewReader(data), nil +} + +// handleMultimodalImageResponse 将 DashScope multimodal 响应转换为 OpenAI Images 响应写回客户端。 +func handleMultimodalImageResponse(resp *http.Response, writer http.ResponseWriter) (*common.Usage, error) { + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, constant.NewUpstreamError(resp.StatusCode, "read response body failed", err) + } + + if resp.StatusCode != http.StatusOK { + var errResp multimodalImageResponse + msg := string(body) + if json.Unmarshal(body, &errResp) == nil && errResp.Message != "" { + msg = errResp.Message + } + return nil, constant.NewUpstreamError(resp.StatusCode, msg, nil) + } + + var mmResp multimodalImageResponse + if err := json.Unmarshal(body, &mmResp); err != nil { + return nil, constant.NewUpstreamError(resp.StatusCode, "parse response failed", err) + } + // 200 但业务错误(顶层 code/message) + if mmResp.Code != "" { + return nil, constant.NewUpstreamError(resp.StatusCode, mmResp.Message, nil) + } + + // 收集所有 choices 里的图片 URL + data := make([]map[string]any, 0) + for _, ch := range mmResp.Output.Choices { + for _, c := range ch.Message.Content { + if c.Image != "" { + data = append(data, map[string]any{"url": c.Image}) + } + } + } + if len(data) == 0 { + return nil, constant.NewUpstreamError(resp.StatusCode, "upstream returned no image", nil) + } + + out := map[string]any{ + "created": time.Now().Unix(), + "data": data, + } + outBody, err := json.Marshal(out) + if err != nil { + return nil, constant.NewUpstreamError(http.StatusInternalServerError, "marshal image response failed", err) + } + + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + _, _ = writer.Write(outBody) + + // 图片按次/张计费,token 用量为空(与 OpenAI dall-e 处理一致) + return &common.Usage{}, nil +} diff --git a/relay/channel/gemini/adaptor.go b/relay/channel/gemini/adaptor.go index 8b02f1f..9b99f33 100644 --- a/relay/channel/gemini/adaptor.go +++ b/relay/channel/gemini/adaptor.go @@ -351,13 +351,16 @@ func (a *Adaptor) handleCodeAssistAggregatedStream(ctx context.Context, resp *ht body = unwrapCodeAssistData(body) clientFormat := info.GetOriginalClientFormat() if clientFormat == constant.RelayFormatGemini { + // Gemini 原生格式:保留完整上游错误响应,标记以防上层二次写入 writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(resp.StatusCode) _, _ = writer.Write(body) - } else { - writeGeminiErrorAsOpenAI(writer, body, resp.StatusCode) + err := constant.NewUpstreamError(resp.StatusCode, string(body), nil) + err.ResponseWritten = true + return nil, err } - return nil, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + // OpenAI 等格式:不写响应,交上层错误写入器统一写入 + return nil, buildGeminiUpstreamError(body, resp.StatusCode) } // 读取所有 SSE chunks 并聚合 diff --git a/relay/channel/gemini/response.go b/relay/channel/gemini/response.go index 18c41de..43ad3bf 100644 --- a/relay/channel/gemini/response.go +++ b/relay/channel/gemini/response.go @@ -50,10 +50,13 @@ func (a *Adaptor) handleGeminiNativeNonStream(ctx context.Context, resp *http.Re } if resp.StatusCode != http.StatusOK { + // Gemini 原生格式透传:已写入完整上游错误响应,标记以防上层二次写入 writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(resp.StatusCode) _, _ = writer.Write(body) - return nil, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + err := constant.NewUpstreamError(resp.StatusCode, string(body), nil) + err.ResponseWritten = true + return nil, err } writer.Header().Set("Content-Type", "application/json") @@ -73,10 +76,13 @@ func (a *Adaptor) handleGeminiNativeStream(ctx context.Context, resp *http.Respo if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) + // Gemini 原生格式透传:已写入完整上游错误响应,标记以防上层二次写入 writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(resp.StatusCode) _, _ = writer.Write(body) - return nil, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + err := constant.NewUpstreamError(resp.StatusCode, string(body), nil) + err.ResponseWritten = true + return nil, err } helper.SetEventStreamHeaders(writer) @@ -163,8 +169,8 @@ func (a *Adaptor) handleNonStreamToOpenAI(ctx context.Context, resp *http.Respon } if resp.StatusCode != http.StatusOK { - writeGeminiErrorAsOpenAI(writer, body, resp.StatusCode) - return nil, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + // 不写响应:交上层 WriteRelayError 统一写入,避免双重写入与重试时的响应污染 + return nil, buildGeminiUpstreamError(body, resp.StatusCode) } var geminiResp dto.GeminiChatResponse @@ -199,8 +205,8 @@ func (a *Adaptor) handleStreamToOpenAI(ctx context.Context, resp *http.Response, if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) - writeGeminiErrorAsOpenAI(writer, body, resp.StatusCode) - return nil, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + // 不写响应:交上层 WriteRelayError 统一写入,避免双重写入与重试时的响应污染 + return nil, buildGeminiUpstreamError(body, resp.StatusCode) } helper.SetEventStreamHeaders(writer) @@ -683,8 +689,10 @@ func geminiStatusToOpenAIType(status string) string { } } -// writeGeminiErrorAsOpenAI 将 Gemini 上游错误转换为 OpenAI 格式写入响应 -func writeGeminiErrorAsOpenAI(writer http.ResponseWriter, body []byte, defaultStatusCode int) { +// buildGeminiUpstreamError 解析 Gemini 上游错误,构造携带正确 type 的 RelayError。 +// 用于 OpenAI 出站路径:adaptor 不直接写响应,交上层错误写入器(WriteRelayError)统一写入一次, +// 既消除双重写入,又避免非流式可重试错误提前 WriteHeader 造成的重试响应污染。 +func buildGeminiUpstreamError(body []byte, defaultStatusCode int) *constant.RelayError { code, status, message := parseGeminiError(body) if code == 0 { code = defaultStatusCode @@ -692,16 +700,9 @@ func writeGeminiErrorAsOpenAI(writer http.ResponseWriter, body []byte, defaultSt if code < 100 || code > 599 { code = 500 } - - openaiErr, _ := json.Marshal(map[string]any{ - "error": map[string]any{ - "message": message, - "type": geminiStatusToOpenAIType(status), - "param": nil, - "code": nil, - }, - }) - writer.Header().Set("Content-Type", "application/json") - writer.WriteHeader(code) - _, _ = writer.Write(openaiErr) + return &constant.RelayError{ + StatusCode: code, + Message: message, + Type: geminiStatusToOpenAIType(status), + } } diff --git a/relay/channel/gemini/response_test.go b/relay/channel/gemini/response_test.go new file mode 100644 index 0000000..04e4679 --- /dev/null +++ b/relay/channel/gemini/response_test.go @@ -0,0 +1,41 @@ +package gemini + +import ( + "strings" + "testing" +) + +// TestBuildGeminiUpstreamError_ResourceExhausted 验证真实 Gemini 429(区域配额耗尽) +// 被解析为携带正确 type 的 RelayError,且 message 为简短文案而非整个 body。 +// 回归保护:OpenAI 出站路径上游报错时,由上层写入器写出单条 rate_limit_error, +// 不再出现 adaptor 与上层各写一次的「双重写入」。 +func TestBuildGeminiUpstreamError_ResourceExhausted(t *testing.T) { + body := []byte(`{ + "error": { + "code": 429, + "message": "Quota exceeded for quota metric 'API requests' and limit 'Request limit per minute for a region' of service 'generativelanguage.googleapis.com' for consumer 'project_number:121235835710'.", + "status": "RESOURCE_EXHAUSTED" + } + }`) + + err := buildGeminiUpstreamError(body, 200) + + if err.StatusCode != 429 { + t.Errorf("StatusCode = %d, want 429", err.StatusCode) + } + if err.Type != "rate_limit_error" { + t.Errorf("Type = %q, want rate_limit_error", err.Type) + } + if !strings.HasPrefix(err.Message, "Quota exceeded") { + t.Errorf("Message = %q, want short upstream message (not the whole body)", err.Message) + } +} + +// TestBuildGeminiUpstreamError_FallbackStatusCode 验证 body 无法解析出 code 时回退到默认状态码 +func TestBuildGeminiUpstreamError_FallbackStatusCode(t *testing.T) { + // 非 JSON body:parseGeminiError 返回 code=0,应回退到 defaultStatusCode + err := buildGeminiUpstreamError([]byte("plain text error"), 503) + if err.StatusCode != 503 { + t.Errorf("StatusCode = %d, want 503 (fallback)", err.StatusCode) + } +} diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index 22e201b..75afb9a 100644 --- a/relay/channel/openai/adaptor.go +++ b/relay/channel/openai/adaptor.go @@ -304,7 +304,9 @@ func (a *Adaptor) handleChatNonStreamResponse(ctx context.Context, resp *http.Re // 尝试解析上游错误格式,如果是标准格式则透传原始响应 if isUpstreamOpenAIError(body) { writeUpstreamErrorResponse(writer, resp.StatusCode, body) - return &common.Usage{}, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr := constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr.ResponseWritten = true + return &common.Usage{}, upstreamErr } return nil, constant.NewUpstreamError(resp.StatusCode, string(body), nil) } @@ -341,7 +343,9 @@ func (a *Adaptor) handleChatStreamResponse(ctx context.Context, resp *http.Respo // 尝试透传上游错误格式 if isUpstreamOpenAIError(body) { writeUpstreamErrorResponse(writer, resp.StatusCode, body) - return &common.Usage{}, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr := constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr.ResponseWritten = true + return &common.Usage{}, upstreamErr } return nil, constant.NewUpstreamError(resp.StatusCode, string(body), nil) } @@ -465,7 +469,9 @@ func (a *Adaptor) handleModerationResponse(_ context.Context, resp *http.Respons } if resp.StatusCode != http.StatusOK { writeUpstreamErrorResponse(writer, resp.StatusCode, body) - return &common.Usage{}, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr := constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr.ResponseWritten = true + return &common.Usage{}, upstreamErr } writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) diff --git a/relay/channel/openai/audio.go b/relay/channel/openai/audio.go index 2642643..728a1b4 100644 --- a/relay/channel/openai/audio.go +++ b/relay/channel/openai/audio.go @@ -21,7 +21,9 @@ func handleAudioSpeechResponse(ctx context.Context, resp *http.Response, info *c body, _ := io.ReadAll(resp.Body) if isUpstreamOpenAIError(body) { writeUpstreamErrorResponse(writer, resp.StatusCode, body) - return &common.Usage{}, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr := constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr.ResponseWritten = true + return &common.Usage{}, upstreamErr } return nil, constant.NewUpstreamError(resp.StatusCode, string(body), nil) } @@ -67,7 +69,9 @@ func handleAudioTranscriptionResponse(ctx context.Context, resp *http.Response, if resp.StatusCode != http.StatusOK { if isUpstreamOpenAIError(body) { writeUpstreamErrorResponse(writer, resp.StatusCode, body) - return &common.Usage{}, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr := constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr.ResponseWritten = true + return &common.Usage{}, upstreamErr } return nil, constant.NewUpstreamError(resp.StatusCode, string(body), nil) } diff --git a/relay/channel/openai/claude_response.go b/relay/channel/openai/claude_response.go index f60586c..56a0038 100644 --- a/relay/channel/openai/claude_response.go +++ b/relay/channel/openai/claude_response.go @@ -34,7 +34,9 @@ func handleClaudeInboundNonStream(ctx context.Context, resp *http.Response, info "error": map[string]any{"type": "api_error", "message": string(body)}, }) _, _ = writer.Write(claudeErr) - return &common.Usage{}, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr := constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr.ResponseWritten = true + return &common.Usage{}, upstreamErr } var openaiResp dto.ChatCompletionResponse @@ -74,7 +76,9 @@ func handleClaudeInboundStream(ctx context.Context, resp *http.Response, info *c "error": map[string]any{"type": "api_error", "message": string(body)}, }) _, _ = writer.Write(claudeErr) - return &common.Usage{}, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr := constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr.ResponseWritten = true + return &common.Usage{}, upstreamErr } helper.SetEventStreamHeaders(writer) diff --git a/relay/channel/openai/completion.go b/relay/channel/openai/completion.go index ab05b77..deff81c 100644 --- a/relay/channel/openai/completion.go +++ b/relay/channel/openai/completion.go @@ -24,7 +24,9 @@ func (a *Adaptor) handleCompletionNonStreamResponse(ctx context.Context, resp *h if resp.StatusCode != http.StatusOK { if isUpstreamOpenAIError(body) { writeUpstreamErrorResponse(writer, resp.StatusCode, body) - return &common.Usage{}, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr := constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr.ResponseWritten = true + return &common.Usage{}, upstreamErr } return nil, constant.NewUpstreamError(resp.StatusCode, string(body), nil) } diff --git a/relay/channel/openai/gemini_response.go b/relay/channel/openai/gemini_response.go index 1bb7318..3f74ffe 100644 --- a/relay/channel/openai/gemini_response.go +++ b/relay/channel/openai/gemini_response.go @@ -27,7 +27,9 @@ func handleGeminiInboundNonStream(ctx context.Context, resp *http.Response, info if resp.StatusCode != http.StatusOK { writeOpenAIErrorAsGemini(writer, body, resp.StatusCode) - return &common.Usage{}, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr := constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr.ResponseWritten = true + return &common.Usage{}, upstreamErr } var openaiResp dto.ChatCompletionResponse @@ -59,7 +61,9 @@ func handleGeminiInboundStream(ctx context.Context, resp *http.Response, info *c body, _ := io.ReadAll(resp.Body) resp.Body.Close() writeOpenAIErrorAsGemini(writer, body, resp.StatusCode) - return &common.Usage{}, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr := constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr.ResponseWritten = true + return &common.Usage{}, upstreamErr } helper.SetEventStreamHeaders(writer) diff --git a/relay/channel/openai/rerank.go b/relay/channel/openai/rerank.go index 8beecdb..8cc64cc 100644 --- a/relay/channel/openai/rerank.go +++ b/relay/channel/openai/rerank.go @@ -26,7 +26,9 @@ func handleRerankResponse(ctx context.Context, resp *http.Response, info *common if resp.StatusCode != http.StatusOK { if isUpstreamOpenAIError(body) { writeUpstreamErrorResponse(writer, resp.StatusCode, body) - return &common.Usage{}, constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr := constant.NewUpstreamError(resp.StatusCode, string(body), nil) + upstreamErr.ResponseWritten = true + return &common.Usage{}, upstreamErr } return nil, constant.NewUpstreamError(resp.StatusCode, string(body), nil) } diff --git a/relay/common/provider.go b/relay/common/provider.go index 0355bbc..f7e2075 100644 --- a/relay/common/provider.go +++ b/relay/common/provider.go @@ -120,6 +120,7 @@ type ChannelSelection struct { ApiKey string // 解密后的上游 API Key UpstreamModelName string IsModelMapped bool + MaxConcurrency int // 该渠道最大并发(0/负值表示不限),供 sync_image worker 做 per-channel 容量控制 Settings ChannelSettings } diff --git a/relay/constant/errors.go b/relay/constant/errors.go index 3efdbed..a868f8f 100644 --- a/relay/constant/errors.go +++ b/relay/constant/errors.go @@ -1,7 +1,11 @@ package constant import ( + "context" "errors" + "io" + "net" + "syscall" "github.com/gogf/gf/v2/errors/gerror" ) @@ -15,6 +19,10 @@ type RelayError struct { Message string Type string // upstream_error / channel_error / auth_error / request_error Cause error + // ResponseWritten 表示 adaptor 已直接向客户端写入响应体, + // 上层错误写入器(WriteRelayError / WriteClaudeRelayError / WriteGeminiRelayError)应跳过二次写入。 + // 字段对重试 / 健康度 / 计费 / 状态码重映射等逻辑透明(仍按 StatusCode 判断)。 + ResponseWritten bool } func (e *RelayError) Error() string { @@ -109,5 +117,45 @@ func IsRetryable(err error) bool { return false } } + // 非 RelayError:检查是否为连接层 / 网络层瞬时错误(请求未获得任何 HTTP 响应) + return isTransientNetworkError(err) +} + +// isTransientNetworkError 判断是否为可重试的连接层 / 网络层瞬时错误。 +// 这类错误发生时请求未获得任何 HTTP 响应(连接被对端提前关闭、重置、超时或拒绝), +// 重试通常能命中健康渠道或新建连接成功,是中转网关最应重试 / 故障转移的场景。 +// 适配器(如 Gemini DoRequest)把 client.Do 的这类错误包成普通 error(非 *RelayError), +// 因此需要在这里显式识别,否则会被 IsRetryable 判为不可重试而直接失败。 +func isTransientNetworkError(err error) bool { + if err == nil { + return false + } + + // 客户端主动断开(context 已取消):响应无处可回,重试无意义且浪费上游调用 + if errors.Is(err, context.Canceled) { + return false + } + + // 连接被对端提前关闭(GFW 干扰 / 上游瞬断的典型表现) + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { + return true + } + + // 连接被重置 / 拒绝 + if errors.Is(err, syscall.ECONNRESET) || errors.Is(err, syscall.ECONNREFUSED) { + return true + } + + // 请求 / 握手超时(i/o timeout、TLS handshake timeout、Client.Timeout 等) + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + return true + } + + // http.Client 超时也会以 context.DeadlineExceeded 形式冒泡 + if errors.Is(err, context.DeadlineExceeded) { + return true + } + return false } diff --git a/relay/constant/task.go b/relay/constant/task.go index 151c087..ebea0cf 100644 --- a/relay/constant/task.go +++ b/relay/constant/task.go @@ -1,5 +1,7 @@ package constant +import "strings" + // TaskPlatform 异步任务平台类型 type TaskPlatform string @@ -11,6 +13,11 @@ const ( TaskPlatformVolcengine TaskPlatform = "volcengine" TaskPlatformAli TaskPlatform = "ali" TaskPlatformGemini TaskPlatform = "gemini" + + // TaskPlatformSyncImage 同步图片厂商异步化专用平台标记。 + // 注意:故意不加入 ProviderTypeToTaskPlatform —— 它由 sync_image worker 池执行, + // 无上游任务 ID 可轮询,polling 轮询分支需跳过它。 + TaskPlatformSyncImage TaskPlatform = "sync_image" ) // TaskAction 异步任务动作类型 @@ -92,6 +99,48 @@ func ProviderTypeToTaskPlatform(p ProviderType) (TaskPlatform, bool) { } } +// IsAsyncImageModel 判断某 provider + 模型的图片生成(RelayModeImagesGenerations)是否为 +// 异步任务式(提交拿 task_id → 轮询取图)。此类模型的同步 /v1/images/generations 端点无法 +// 一次性返回,必须走 /v1/images/generations/async 提交 + 轮询。 +// +// 这是「图片必须走异步」的唯一判定源:同步端点拦截 gate(relay_handler)与租户模型列表的 +// async_image 标记都调用它,保证在线体验示例与后端实际行为一致。 +// +// 目前仅阿里云 DashScope 图片模型分两族: +// - 异步族(wanx*、qwen-image、qwen-image-plus、flux* 等):text2image/image-synthesis + 轮询 +// - 同步 multimodal 族(qwen-image-2.x 系列):multimodal-generation/generation,一次性同步返回 +// +// 故对 Ali 默认异步,qwen-image-2.x 系列除外。其他 provider 图片均为同步(含 Gemini Imagen)。 +func IsAsyncImageModel(p ProviderType, model string) bool { + if p != ProviderAli { + return false + } + return !IsAliSyncMultimodalImageModel(model) +} + +// IsAliSyncMultimodalImageModel 判断是否为阿里云 DashScope「multimodal messages」协议图片模型。 +// 这类模型走同步 /api/v1/services/aigc/multimodal-generation/generation 端点,请求体为 +// input.messages 格式、响应从 output.choices[].message.content[].image 取图;不走旧版 +// text2image/image-synthesis(input.prompt)端点。本项目对它们统一用同步 multimodal 处理 +// (同步端点直出 / 异步端点交 sync_image worker 池同步执行)。目前包含: +// - qwen-image-2.x 系列(qwen-image-2.0 / qwen-image-2.0-pro 等) +// - z-image 系列(z-image-turbo 等) +// - 万相新版协议图片模型:wan2.6-t2i、wan2.7-image / wan2.7-image-pro +// +// 注意:万相旧版图片模型(wan2.5-t2i-preview、wan2.2-t2i-*、wanx2.x-t2i-*)走旧版 +// image-synthesis(input.prompt)异步协议,不在此列,由 taskchannel 的 buildImageRequest 处理。 +// +// 该判定是「Ali multimodal 图片」的唯一真相源——同步端点拦截 gate、canPassThrough、 +// isMultimodalImageMode、异步端点 worker 池路由、租户模型列表 async_image 标记均调用它, +// 改这一处即全链路生效。 +func IsAliSyncMultimodalImageModel(model string) bool { + m := strings.ToLower(model) + return strings.HasPrefix(m, "qwen-image-2") || + strings.HasPrefix(m, "z-image") || + strings.HasPrefix(m, "wan2.6-t2i") || + strings.HasPrefix(m, "wan2.7-image") +} + // RelayModeToTaskAction 将 RelayMode 映射到异步任务动作 func RelayModeToTaskAction(mode RelayMode) (TaskAction, bool) { switch mode { diff --git a/relay/handler/chat_handler.go b/relay/handler/chat_handler.go index 25288a5..a842399 100644 --- a/relay/handler/chat_handler.go +++ b/relay/handler/chat_handler.go @@ -145,6 +145,12 @@ func WriteRelayError(w http.ResponseWriter, err error) { return } + // adaptor 已直接写入响应体(如 Gemini 原生格式透传),跳过二次写入 + var prewritten *constant.RelayError + if errors.As(err, &prewritten) && prewritten.ResponseWritten { + return + } + var relayErr *constant.RelayError var rateLimitErr *RelayErrorWithRateLimit statusCode := http.StatusInternalServerError @@ -171,8 +177,10 @@ func WriteRelayError(w http.ResponseWriter, err error) { statusCode = http.StatusInternalServerError } - if statusCode >= 500 { - g.Log().Errorf(context.Background(), "[RelayError] statusCode=%d type=%s message=%s originalError=%v", + // 无可用渠道是正常业务条件,已在 handleChannelUnavailable 中以 Warning 记录,此处跳过避免重复日志; + // 其余 5xx 为真实错误,保留 ERROR 但禁用堆栈打印(此处调用栈固定,无调试价值) + if statusCode >= 500 && !errors.Is(err, common.ErrChannelUnavailable) { + g.Log().Stack(false).Errorf(context.Background(), "[RelayError] statusCode=%d type=%s message=%s originalError=%v", statusCode, errType, errMsg, err) } diff --git a/relay/handler/claude_handler.go b/relay/handler/claude_handler.go index 9c6427a..a089a59 100644 --- a/relay/handler/claude_handler.go +++ b/relay/handler/claude_handler.go @@ -25,6 +25,12 @@ func WriteClaudeRelayError(w http.ResponseWriter, err error) { return } + // adaptor 已直接写入响应体(如 Gemini 原生格式透传),跳过二次写入 + var prewritten *constant.RelayError + if errors.As(err, &prewritten) && prewritten.ResponseWritten { + return + } + var relayErr *constant.RelayError var rateLimitErr *RelayErrorWithRateLimit statusCode := http.StatusInternalServerError @@ -48,8 +54,10 @@ func WriteClaudeRelayError(w http.ResponseWriter, err error) { statusCode = http.StatusInternalServerError } - if statusCode >= 500 { - g.Log().Errorf(context.Background(), "[ClaudeRelayError] statusCode=%d type=%s message=%s originalError=%v", + // 无可用渠道是正常业务条件,已在 handleChannelUnavailable 中以 Warning 记录,此处跳过避免重复日志; + // 其余 5xx 为真实错误,保留 ERROR 但禁用堆栈打印(此处调用栈固定,无调试价值) + if statusCode >= 500 && !errors.Is(err, common.ErrChannelUnavailable) { + g.Log().Stack(false).Errorf(context.Background(), "[ClaudeRelayError] statusCode=%d type=%s message=%s originalError=%v", statusCode, errType, errMsg, err) } diff --git a/relay/handler/error_writer_test.go b/relay/handler/error_writer_test.go new file mode 100644 index 0000000..5aab9b9 --- /dev/null +++ b/relay/handler/error_writer_test.go @@ -0,0 +1,67 @@ +package handler + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/qianfree/team-api/relay/constant" +) + +// TestErrorWriters_SkipResponseWritten 验证当 adaptor 已置 ResponseWritten=true 时, +// 三个错误写入器都跳过二次写入(不写 header、不写 body)。 +// 这是 Gemini/OpenAI「双重写入」修复的核心机制兜底。 +func TestErrorWriters_SkipResponseWritten(t *testing.T) { + writtenErr := constant.NewUpstreamError(http.StatusTooManyRequests, "upstream 429", nil) + writtenErr.ResponseWritten = true + + cases := []struct { + name string + fn func(http.ResponseWriter, error) + }{ + {"WriteRelayError", WriteRelayError}, + {"WriteClaudeRelayError", WriteClaudeRelayError}, + {"WriteGeminiRelayError", WriteGeminiRelayError}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + rec := httptest.NewRecorder() + c.fn(rec, writtenErr) + + if rec.Code != http.StatusOK { + t.Errorf("Code = %d, want 200 (no WriteHeader should happen)", rec.Code) + } + if rec.Body.Len() != 0 { + t.Errorf("body = %q, want empty (no write should happen)", rec.Body.String()) + } + }) + } +} + +// TestErrorWriters_WriteNormalError 验证普通(未标记 ResponseWritten)的 RelayError +// 仍被三个写入器正常写入一次,确保短路逻辑不影响正常错误路径。 +func TestErrorWriters_WriteNormalError(t *testing.T) { + normalErr := constant.NewUpstreamError(http.StatusTooManyRequests, "upstream 429", nil) + + cases := []struct { + name string + fn func(http.ResponseWriter, error) + }{ + {"WriteRelayError", WriteRelayError}, + {"WriteClaudeRelayError", WriteClaudeRelayError}, + {"WriteGeminiRelayError", WriteGeminiRelayError}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + rec := httptest.NewRecorder() + c.fn(rec, normalErr) + + if rec.Code != http.StatusTooManyRequests { + t.Errorf("Code = %d, want 429", rec.Code) + } + if rec.Body.Len() == 0 { + t.Errorf("body empty, want non-empty error response") + } + }) + } +} diff --git a/relay/handler/gemini_handler.go b/relay/handler/gemini_handler.go index 87598ff..1168a92 100644 --- a/relay/handler/gemini_handler.go +++ b/relay/handler/gemini_handler.go @@ -61,6 +61,12 @@ func HandleGeminiModelDetail(ctx context.Context, tenantID int64, modelName stri // WriteGeminiRelayError 写入 Gemini 格式的错误响应 func WriteGeminiRelayError(w http.ResponseWriter, err error) { + // adaptor 已直接写入响应体(如 Gemini 原生格式透传),跳过二次写入 + var prewritten *constant.RelayError + if errors.As(err, &prewritten) && prewritten.ResponseWritten { + return + } + var relayErr *constant.RelayError var rateLimitErr *RelayErrorWithRateLimit statusCode := http.StatusInternalServerError @@ -95,8 +101,10 @@ func WriteGeminiRelayError(w http.ResponseWriter, err error) { statusCode = http.StatusInternalServerError } - if statusCode >= 500 { - g.Log().Errorf(context.Background(), "[GeminiRelayError] statusCode=%d status=%s message=%s originalError=%v", + // 无可用渠道是正常业务条件,已在 handleChannelUnavailable 中以 Warning 记录,此处跳过避免重复日志; + // 其余 5xx 为真实错误,保留 ERROR 但禁用堆栈打印(此处调用栈固定,无调试价值) + if statusCode >= 500 && !errors.Is(err, common.ErrChannelUnavailable) { + g.Log().Stack(false).Errorf(context.Background(), "[GeminiRelayError] statusCode=%d status=%s message=%s originalError=%v", statusCode, errStatus, errMsg, err) } diff --git a/relay/handler/passthrough.go b/relay/handler/passthrough.go index 98be742..dd7e5d3 100644 --- a/relay/handler/passthrough.go +++ b/relay/handler/passthrough.go @@ -9,6 +9,14 @@ import ( func canPassThrough(info *common.RelayInfo) bool { settings := info.ChannelMeta.Settings + // 阿里云同步 multimodal 图片(qwen-image-2.x):入站 OpenAI Images 格式与上游 DashScope + // multimodal-generation 的 messages 结构不兼容,必须经 ConvertRequest 转换。即使运营者 + // 显式开启直连、或未配置模型映射,也不能原样透传,否则上游因请求体格式错误报错。 + if constant.RelayMode(info.RelayMode) == constant.RelayModeImagesGenerations && + constant.IsAliSyncMultimodalImageModel(info.ChannelMeta.UpstreamModelName) { + return false + } + // 显式开启:运营者明确配置直连,不做额外检查 if settings.PassThroughBodyEnabled { return true diff --git a/relay/handler/relay_handler.go b/relay/handler/relay_handler.go index 268d275..dd3aa5f 100644 --- a/relay/handler/relay_handler.go +++ b/relay/handler/relay_handler.go @@ -420,6 +420,20 @@ func RelayHandler(ctx context.Context, body []byte, path string, headers http.He return result.usage, result.billingResult, result.err } + // 阿里云百炼(DashScope)异步图片模型(wanx*、qwen-image-plus 等)上游为异步任务式 + // (提交拿 task_id → 轮询取图),其 image-synthesis 端点强制异步,无法经同步 + // /v1/images/generations 一次性返回图片。命中时直接引导客户端改用异步端点。 + // 判定收敛到 constant.IsAsyncImageModel(按 provider + 模型),qwen-image-2.x 等 + // 同步 multimodal 模型不在此列,会继续走下方同步转发。租户模型列表 async_image 标记同源。 + if v.relayMode == constant.RelayModeImagesGenerations && + constant.IsAsyncImageModel(constant.ProviderType(selection.ChannelType), selection.UpstreamModelName) { + if billing != nil && preDeductAmount > 0 { + _ = billing.SettleFailed(ctx, rc.TenantID, rc.RequestID, preDeductAmount) + } + return nil, v.billingResult, constant.NewRequestError( + "this image model uses asynchronous generation on Alibaba DashScope; submit via POST /v1/images/generations/async and poll for the result", nil) + } + info := buildRelayInfo(ctx, rc, v, selection, path, headers) if tr := monitor.GetTrackedRequest(rc.RequestID); tr != nil { @@ -675,7 +689,8 @@ func handleChannelUnavailable( if billing != nil && preDeductAmount > 0 { _ = billing.SettleFailed(ctx, rc.TenantID, rc.RequestID, preDeductAmount) } - g.Log().Errorf(ctx, "[RelayHandler] All %d channels failed for model=%s tenant=%d user=%d request=%s. Failure details: %s", + // 全部渠道失败是真实运营告警,保留 ERROR 级别;但调用栈固定无意义,禁用堆栈打印 + g.Log().Stack(false).Errorf(ctx, "[RelayHandler] All %d channels failed for model=%s tenant=%d user=%d request=%s. Failure details: %s", len(channelErrors), v.modelName, rc.TenantID, rc.UserID, rc.RequestID, strings.Join(channelErrors, "\n")) allFailedErr := constant.NewChannelError( fmt.Sprintf("all %d channels failed for model: %s", len(channelErrors), v.modelName), @@ -688,7 +703,9 @@ func handleChannelUnavailable( if billing != nil && preDeductAmount > 0 { _ = billing.SettleFailed(ctx, rc.TenantID, rc.RequestID, preDeductAmount) } - g.Log().Errorf(ctx, "[RelayHandler] No available channel for model=%s tenant=%d user=%d", v.modelName, rc.TenantID, rc.UserID) + // 无可用渠道属于正常业务条件(用户请求了未配置/未启用渠道的模型), + // 降级为 Warning 并禁用堆栈打印,避免污染 ERROR 日志与刷屏无意义的调用栈 + g.Log().Stack(false).Warningf(ctx, "[RelayHandler] No available channel for model=%s tenant=%d user=%d", v.modelName, rc.TenantID, rc.UserID) noChErr := constant.NewChannelError("no available channel for model: "+v.modelName, err) recordFailedUsage(provider, rc, 0, v.modelName, v.relayMode, v.isStream, noChErr) return &channelUnavailableResult{nil, v.billingResult, noChErr} diff --git a/relay/taskchannel/ali/adaptor.go b/relay/taskchannel/ali/adaptor.go index 4c89fe2..b49d74c 100644 --- a/relay/taskchannel/ali/adaptor.go +++ b/relay/taskchannel/ali/adaptor.go @@ -118,9 +118,15 @@ func (a *AliAdaptor) Init(info *common.RelayInfo) { a.info = info } -// detectVideo 根据模型名判断是否为视频生成 +// detectVideo 根据模型名判断是否为视频生成。 +// 注意:万相图片模型(-t2i 文生图、-i2i 图像编辑)即使以 wan2. 开头也不是视频,必须先排除, +// 否则会被误判为视频而 POST 到 video-synthesis 端点,被上游以 "url error" 拒绝。 func (a *AliAdaptor) detectVideo(modelName string) bool { - return strings.HasPrefix(modelName, "wan2.") || strings.HasPrefix(modelName, "wanx2.1-t2v") + m := strings.ToLower(modelName) + if strings.Contains(m, "-t2i") || strings.Contains(m, "-i2i") { + return false + } + return strings.HasPrefix(m, "wan2.") || strings.HasPrefix(m, "wanx2.1-t2v") } func (a *AliAdaptor) ValidateRequest(_ context.Context, _ *common.RelayInfo, body []byte) *common.TaskError { diff --git a/test/webhook_receiver/main.go b/test/webhook_receiver/main.go new file mode 100644 index 0000000..089aa0b --- /dev/null +++ b/test/webhook_receiver/main.go @@ -0,0 +1,168 @@ +package main + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "io" + "log" + "net/http" + "os" + "strings" + "time" +) + +// WebhookEvent 接收到的 webhook 事件格式 +type WebhookEvent struct { + EventID string `json:"event_id"` + EventType string `json:"event_type"` + TenantID int64 `json:"tenant_id"` + Timestamp string `json:"timestamp"` + Data map[string]interface{} `json:"data"` +} + +// 全局变量:webhook 密钥,用于验签 +var secretKey string + +func main() { + // 从命令行参数或环境变量获取密钥 + secretKey = os.Getenv("WEBHOOK_SECRET") + if len(os.Args) > 1 { + secretKey = os.Args[1] + } + + if secretKey == "" { + log.Println("⚠️ 未提供 webhook 密钥,签名验证将跳过") + log.Println(" 用法: go run main.go ") + log.Println(" 或设置环境变量: WEBHOOK_SECRET=whk_xxx go run main.go") + log.Println() + } else { + log.Printf("🔑 已加载密钥: %s...%s\n", secretKey[:8], secretKey[len(secretKey)-4:]) + } + + port := os.Getenv("PORT") + if port == "" { + port = "9090" + } + + http.HandleFunc("/webhook", handleWebhook) + http.HandleFunc("/health", handleHealth) + http.HandleFunc("/", handleNotFound) + + addr := ":" + port + log.Printf("🚀 Webhook 接收服务启动在 http://localhost%s/webhook\n", addr) + log.Printf(" 健康检查: http://localhost%s/health\n", addr) + log.Printf(" 等待接收 webhook 事件...\n") + log.Println(strings.Repeat("=", 70)) + + if err := http.ListenAndServe(addr, nil); err != nil { + log.Fatalf("服务启动失败: %v", err) + } +} + +func handleHealth(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) +} + +func handleNotFound(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{ + "error": "not_found", + "message": "请使用 POST /webhook 接收事件", + }) +} + +func handleWebhook(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, `{"error":"method_not_allowed"}`, http.StatusMethodNotAllowed) + return + } + + now := time.Now() + receiveTime := now.Format("2006-01-02 15:04:05.000") + + // 读取请求体 + body, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("❌ [%s] 读取请求体失败: %v\n", receiveTime, err) + http.Error(w, `{"error":"read_body_failed"}`, http.StatusBadRequest) + return + } + defer r.Body.Close() + + // 提取关键 Header + signature := r.Header.Get("X-Webhook-Signature") + timestamp := r.Header.Get("X-Webhook-Timestamp") + eventType := r.Header.Get("X-Webhook-Event") + eventID := r.Header.Get("X-Webhook-ID") + + log.Printf("📩 [%s] 收到 Webhook 事件", receiveTime) + log.Println(strings.Repeat("-", 50)) + log.Printf(" Event ID: %s", eventID) + log.Printf(" Event Type: %s", eventType) + log.Printf(" Timestamp: %s", timestamp) + + // 签名验证 + if secretKey != "" { + expectedSig := computeSignature(secretKey, timestamp, body) + if hmac.Equal([]byte(signature), []byte(expectedSig)) { + log.Printf(" 签名验证: ✅ 通过") + } else { + log.Printf(" 签名验证: ❌ 失败!") + log.Printf(" 期望签名: %s", expectedSig) + log.Printf(" 实际签名: %s", signature) + } + } else { + log.Printf(" 签名验证: ⏭️ 跳过(未配置密钥)") + } + + // 解析 Payload + var event WebhookEvent + if err := json.Unmarshal(body, &event); err != nil { + log.Printf(" JSON 解析: ❌ 失败: %v", err) + log.Printf(" 原始 Body: %s", string(body)) + } else { + log.Printf(" Tenant ID: %d", event.TenantID) + log.Printf(" Event Time: %s", event.Timestamp) + + // 格式化输出 Data + if event.Data != nil { + dataJSON, _ := json.MarshalIndent(event.Data, " ", " ") + log.Printf(" Data:") + log.Printf(" %s", string(dataJSON)) + } + } + + // 原始 Body(美化的 JSON) + var prettyBody map[string]interface{} + if err := json.Unmarshal(body, &prettyBody); err == nil { + beautified, _ := json.MarshalIndent(prettyBody, " ", " ") + log.Printf(" 完整 Payload:") + log.Printf(" %s", string(beautified)) + } else { + log.Printf(" 原始 Body: %s", string(body)) + } + + log.Println(strings.Repeat("-", 50)) + log.Printf(" ✅ 已处理 (%d bytes)", len(body)) + log.Println(strings.Repeat("=", 70)) + + // 返回 200 OK + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{ + "status": "received", + "event_id": eventID, + }) +} + +// computeSignature 计算 HMAC-SHA256 签名,与系统实现一致 +// 签名内容: timestamp + "." + body +func computeSignature(secret, timestamp string, body []byte) string { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(timestamp + "." + string(body))) + return hex.EncodeToString(mac.Sum(nil)) +} diff --git a/web/admin/src/router/admin.ts b/web/admin/src/router/admin.ts index 3a074f6..9897bc3 100644 --- a/web/admin/src/router/admin.ts +++ b/web/admin/src/router/admin.ts @@ -216,6 +216,12 @@ const adminRoutes: RouteRecordRaw[] = [ component: () => import('@/views/system/SettingsPage.vue'), meta: { title: '系统设置' }, }, + { + path: 'files', + name: 'AdminFiles', + component: () => import('@/views/system/FilesPage.vue'), + meta: { title: '文件管理' }, + }, { path: 'help-categories', name: 'AdminHelpCategories', diff --git a/web/admin/src/views/AdminLayout.vue b/web/admin/src/views/AdminLayout.vue index 3767714..9b3c96b 100644 --- a/web/admin/src/views/AdminLayout.vue +++ b/web/admin/src/views/AdminLayout.vue @@ -355,6 +355,7 @@ const menuGroups = [ { name: 'AdminUsers', label: '用户管理', icon: IconUserGroup }, { name: 'AdminPlugins', label: '插件管理', icon: IconCodeBlock }, { name: 'AdminSettings', label: '系统设置', icon: IconSettings }, + { name: 'AdminFiles', label: '文件管理', icon: IconStorage }, { name: 'AdminPaymentSettings', label: '支付设置', icon: IconStorage }, { name: 'AdminHelpCategories', label: '帮助分类', icon: IconLayers }, { name: 'AdminHelpArticles', label: '帮助文章', icon: IconFile }, diff --git a/web/admin/src/views/ai/ChannelsPage.vue b/web/admin/src/views/ai/ChannelsPage.vue index c10d085..2b14f63 100644 --- a/web/admin/src/views/ai/ChannelsPage.vue +++ b/web/admin/src/views/ai/ChannelsPage.vue @@ -102,9 +102,18 @@ const columns: TableColumnData[] = [ }, { title: '创建时间', dataIndex: 'created_at', width: 170 }, { - title: '操作', dataIndex: 'actions', width: 150, fixed: 'right', + title: '操作', dataIndex: 'actions', width: 210, fixed: 'right', render({ record }) { + const isActive = record.status === 'active' + const action = isActive ? '禁用' : '启用' return h(Space, { size: 4 }, () => [ + h(Popconfirm, { + content: `确定${action}该渠道?`, + onOk: () => toggleStatus(record), + }, () => h(Button, { + size: 'small', + status: isActive ? 'normal' : 'success', + }, () => action)), h(Button, { size: 'small', type: 'primary', onClick: () => openDetail(record) }, () => '详情'), h(Popconfirm, { content: '确定删除该渠道?此操作不可撤销。', onOk: () => deleteChannel(record) }, () => h(Button, { size: 'small', status: 'danger' }, () => '删除')), ]) @@ -195,6 +204,30 @@ async function deleteChannel(row: any) { catch { /* interceptor handles error toast */ } } +// 快捷启用/禁用渠道:复用 PUT /admin/channels/{id}。 +// 后端 UpdateChannel 会无条件写入 priority/weight,故必须带上当前值,否则会被重置为 0。 +// status 为 active → 禁用;其它(disabled/testing)→ 启用为 active。 +async function toggleStatus(record: any) { + if (record._toggling) return + const isActive = record.status === 'active' + const nextStatus = isActive ? 'disabled' : 'active' + const prevStatus = record.status + record._toggling = true + record.status = nextStatus // 乐观更新,状态 Tag 即刻变化 + try { + await request.put(`/admin/channels/${record.id}`, { + status: nextStatus, + priority: record.priority, + weight: record.weight, + }) + message.success(nextStatus === 'active' ? '已启用' : '已禁用') + } catch { + record.status = prevStatus // 失败回滚,错误提示由 request 拦截器统一处理 + } finally { + record._toggling = false + } +} + onMounted(fetchData) // === OAuth === diff --git a/web/admin/src/views/monitor/RealtimeMonitorPage.vue b/web/admin/src/views/monitor/RealtimeMonitorPage.vue index d201a44..578f758 100644 --- a/web/admin/src/views/monitor/RealtimeMonitorPage.vue +++ b/web/admin/src/views/monitor/RealtimeMonitorPage.vue @@ -7,7 +7,7 @@ import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/compon import VChart from 'vue-echarts' import PageHeader from '@/components/PageHeader.vue' import request from '@/utils/request' -import { IconThunderbolt, IconSwap, IconClockCircle, IconCode, IconBarChart, IconLayers, IconApps, IconUserGroup, IconFire } from '@arco-design/web-vue/es/icon' +import { IconThunderbolt, IconSwap, IconClockCircle, IconCode, IconBarChart, IconLayers, IconApps, IconUserGroup, IconFire, IconStorage } from '@arco-design/web-vue/es/icon' use([CanvasRenderer, LineChart, BarChart, GridComponent, TooltipComponent, LegendComponent]) @@ -166,6 +166,28 @@ const byChannel = computed(() => { return entries.map(([k, v]) => ({ channel: k, count: v as number, pct: ((v as number) / max) * 100 })) }) +// 同步图片「异步化」Worker 池状态 +const pool = computed(() => data.value.sync_image_pool || {}) +const workerPct = computed(() => { + const t = pool.value.worker_total || 0 + return t > 0 ? Math.min(100, ((pool.value.worker_busy || 0) / t) * 100) : 0 +}) +const queuePct = computed(() => { + const c = pool.value.queue_cap || 0 + return c > 0 ? Math.min(100, ((pool.value.queue_depth || 0) / c) * 100) : 0 +}) +// 队列水位配色:>95% 红(即将背压 429)、>80% 橙、否则蓝 +const queueColor = computed(() => { + const p = queuePct.value + return p > 95 ? '#F53F3F' : p > 80 ? '#FF7D00' : '#165DFF' +}) +const poolChannelInflight = computed(() => { + const m: Record = pool.value.channel_inflight || {} + const entries = Object.entries(m).sort(([, a], [, b]) => (b as number) - (a as number)).slice(0, 10) + const max = entries.length > 0 ? (entries[0][1] as number) : 1 + return entries.map(([k, v]) => ({ channel: k, count: v as number, pct: ((v as number) / max) * 100 })) +}) + let pollTimer: ReturnType | null = null let resizeTimer: ReturnType | null = null @@ -354,6 +376,66 @@ onUnmounted(() => { + + + + +
+ +
+
+
+ Worker 利用率 + {{ pool.worker_busy || 0 }} / {{ pool.worker_total || 0 }} 忙碌 +
+ +
+
+
+ 队列水位 + {{ pool.queue_depth || 0 }} / {{ pool.queue_cap || 0 }} +
+ +
+
+ +
+
+ {{ (pool.enqueued || 0).toLocaleString() }} + 已入队 +
+
+ {{ (pool.succeeded || 0).toLocaleString() }} + 成功 +
+
+ {{ (pool.failed || 0).toLocaleString() }} + 失败 +
+
+ {{ (pool.rejected || 0).toLocaleString() }} + 拒绝 (429) +
+
+
+ +
+
按渠道在途(层② per-channel 容量)
+
暂无在途任务
+
+
+ 渠道 {{ item.channel }} + {{ item.count }} +
+
+
+
+
+
+ +
@@ -639,6 +721,59 @@ onUnmounted(() => { font-weight: 500; } +/* Sync-image worker pool */ +.pool-grid { + display: grid; + grid-template-columns: 1.4fr 1fr; + gap: 24px; + align-items: center; +} +.pool-bars { + display: flex; + flex-direction: column; + gap: 14px; +} +.pool-bar-label { + display: flex; + justify-content: space-between; + font-size: 13px; + color: var(--color-text-2); + margin-bottom: 6px; +} +.pool-stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; + padding-left: 24px; + border-left: 1px solid var(--color-border); +} +.pool-stat { + text-align: center; +} +.pool-stat-value { + display: block; + font-size: 20px; + font-weight: 600; + color: var(--color-text-1); + font-family: 'Menlo', 'Monaco', 'Courier New', monospace; +} +.pool-stat-label { + display: block; + font-size: 11px; + color: var(--color-text-3); + margin-top: 2px; +} +.pool-inflight { + margin-top: 16px; + padding-top: 12px; + border-top: 1px solid var(--color-border); +} +.pool-inflight-title { + font-size: 12px; + color: var(--color-text-3); + margin-bottom: 10px; +} + /* Distribution bars */ .dist-row { margin-bottom: 10px; @@ -698,6 +833,8 @@ onUnmounted(() => { } @media (max-width: 768px) { .row-2 { grid-template-columns: 1fr; } + .pool-grid { grid-template-columns: 1fr; } + .pool-stats { padding-left: 0; border-left: none; } } @media (max-width: 480px) { .metrics-grid { diff --git a/web/admin/src/views/system/FilesPage.vue b/web/admin/src/views/system/FilesPage.vue new file mode 100644 index 0000000..c78865e --- /dev/null +++ b/web/admin/src/views/system/FilesPage.vue @@ -0,0 +1,544 @@ + + + + + diff --git a/web/admin/src/views/system/settings/ChannelTab.vue b/web/admin/src/views/system/settings/ChannelTab.vue index 70b1573..da9f043 100644 --- a/web/admin/src/views/system/settings/ChannelTab.vue +++ b/web/admin/src/views/system/settings/ChannelTab.vue @@ -70,6 +70,27 @@ const values = useFormValues() 全局代理 URL,支持 http:// 和 socks5:// 协议。在渠道编辑中开启"使用代理"即可生效。
+ + +
+
同步图片异步化
+
+ 同步图片厂商异步化 + 同步阻塞返回的图片厂商(OpenAI/DALL·E 等)走 /v1/images/generations/async 时改由后台 worker 池异步处理,客户端提交即拿 task_id 后轮询取图;关闭则该端点对同步厂商返回不支持 + +
+
+ 图片 URL 转存对象存储 + 上游返回图片 URL 时下载并转存对象存储(返回 24h 稳定链接,需已配置存储);关闭则直接透传上游 URL(部分厂商约 1h 过期)。b64_json 始终转存 + +
+
diff --git a/web/admin/src/views/system/settings/StorageTab.vue b/web/admin/src/views/system/settings/StorageTab.vue index da2b5c0..6aa5dea 100644 --- a/web/admin/src/views/system/settings/StorageTab.vue +++ b/web/admin/src/views/system/settings/StorageTab.vue @@ -1,6 +1,17 @@