388 lines
12 KiB
Markdown
388 lines
12 KiB
Markdown
# Dify 工作流配置文档:`laser_card_variants_v1`
|
||
|
||
> 文件路径: `docs/specs/2026-06-02-dify-laser-card-workflow.md`
|
||
>
|
||
> 本文档是 Dify 平台上 `laser_card_variants_v1` 工作流的完整配置指南。
|
||
|
||
## 工作流概述
|
||
|
||
- **类型:** Workflow(API 触发)
|
||
- **名称:** `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 分支 →** 直接进入循环
|
||
|
||
---
|
||
|
||
### 节点 3:HTTP 节点 — 抠图(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 个子节点(串行执行):
|
||
|
||
---
|
||
|
||
#### 子节点 4a:MiniMax 生成背景图
|
||
|
||
```
|
||
方法: 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。
|
||
|
||
---
|
||
|
||
#### 子节点 4b:MiniMax 生成装饰图
|
||
|
||
```
|
||
方法: 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#}}`
|
||
|
||
---
|
||
|
||
#### 子节点 4c:laser-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 全量测试
|