topfans/docs/specs/2026-06-02-dify-laser-card-workflow.md
2026-06-03 22:19:22 +08:00

388 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Dify 工作流配置文档:`laser_card_variants_v1`
> 文件路径: `docs/specs/2026-06-02-dify-laser-card-workflow.md`
>
> 本文档是 Dify 平台上 `laser_card_variants_v1` 工作流的完整配置指南。
## 工作流概述
- **类型:** WorkflowAPI 触发)
- **名称:** `laser_card_variants_v1`
- **用途:** 接收用户图片 → 抠图 → MiniMax AI 生成背景+装饰 → laser-compositor 合成 → 返回 5 张镭射卡 URL
- **预计耗时:** 30-60 秒
## 开始节点 — 输入变量
> **Dify 平台限制:** Dify 的开始节点不支持 `array` 类型。`preset_codes` 和 `render_configs` 使用 **文本 (string)** 类型,传入 JSON 字符串,在代码节点中 `json.loads` 解析。
| 变量 | Dify 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `source_image_url` | 文本 (string) | 是 | — | 用户原图 OSS signed URL |
| `use_cutout` | 复选框 (boolean) | 是 | — | 是否人像抠图 |
| `preset_codes` | 文本 (string) | 否 | `["dream","classic","holoFull","ice","sunset"]` | JSON 数组字符串,例如 `"[\"dream\",\"classic\"]"` |
| `render_configs` | 文本 (string) | 是 | — | JSON 数组字符串,包含每个 variant 的 grating_config、bg_prompt、overlay_prompt |
**Dify 平台操作步骤:**
1. 添加 `preset_codes`:类型 **文本**,变量名 `preset_codes`**不**勾选必填,默认值填 `["dream","classic","holoFull","ice","sunset"]`
2. 添加 `render_configs`:类型 **文本**,变量名 `render_configs`**勾选**必填,默认值留空
3. 确认 `source_image_url`(文本,必填)和 `use_cutout`(复选框,必填)正确
4. **保存**开始节点
## 节点链路
```mermaid
flowchart TB
START[开始节点] --> NORM[代码节点1: 参数展开]
NORM --> BRANCH{条件: use_cutout?}
BRANCH -->|true| SEG[HTTP节点: Gateway /segment 抠图]
BRANCH -->|false| LOOP
SEG -->|成功| LOOP[循环节点: preset_codes x5]
SEG -->|失败| WARN1[添加 warning]
WARN1 --> LOOP
subgraph variant_loop[循环内: 每个 variant]
BG[HTTP节点: MiniMax 生成背景图]
DECO[HTTP节点: MiniMax 生成装饰图]
COMP[HTTP节点: laser-compositor /compose]
BG --> DECO --> COMP
end
LOOP --> variant_loop
COMP --> AGG[代码节点2: 聚合输出]
AGG --> END_NODE[结束节点]
```
---
## 各节点详细配置
### 节点 1代码节点 — 参数展开
**输入:** `preset_codes`, `render_configs`, `use_cutout`
**代码Python**
```python
import json
def main(preset_codes: str, render_configs: str, use_cutout: bool) -> dict:
"""
解析 JSON 字符串并展开 variants 列表
因为 Dify 开始节点不支持 array 类型preset_codes 和 render_configs 都以 string 传入
"""
# 解析 preset_codes字符串 → 列表)
if preset_codes and preset_codes.strip():
try:
preset_list = json.loads(preset_codes)
except:
preset_list = []
else:
preset_list = []
if not preset_list:
preset_list = ["dream", "classic", "holoFull", "ice", "sunset"]
# 解析 render_configs字符串 → 列表)
if render_configs and render_configs.strip():
try:
configs = json.loads(render_configs)
except:
configs = []
else:
configs = []
# 构建 preset_id → config 映射
config_map = {rc.get("preset_id"): rc for rc in configs if "preset_id" in rc}
variants = []
for pc in preset_list:
if pc in config_map:
cfg = config_map[pc]
variants.append({
"preset_id": pc,
"grating_config": cfg.get("grating_config", {}),
"bg_prompt": cfg.get("bg_prompt", ""),
"overlay_prompt": cfg.get("overlay_prompt", ""),
})
return {
"variants": json.dumps(variants),
"variant_count": len(variants),
"use_cutout": use_cutout,
}
```
---
### 节点 2条件分支 — 抠图判断
**条件:** `{{#代码节点1.use_cutout#}}` == true
- **true 分支 →** HTTP 节点调用抠图 API
- **false 分支 →** 直接进入循环
---
### 节点 3HTTP 节点 — 抠图Segment
```
方法: POST
URL: http://{GATEWAY_HOST}/api/v1/segment
Headers:
Content-Type: multipart/form-data
Body (form-data):
image: {{文件上传引用}}
scene: portrait
Authorization: Bearer {{#env.GATEWAY_JWT_TOKEN#}}
```
> **注意:** Dify HTTP 节点的 multipart 能力有限。如果 Dify 不支持 multipart form-data可以用代码节点构造 HTTP 请求,或改为让 Gateway 提供 JSON 接口(传入 image_url
**替代方案推荐Gateway 扩展 `/api/v1/segment` 支持 JSON body**
```
POST http://{GATEWAY_HOST}/api/v1/segment/json
Body (JSON):
{
"image_url": "{{#开始节点.source_image_url#}}",
"scene": "portrait"
}
```
返回:
```json
{
"success": true,
"cutout_oss_key": "laser-card/.../xxx_cutout.png",
"cutout_url_signed": "https://oss.example.com/..."
}
```
如果失败:设置 `cutout_url = ""`,记 warning。
---
### 节点 4循环节点 — variant 遍历
**循环对象:** `{{#代码节点1.variants#}}`JSON array
**循环变量名:** `variant_item`
循环内包含 3 个子节点(串行执行):
---
#### 子节点 4aMiniMax 生成背景图
```
方法: POST
URL: https://api.minimaxi.com/v1/image_generation
Headers:
Authorization: Bearer {{#env.MINIMAX_API_KEY#}}
Content-Type: application/json
Body (JSON):
{
"model": "image-01",
"prompt": "{{#variant_item.bg_prompt#}}",
"aspect_ratio": "3:4",
"n": 1,
"prompt_optimizer": true,
"response_format": "url"
}
```
**超时:** 30 秒
**输出提取:**
- `bg_url` = `{{#输出.body.data[0].url#}}`
如果失败,设置 `bg_url = ""`,记 warning。
---
#### 子节点 4bMiniMax 生成装饰图
```
方法: POST
URL: https://api.minimaxi.com/v1/image_generation
Headers:
Authorization: Bearer {{#env.MINIMAX_API_KEY#}}
Content-Type: application/json
Body (JSON):
{
"model": "image-01",
"prompt": "{{#variant_item.overlay_prompt#}}",
"aspect_ratio": "3:4",
"n": 1,
"prompt_optimizer": true,
"response_format": "url"
}
```
**超时:** 30 秒
**输出提取:**
- `overlay_url` = `{{#输出.body.data[0].url#}}`
---
#### 子节点 4claser-compositor 合成
```
方法: POST
URL: http://{LASER_COMPOSITOR_HOST}/compose
Headers:
Content-Type: application/json
Body (JSON):
{
"background_url": "{{#子节点4a.bg_url#}}",
"cutout_url": "{{#代码节点3.cutout_url_signed#}}",
"overlay_url": "{{#子节点4b.overlay_url#}}",
"grating_config": {{#variant_item.grating_config#}},
"export_width": 450,
"export_height": 600,
"variant_index": {{#循环索引#}},
"output_oss_key": "laser-card/{date}/{uuid}_variant_{{循环索引}}"
}
```
**超时:** 30 秒
**返回:**
```json
{
"status": "succeeded",
"variant_index": 0,
"width": 450,
"height": 600,
"oss_key": "laser-card/.../variant_0.png",
"signed_url": "https://oss.example.com/..."
}
```
---
### 节点 5代码节点 — 聚合输出
**目的:** 将循环中 5 个 variant 的结果聚合为统一 JSON
```python
import json
def main(variant_results: list, cutout_url: str, warnings: list) -> dict:
"""
聚合循环结果
variant_results: 从循环节点收集的结果数组
"""
variants = []
output_warnings = warnings.copy() if warnings else []
for i, vr in enumerate(variant_results):
if vr and vr.get("status") == "succeeded":
variants.append({
"preset_id": vr.get("preset_id", f"variant_{i}"),
"oss_key": vr.get("oss_key", ""),
"signed_url": vr.get("signed_url", ""),
"width": vr.get("width", 450),
"height": vr.get("height", 600),
})
else:
output_warnings.append(f"variant_{i} failed")
return {
"status": "succeeded" if len(variants) > 0 else "failed",
"variants": variants,
"cutout_oss_key": cutout_url or "",
"warnings": output_warnings,
}
```
---
### 结束节点 — 输出格式
```json
{
"status": "succeeded",
"variants": [
{
"preset_id": "dream",
"oss_key": "laser-card/xxxx/variant_0.png",
"signed_url": "https://oss.example.com/...",
"width": 450,
"height": 600
}
],
"cutout_oss_key": "laser-card/xxxx/cutout.png",
"warnings": []
}
```
---
## 环境变量(在 Dify 工作流中配置)
| 变量 | 说明 |
|---|---|
| `GATEWAY_HOST` | Gateway 地址,如 `http://192.168.1.100:8080` |
| `MINIMAX_API_KEY` | MiniMax API 密钥 |
| `LASER_COMPOSITOR_HOST` | laser-compositor 地址,如 `http://127.0.0.1:7000` |
---
## 鉴权说明
Dify 工作流调用 Gateway `/api/v1/segment`(需要 JWT需要在 Dify 的 HTTP 节点中传入 Bearer Token。有两种方式
1. **在 Dify 环境变量中配置一个长期有效的 JWT token**`GATEWAY_JWT_TOKEN`)— 简单但需要定期维护
2. **Gateway 扩展 `/api/v1/segment/json`** — 不强制 JWT 鉴权仅限内网调用Dify→Gateway 走内网)
推荐方案 2减少 Token 维护成本。
---
## 装饰图 prompt 模板
每个 preset 的 `bg_prompt``overlay_prompt` 由前端 `laserPresets.js``buildRenderConfigs()` 动态生成并传入。Dify 不需要维护 prompt 数据。
5 个 preset 的参考 prompt维护在前端 `laserPresets.js`
| Preset | bg_prompt | overlay_prompt |
|---|---|---|
| dream | 梦幻柔光渐变背景淡紫色调和粉色柔和泡泡和星光光斑虚化景深效果3:4竖版比例 | 精致银色星点散布细边框装饰线透明背景SVG风格手绘装饰元素 |
| classic | 经典复古胶片风格背景暖色调浅香槟色渐变艺术纹理和柔和光影3:4竖版比例 | 经典复古边框花纹装饰,暖金色调和银色细线,透明背景,精致几何图案 |
| holoFull | 全息科技感背景银色金属质感未来主义光影和几何线条珍珠光泽渐变3:4竖版比例 | 全息科技感装饰边框,霓虹色细线光晕,透明背景,电路板几何图案和星形闪光 |
| ice | 冰雪冷色调背景浅蓝灰渐变冰晶纹理和雪花光斑清爽透明感3:4竖版比例 | 冰晶装饰边框,浅蓝色和银色星点,透明背景,雪花图案和细线装饰 |
| sunset | 日落暖色调背景玫瑰金渐变暖橙色和粉色光影温馨氛围3:4竖版比例 | 日落暖色调装饰边框,玫瑰金和琥珀色星点,透明背景,柔和花卉和光线元素 |
---
## 测试方法
1. 在 Dify 平台创建 Workflow按本文档配置各节点**特别注意**`preset_codes` 和 `render_configs` 选**文本**类型)
2. 使用 Dify 的"运行"功能测试,传入示例 input
> **关键:** `preset_codes` 和 `render_configs` 的值必须是 **JSON 字符串**(带转义引号),不是 JSON 对象。
```json
{
"source_image_url": "https://oss.example.com/test/user_photo.jpg",
"use_cutout": true,
"preset_codes": "[\"dream\"]",
"render_configs": "[{\"preset_id\":\"dream\",\"grating_config\":{\"sheen_band_angle\":135,\"sheen_intensity\":0.40,\"sheen_speed\":0.35,\"foil_coverage\":0.75,\"backdrop_tone\":\"#A8ACB2\"},\"bg_prompt\":\"梦幻柔光渐变背景,淡紫色调\",\"overlay_prompt\":\"精致银色星点装饰,透明背景\"}]"
}
```
**外部 API 调用示例(通过 Gateway 触发):**
```json
{
"source_image_url": "https://oss.example.com/photo.jpg",
"use_cutout": true,
"preset_codes": "[\"dream\",\"classic\"]",
"render_configs": "[{\"preset_id\":\"dream\",\"grating_config\":{\"sheen_band_angle\":135,\"sheen_intensity\":0.4},\"bg_prompt\":\"...\",\"overlay_prompt\":\"...\"}]"
}
```
3. 单个 preset 测试成功后,改为 5 个 preset 全量测试