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.4 workflow: conversation_variables: [] environment_variables: - description: 'laser-compositor 合成服务地址' name: LASER_COMPOSITOR_HOST value: 'http://host.docker.internal:7000' value_type: string - description: 'MiniMax API 密钥' name: MINIMAX_API_KEY value: '' value_type: secret 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 → 参数展开 - 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 # 参数展开 → 循环 - data: isInIteration: false isInLoop: false sourceType: code targetType: iteration id: code-param-source-loop-variants-target source: code-param sourceHandle: source target: loop-variants targetHandle: target type: custom zIndex: 0 # 循环 → 聚合 - 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 # 聚合 → 结束 - 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 → MiniMax 背景 - data: isInIteration: true iteration_id: loop-variants isInLoop: false sourceType: start targetType: http-request id: iter-start-source-http-bg-target source: iter-start sourceHandle: source target: http-bg targetHandle: target type: custom zIndex: 1000 # MiniMax 背景 → MiniMax 装饰 - data: isInIteration: true iteration_id: loop-variants isInLoop: false sourceType: http-request targetType: http-request id: http-bg-source-http-overlay-target source: http-bg sourceHandle: source target: http-overlay targetHandle: target type: custom zIndex: 1000 # MiniMax 装饰 → compositor - data: isInIteration: true iteration_id: loop-variants isInLoop: false sourceType: http-request targetType: http-request id: http-overlay-source-http-compositor-target source: http-overlay sourceHandle: source target: http-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 # ================================================================ # 代码节点 — 参数展开 # ================================================================ - 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 pc in preset_list:\n if pc in config_map:\n cfg = config_map[pc]\n variants.append({\n \"preset_id\": pc,\n \"grating_config\": cfg.get(\"grating_config\", {}),\n \"bg_prompt\": cfg.get(\"bg_prompt\", \"\"),\n \"overlay_prompt\": cfg.get(\"overlay_prompt\", \"\"),\n })\n return {\n \"variants\": variants,\n \"variant_count\": len(variants),\n }\n" code_language: python3 dependencies: [] desc: '解析 JSON 字符串 → 构建 variants 列表' outputs: variant_count: children: null type: number variants: children: null type: array[object] selected: false title: 参数展开 type: code variables: - value_selector: - start - preset_codes variable: preset_codes - value_selector: - start - render_configs variable: render_configs height: 84 id: code-param position: x: 320 y: 300 positionAbsolute: x: 320 y: 300 selected: false sourcePosition: right targetPosition: left type: custom width: 244 # ================================================================ # 循环节点 # ================================================================ - data: desc: '对每个 variant 分别生成背景图、装饰图、合成最终镭射卡' error_handle_mode: terminated height: 351 iterator_selector: - code-param - variants is_parallel: false output_selector: - http-compositor - body output_type: array[string] parallel_nums: 1 selected: false startNodeType: start start_node_id: iter-start title: 遍历 Variants type: iteration width: 953 height: 351 id: loop-variants position: x: 610 y: 300 positionAbsolute: x: 610 y: 300 selected: false sourcePosition: right targetPosition: left type: custom width: 953 zIndex: 1 # ================================================================ # 循环内部 — 开始 # ================================================================ - data: desc: 当前 variant_item 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: 697 y: 420 selected: false sourcePosition: right targetPosition: left type: custom width: 244 zIndex: 1001 # ================================================================ # 循环内部 — MiniMax 背景 # ================================================================ - data: authorization: config: api_key: '{{#env.MINIMAX_API_KEY#}}' type: bearer-api-key body: data: '{"model": "image-01", "prompt": "{{#loop-variants.item.bg_prompt#}}", "aspect_ratio": "3:4", "n": 1, "prompt_optimizer": true, "response_format": "url"}' type: json desc: '根据 bg_prompt 生成 3:4 背景' headers: 'Content-Type: application/json' isInIteration: true iteration_id: loop-variants method: post params: '' selected: false timeout: max_connect_timeout: 10 max_read_timeout: 60 max_write_timeout: 20 title: MiniMax 背景图 type: http-request url: 'https://api.minimaxi.com/v1/image_generation' variables: [] extent: parent height: 93 id: http-bg parentId: loop-variants position: x: 377 y: 120 positionAbsolute: x: 987 y: 420 selected: false sourcePosition: right targetPosition: left type: custom width: 244 zIndex: 1001 # ================================================================ # 循环内部 — MiniMax 装饰 # ================================================================ - data: authorization: config: api_key: '{{#env.MINIMAX_API_KEY#}}' type: bearer-api-key body: data: '{"model": "image-01", "prompt": "{{#loop-variants.item.overlay_prompt#}}", "aspect_ratio": "3:4", "n": 1, "prompt_optimizer": true, "response_format": "url"}' type: json desc: '根据 overlay_prompt 生成透明装饰层' headers: 'Content-Type: application/json' isInIteration: true iteration_id: loop-variants method: post params: '' selected: false timeout: max_connect_timeout: 10 max_read_timeout: 60 max_write_timeout: 20 title: MiniMax 装饰图 type: http-request url: 'https://api.minimaxi.com/v1/image_generation' variables: [] extent: parent height: 93 id: http-overlay parentId: loop-variants position: x: 667 y: 120 positionAbsolute: x: 1277 y: 420 selected: false sourcePosition: right targetPosition: left type: custom width: 244 zIndex: 1001 # ================================================================ # 循环内部 — compositor 合成 # ================================================================ - data: authorization: type: no-auth body: data: '{"background_url": "{{#http-bg.body.data.image_urls[0]#}}", "cutout_url": "{{#start.cutout_url#}}", "overlay_url": "{{#http-overlay.body.data.image_urls[0]#}}", "grating_config": {{#loop-variants.item.grating_config#}}, "export_width": 450, "export_height": 600, "output_oss_key": "laser-card/dify/{{#loop-variants.item.preset_id#}}"}' type: json desc: '调用 laser-compositor /compose 6层合成' headers: 'Content-Type: application/json' isInIteration: true iteration_id: loop-variants method: post params: '' selected: false timeout: max_connect_timeout: 10 max_read_timeout: 60 max_write_timeout: 20 title: 合成 6 层镭射卡 type: http-request url: '{{#env.LASER_COMPOSITOR_HOST#}}/compose' variables: [] extent: parent height: 93 id: http-compositor parentId: loop-variants position: x: 957 y: 120 positionAbsolute: x: 1567 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\"variant_{i} 合成失败\")\n return {\n \"status\": \"succeeded\" if len(variants) > 0 else \"failed\",\n \"variants\": json.dumps(variants),\n \"warnings\": json.dumps(warnings),\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