- 替换中转站从 xbcl.link 到 weda.cc - prompt 模板改为镭射卡全图生成(去掉 6 层合成/抠图依赖) - 4 路并发调用 + 原图展示 = 5 张 variant - 前端提示词中译英支持 - 全局 Vue errorHandler - WebSocket 鉴权失败跳登录 - 删除已弃用的 laserCompositor 微服务 Co-Authored-By: Claude <noreply@anthropic.com>
575 lines
23 KiB
YAML
575 lines
23 KiB
YAML
app:
|
||
description: 'Laser card AI 镭射卡 5-variant 生成器。前端已抠图,传入 cutout_url 直接合成。'
|
||
icon: 🤖
|
||
icon_background: '#FFEAD5'
|
||
mode: workflow
|
||
name: laser_card_variants_v1
|
||
use_icon_as_answer_icon: false
|
||
kind: app
|
||
version: 0.1.6
|
||
workflow:
|
||
conversation_variables: []
|
||
environment_variables:
|
||
- description: 'laser-compositor 合成服务地址'
|
||
name: LASER_COMPOSITOR_HOST
|
||
value: 'http://host.docker.internal:18090/api/v1/laser'
|
||
value_type: string
|
||
- description: 'MiniMax API 密钥'
|
||
name: MINIMAX_API_KEY
|
||
value: 'sk-api-oezuuNMr5iwPdlJ1JgTJTSzhMhGtaUR5Odjjg0ZqVQ7MoMIqLuE_ginMWRkNiAiDgMY6MvTVkYCWSQ8SK1-LuldrFmohCHxCgZIbxsFYr9zxA8z08Eb8nbo'
|
||
value_type: string
|
||
features:
|
||
file_upload:
|
||
image:
|
||
enabled: false
|
||
number_limits: 3
|
||
transfer_methods:
|
||
- local_file
|
||
- remote_url
|
||
opening_statement: ''
|
||
retriever_resource:
|
||
enabled: false
|
||
sensitive_word_avoidance:
|
||
enabled: false
|
||
speech_to_text:
|
||
enabled: false
|
||
suggested_questions: []
|
||
suggested_questions_after_answer:
|
||
enabled: false
|
||
text_to_speech:
|
||
enabled: false
|
||
language: ''
|
||
voice: ''
|
||
graph:
|
||
edges:
|
||
# start → code-param
|
||
- data:
|
||
isInIteration: false
|
||
isInLoop: false
|
||
sourceType: start
|
||
targetType: code
|
||
id: start-source-code-param-target
|
||
source: start
|
||
sourceHandle: source
|
||
target: code-param
|
||
targetHandle: target
|
||
type: custom
|
||
zIndex: 0
|
||
# code-param → http-bg-all(一次生成 5 张背景图)
|
||
- data:
|
||
isInIteration: false
|
||
isInLoop: false
|
||
sourceType: code
|
||
targetType: http-request
|
||
id: code-param-source-http-bg-all-target
|
||
source: code-param
|
||
sourceHandle: source
|
||
target: http-bg-all
|
||
targetHandle: target
|
||
type: custom
|
||
zIndex: 0
|
||
# code-param → http-overlay-all(一次生成 5 张装饰图)
|
||
- data:
|
||
isInIteration: false
|
||
isInLoop: false
|
||
sourceType: code
|
||
targetType: http-request
|
||
id: code-param-source-http-overlay-all-target
|
||
source: code-param
|
||
sourceHandle: source
|
||
target: http-overlay-all
|
||
targetHandle: target
|
||
type: custom
|
||
zIndex: 0
|
||
# code-param → code-prepare(传递 variants 元数据)
|
||
- data:
|
||
isInIteration: false
|
||
isInLoop: false
|
||
sourceType: code
|
||
targetType: code
|
||
id: code-param-source-code-prepare-target
|
||
source: code-param
|
||
sourceHandle: source
|
||
target: code-prepare
|
||
targetHandle: target
|
||
type: custom
|
||
zIndex: 0
|
||
# http-bg-all → code-prepare
|
||
- data:
|
||
isInIteration: false
|
||
isInLoop: false
|
||
sourceType: http-request
|
||
targetType: code
|
||
id: http-bg-all-source-code-prepare-target
|
||
source: http-bg-all
|
||
sourceHandle: source
|
||
target: code-prepare
|
||
targetHandle: target
|
||
type: custom
|
||
zIndex: 0
|
||
# http-overlay-all → code-prepare
|
||
- data:
|
||
isInIteration: false
|
||
isInLoop: false
|
||
sourceType: http-request
|
||
targetType: code
|
||
id: http-overlay-all-source-code-prepare-target
|
||
source: http-overlay-all
|
||
sourceHandle: source
|
||
target: code-prepare
|
||
targetHandle: target
|
||
type: custom
|
||
zIndex: 0
|
||
# code-prepare → loop-variants
|
||
- data:
|
||
isInIteration: false
|
||
isInLoop: false
|
||
sourceType: code
|
||
targetType: iteration
|
||
id: code-prepare-source-loop-variants-target
|
||
source: code-prepare
|
||
sourceHandle: source
|
||
target: loop-variants
|
||
targetHandle: target
|
||
type: custom
|
||
zIndex: 0
|
||
# loop-variants → code-agg
|
||
- data:
|
||
isInIteration: false
|
||
isInLoop: false
|
||
sourceType: iteration
|
||
targetType: code
|
||
id: loop-variants-source-code-agg-target
|
||
source: loop-variants
|
||
sourceHandle: source
|
||
target: code-agg
|
||
targetHandle: target
|
||
type: custom
|
||
zIndex: 0
|
||
# code-agg → end
|
||
- data:
|
||
isInIteration: false
|
||
isInLoop: false
|
||
sourceType: code
|
||
targetType: end
|
||
id: code-agg-source-end-target
|
||
source: code-agg
|
||
sourceHandle: source
|
||
target: end
|
||
targetHandle: target
|
||
type: custom
|
||
zIndex: 0
|
||
# === 循环内部 ===
|
||
# iter-start → code-call-compositor(直接合成,bg/overlay 已预生成)
|
||
- data:
|
||
isInIteration: true
|
||
iteration_id: loop-variants
|
||
isInLoop: false
|
||
sourceType: start
|
||
targetType: code
|
||
id: iter-start-source-code-call-compositor-target
|
||
source: iter-start
|
||
sourceHandle: source
|
||
target: code-call-compositor
|
||
targetHandle: target
|
||
type: custom
|
||
zIndex: 1000
|
||
nodes:
|
||
# ================================================================
|
||
# 开始节点
|
||
# ================================================================
|
||
- data:
|
||
desc: '接收抠图后的 cutout_url 和 generation 参数'
|
||
selected: false
|
||
title: 开始
|
||
type: start
|
||
variables:
|
||
- label: cutout_url
|
||
max_length: 2048
|
||
options: []
|
||
required: false
|
||
type: text-input
|
||
variable: cutout_url
|
||
- default: '["dream","classic","holoFull","ice","sunset"]'
|
||
label: preset_codes
|
||
max_length: 2048
|
||
options: []
|
||
required: false
|
||
type: text-input
|
||
variable: preset_codes
|
||
- label: render_configs
|
||
max_length: 20480
|
||
options: []
|
||
required: true
|
||
type: text-input
|
||
variable: render_configs
|
||
height: 150
|
||
id: start
|
||
position:
|
||
x: 30
|
||
y: 300
|
||
positionAbsolute:
|
||
x: 30
|
||
y: 300
|
||
selected: false
|
||
sourcePosition: right
|
||
targetPosition: left
|
||
type: custom
|
||
width: 244
|
||
# ================================================================
|
||
# 代码节点 — 参数展开(同时输出合并的 prompt 字符串)
|
||
# ================================================================
|
||
- data:
|
||
code: "import json\n\ndef main(preset_codes: str, render_configs: str) -> dict:\n preset_list = []\n if preset_codes and preset_codes.strip():\n try:\n parsed = json.loads(preset_codes)\n if isinstance(parsed, list):\n preset_list = parsed\n except:\n pass\n if not preset_list:\n preset_list = [\"dream\", \"classic\", \"holoFull\", \"ice\", \"sunset\"]\n configs = []\n if render_configs and render_configs.strip():\n try:\n parsed = json.loads(render_configs)\n if isinstance(parsed, list):\n configs = parsed\n except:\n pass\n config_map = {}\n for rc in configs:\n if isinstance(rc, dict) and \"preset_id\" in rc:\n config_map[rc[\"preset_id\"]] = rc\n variants = []\n for idx, pc in enumerate(preset_list):\n if pc in config_map:\n cfg = config_map[pc]\n variants.append({\n \"preset_id\": pc,\n \"variant_index\": idx,\n \"grating_config\": cfg.get(\"grating_config\", {}),\n \"bg_prompt\": cfg.get(\"bg_prompt\", \"\"),\n \"overlay_prompt\": cfg.get(\"overlay_prompt\", \"\"),\n })\n bg_parts = [v.get(\"bg_prompt\", \"\") or \"\" for v in variants]\n ov_parts = [v.get(\"overlay_prompt\", \"\") or \"\" for v in variants]\n style_names = {\"dream\": \"梦幻\", \"classic\": \"经典\", \"holoFull\": \"全息炫彩\", \"ice\": \"冰晶\", \"sunset\": \"落日\"}\n style_list = [style_names.get(v[\"preset_id\"], v[\"preset_id\"]) for v in variants]\n bg_descs = [bp if bp else f\"{sn}风格渐变背景\" for sn, bp in zip(style_list, bg_parts)]\n ov_descs = [op if op else f\"柔和光晕\" for sn, op in zip(style_list, ov_parts)]\n bg_prompts_all = \" | \".join(bg_descs)\n overlay_prompts_all = \" | \".join(ov_descs)\n return {\n \"variants\": variants,\n \"variant_count\": len(variants),\n \"bg_prompts_all\": bg_prompts_all,\n \"overlay_prompts_all\": overlay_prompts_all,\n }\n"
|
||
code_language: python3
|
||
dependencies: []
|
||
desc: '解析 JSON 字符串 → 构建 variants 列表 + 合并 prompt'
|
||
outputs:
|
||
variant_count:
|
||
children: null
|
||
type: number
|
||
variants:
|
||
children: null
|
||
type: array[object]
|
||
bg_prompts_all:
|
||
children: null
|
||
type: string
|
||
overlay_prompts_all:
|
||
children: null
|
||
type: string
|
||
selected: false
|
||
title: 参数展开
|
||
type: code
|
||
variables:
|
||
- value_selector:
|
||
- start
|
||
- preset_codes
|
||
variable: preset_codes
|
||
- value_selector:
|
||
- start
|
||
- render_configs
|
||
variable: render_configs
|
||
height: 120
|
||
id: code-param
|
||
position:
|
||
x: 320
|
||
y: 300
|
||
positionAbsolute:
|
||
x: 320
|
||
y: 300
|
||
selected: false
|
||
sourcePosition: right
|
||
targetPosition: left
|
||
type: custom
|
||
width: 244
|
||
# ================================================================
|
||
# 前置 HTTP — 一次生成 5 张背景图 (n=5)
|
||
# ================================================================
|
||
- data:
|
||
authorization:
|
||
config:
|
||
api_key: '{{#env.MINIMAX_API_KEY#}}'
|
||
type: bearer
|
||
type: api-key
|
||
body:
|
||
data: '{"model": "image-01", "prompt": "Generate 5 distinct background images based on these style directions. Each background must be a clean empty backdrop (no people, no products, no holographic card objects, no product photography). Laser holographic effects will be applied in post-processing, so focus only on the artistic background direction. Background directions: {{#code-param.bg_prompts_all#}}", "aspect_ratio": "3:4", "n": 5, "prompt_optimizer": true, "response_format": "url"}'
|
||
type: json
|
||
desc: '一次生成 5 种不同风格的背景图(干净背景,镭射效果后处理)'
|
||
headers: 'Content-Type: application/json'
|
||
isInIteration: false
|
||
method: post
|
||
params: ''
|
||
selected: false
|
||
timeout:
|
||
max_connect_timeout: 10
|
||
max_read_timeout: 200
|
||
max_write_timeout: 30
|
||
title: MiniMax 批量背景图
|
||
type: http-request
|
||
url: 'https://api.minimaxi.com/v1/image_generation'
|
||
variables:
|
||
- value_selector:
|
||
- code-param
|
||
- bg_prompts_all
|
||
variable: bg_prompts_all
|
||
height: 93
|
||
id: http-bg-all
|
||
position:
|
||
x: 610
|
||
y: 180
|
||
positionAbsolute:
|
||
x: 610
|
||
y: 180
|
||
selected: false
|
||
sourcePosition: right
|
||
targetPosition: left
|
||
type: custom
|
||
width: 244
|
||
# ================================================================
|
||
# 前置 HTTP — 一次生成 5 张装饰图 (n=5)
|
||
# ================================================================
|
||
- data:
|
||
authorization:
|
||
config:
|
||
api_key: '{{#env.MINIMAX_API_KEY#}}'
|
||
type: bearer
|
||
type: api-key
|
||
body:
|
||
data: '{"model": "image-01", "prompt": "Generate 5 distinct transparent overlay decoration layers. Each overlay must be a clean transparent decoration on empty background (no people, no products, no holographic card objects). Soft glows, micro-particles, light flow effects that enhance atmosphere without occluding main subject. Overlay directions: {{#code-param.overlay_prompts_all#}}", "aspect_ratio": "3:4", "n": 5, "prompt_optimizer": true, "response_format": "url"}'
|
||
type: json
|
||
desc: '一次生成 5 种不同风格的装饰层(透明叠加,不承载主题,镭射效果由 compositor 保证)'
|
||
headers: 'Content-Type: application/json'
|
||
isInIteration: false
|
||
method: post
|
||
params: ''
|
||
selected: false
|
||
timeout:
|
||
max_connect_timeout: 10
|
||
max_read_timeout: 200
|
||
max_write_timeout: 30
|
||
title: MiniMax 批量装饰图
|
||
type: http-request
|
||
url: 'https://api.minimaxi.com/v1/image_generation'
|
||
variables:
|
||
- value_selector:
|
||
- code-param
|
||
- overlay_prompts_all
|
||
variable: overlay_prompts_all
|
||
height: 93
|
||
id: http-overlay-all
|
||
position:
|
||
x: 610
|
||
y: 420
|
||
positionAbsolute:
|
||
x: 610
|
||
y: 420
|
||
selected: false
|
||
sourcePosition: right
|
||
targetPosition: left
|
||
type: custom
|
||
width: 244
|
||
# ================================================================
|
||
# 代码节点 — 将 bg/overlay URLs 与 variants 配对
|
||
# ================================================================
|
||
- data:
|
||
code: "import json\n\ndef main(bg_body: str, overlay_body: str, variants: list) -> dict:\n # 解析 MiniMax 批量响应,提取 5 张图 URL\n bg_data = json.loads(bg_body) if isinstance(bg_body, str) else bg_body\n bg_urls = (bg_data.get(\"data\") or {}).get(\"image_urls\", [])\n ov_data = json.loads(overlay_body) if isinstance(overlay_body, str) else overlay_body\n overlay_urls = (ov_data.get(\"data\") or {}).get(\"image_urls\", [])\n\n enriched = []\n for i, v in enumerate(variants):\n enriched.append({\n \"preset_id\": v.get(\"preset_id\", f\"v_{i}\"),\n \"variant_index\": v.get(\"variant_index\", i),\n \"grating_config\": v.get(\"grating_config\", {}),\n \"bg_prompt\": v.get(\"bg_prompt\", \"\"),\n \"overlay_prompt\": v.get(\"overlay_prompt\", \"\"),\n \"bg_url\": bg_urls[i] if i < len(bg_urls) else \"\",\n \"overlay_url\": overlay_urls[i] if i < len(overlay_urls) else \"\",\n })\n return {\"enriched_variants\": enriched}\n"
|
||
code_language: python3
|
||
dependencies: []
|
||
desc: '解析 MiniMax 批量响应 → 将 5 bg_urls + 5 overlay_urls 配对到每个 variant'
|
||
outputs:
|
||
enriched_variants:
|
||
children: null
|
||
type: array[object]
|
||
selected: false
|
||
title: 批量结果配对
|
||
type: code
|
||
variables:
|
||
- value_selector:
|
||
- http-overlay-all
|
||
- body
|
||
variable: overlay_body
|
||
- value_selector:
|
||
- http-bg-all
|
||
- body
|
||
variable: bg_body
|
||
- value_selector:
|
||
- code-param
|
||
- variants
|
||
variable: variants
|
||
height: 84
|
||
id: code-prepare
|
||
position:
|
||
x: 900
|
||
y: 300
|
||
positionAbsolute:
|
||
x: 900
|
||
y: 300
|
||
selected: false
|
||
sourcePosition: right
|
||
targetPosition: left
|
||
type: custom
|
||
width: 244
|
||
# ================================================================
|
||
# 循环节点(并行 5)
|
||
# ================================================================
|
||
- data:
|
||
desc: '对每个 variant 并行合成最终镭射卡'
|
||
error_handle_mode: terminated
|
||
height: 351
|
||
iterator_selector:
|
||
- code-prepare
|
||
- enriched_variants
|
||
is_parallel: true
|
||
output_selector:
|
||
- code-call-compositor
|
||
- compose_result
|
||
output_type: array[string]
|
||
parallel_nums: 5
|
||
selected: false
|
||
startNodeType: start
|
||
start_node_id: iter-start
|
||
title: 遍历 Variants(并行5)
|
||
type: iteration
|
||
width: 953
|
||
height: 351
|
||
id: loop-variants
|
||
position:
|
||
x: 1200
|
||
y: 300
|
||
positionAbsolute:
|
||
x: 1200
|
||
y: 300
|
||
selected: false
|
||
sourcePosition: right
|
||
targetPosition: left
|
||
type: custom
|
||
width: 953
|
||
zIndex: 1
|
||
# ================================================================
|
||
# 循环内部 — 开始
|
||
# ================================================================
|
||
- data:
|
||
desc: 当前 variant_item(含预生成的 bg_url / overlay_url)
|
||
isInIteration: true
|
||
iteration_id: loop-variants
|
||
selected: false
|
||
title: 循环项
|
||
type: start
|
||
variables: []
|
||
extent: parent
|
||
height: 54
|
||
id: iter-start
|
||
parentId: loop-variants
|
||
position:
|
||
x: 87
|
||
y: 120
|
||
positionAbsolute:
|
||
x: 1287
|
||
y: 420
|
||
selected: false
|
||
sourcePosition: right
|
||
targetPosition: left
|
||
type: custom
|
||
width: 244
|
||
zIndex: 1001
|
||
# ================================================================
|
||
# 循环内部 — Code 节点:调 compositor 合成
|
||
# 直接从 enriched item 读取 bg_url / overlay_url,不再调 MiniMax
|
||
# ================================================================
|
||
- data:
|
||
code: "import json\nimport requests\n\ndef main(\n cutout_url: str,\n item: dict,\n compositor_host: str\n) -> dict:\n \"\"\"\n 从 enriched item 读取预生成的 bg_url / overlay_url,\n 调 laser-compositor /compose 合成镭射卡。\n \"\"\"\n preset_id = item.get(\"preset_id\", \"unknown\")\n variant_index = item.get(\"variant_index\", 0)\n grating_config = item.get(\"grating_config\", {})\n bg_url = item.get(\"bg_url\", \"\")\n overlay_url = item.get(\"overlay_url\", \"\")\n\n compose_body = {\n \"background_url\": bg_url,\n \"cutout_url\": cutout_url,\n \"overlay_url\": overlay_url,\n \"grating_config\": grating_config,\n \"export_width\": 450,\n \"export_height\": 600,\n \"variant_index\": variant_index,\n \"output_oss_key\": f\"laser-card/dify/{preset_id}\",\n }\n\n try:\n resp = requests.post(\n f\"{compositor_host}/compose\",\n json=compose_body,\n timeout=60,\n )\n resp.raise_for_status()\n compose_resp = resp.json()\n except Exception as e:\n return {\"compose_result\": json.dumps({\n \"status\": \"failed\",\n \"error\": f\"compositor call failed: {e}\",\n \"preset_id\": preset_id,\n \"variant_index\": variant_index,\n \"width\": 450,\n \"height\": 600,\n }, ensure_ascii=False)}\n\n compose_resp[\"preset_id\"] = preset_id\n return {\"compose_result\": json.dumps(compose_resp, ensure_ascii=False)}\n"
|
||
code_language: python3
|
||
dependencies:
|
||
- name: requests
|
||
version: 2.32.3
|
||
desc: '用预生成的 bg_url/overlay_url → 调 compositor 合成 → 注入 preset_id'
|
||
outputs:
|
||
compose_result:
|
||
children: null
|
||
type: string
|
||
selected: false
|
||
title: 调 compositor 合成(批量优化版)
|
||
type: code
|
||
variables:
|
||
- value_selector:
|
||
- start
|
||
- cutout_url
|
||
variable: cutout_url
|
||
- value_selector:
|
||
- loop-variants
|
||
- item
|
||
variable: item
|
||
- value_selector:
|
||
- env
|
||
- LASER_COMPOSITOR_HOST
|
||
variable: compositor_host
|
||
extent: parent
|
||
height: 93
|
||
id: code-call-compositor
|
||
parentId: loop-variants
|
||
position:
|
||
x: 957
|
||
y: 120
|
||
positionAbsolute:
|
||
x: 1767
|
||
y: 420
|
||
selected: false
|
||
sourcePosition: right
|
||
targetPosition: left
|
||
type: custom
|
||
width: 244
|
||
zIndex: 1001
|
||
# ================================================================
|
||
# 代码节点 — 聚合输出
|
||
# ================================================================
|
||
- data:
|
||
code: "import json\n\ndef main(loop_output: list) -> dict:\n variants = []\n warnings = []\n for i, item in enumerate(loop_output or []):\n if isinstance(item, str):\n try:\n item = json.loads(item)\n except:\n pass\n if isinstance(item, dict) and item.get(\"status\") == \"succeeded\":\n variants.append({\n \"preset_id\": item.get(\"preset_id\", f\"variant_{i}\"),\n \"oss_key\": item.get(\"oss_key\", \"\"),\n \"signed_url\": item.get(\"signed_url\", \"\"),\n \"width\": item.get(\"width\", 450),\n \"height\": item.get(\"height\", 600),\n })\n else:\n warnings.append(f\"{item.get('preset_id', f'variant_{i}')} 合成失败: {item.get('error', 'unknown')}\")\n return {\n \"status\": \"succeeded\" if len(variants) > 0 else \"failed\",\n \"variants\": json.dumps(variants, ensure_ascii=False),\n \"warnings\": json.dumps(warnings, ensure_ascii=False),\n }\n"
|
||
code_language: python3
|
||
dependencies: []
|
||
desc: '收集循环结果,输出统一 JSON'
|
||
outputs:
|
||
status:
|
||
children: null
|
||
type: string
|
||
variants:
|
||
children: null
|
||
type: string
|
||
warnings:
|
||
children: null
|
||
type: string
|
||
selected: false
|
||
title: 聚合输出
|
||
type: code
|
||
variables:
|
||
- value_selector:
|
||
- loop-variants
|
||
- output
|
||
variable: loop_output
|
||
height: 84
|
||
id: code-agg
|
||
position:
|
||
x: 1610
|
||
y: 300
|
||
positionAbsolute:
|
||
x: 1610
|
||
y: 300
|
||
selected: false
|
||
sourcePosition: right
|
||
targetPosition: left
|
||
type: custom
|
||
width: 244
|
||
# ================================================================
|
||
# 结束节点
|
||
# ================================================================
|
||
- data:
|
||
desc: '返回 5 张镭射卡 signed URL'
|
||
outputs:
|
||
- value_selector:
|
||
- code-agg
|
||
- status
|
||
variable: status
|
||
- value_selector:
|
||
- code-agg
|
||
- variants
|
||
variable: variants
|
||
- value_selector:
|
||
- code-agg
|
||
- warnings
|
||
variable: warnings
|
||
selected: false
|
||
title: 结束
|
||
type: end
|
||
height: 90
|
||
id: end
|
||
position:
|
||
x: 1900
|
||
y: 300
|
||
positionAbsolute:
|
||
x: 1900
|
||
y: 300
|
||
selected: false
|
||
sourcePosition: right
|
||
targetPosition: left
|
||
type: custom
|
||
width: 244
|