topfans/docs/superpowers/specs/2026-07-07-laser-card-dify-workflow-refactor-design.md
2026-07-09 16:08:25 +08:00

65 KiB
Raw Blame History

镭射卡 Dify 工作流重构设计v2 — 5 系列 × 主题叙事)

作者Claudebrainstorming with user 创建日期2026-07-07 关联 commit139d91dfeat/len 分支) 状态:草案,待用户审 spec


1. 方案概述(必读)

1.1 要解决的问题

业务问题

  • 当前用户进入镭射卡生成后看到的是"AI 出图但没有金属镭射光效"的 5 张普通图
  • 5 张预设dream/classic/holoFull/ice/sunset都是"金属银 + 单一彩虹带",物理上同质,用户感觉"5 张几乎一样"
  • 用户倾斜手机时,前端无任何视觉反馈(当前缺前端 WebGL tilt 渲染层)

技术问题

  • 前端 useLaserBatchGenerate.js 硬编码 genMode = 'openai',但代码里同时存在 minimax / dify / openai / relay-dify 4 个 provider 分支minimax 是默认 provider 但前端从不触发)
  • OpenAI /v1/images/edits 路径跳过了 compositor 服务,导致 GPT-image 不擅长金属镭射 → 出图无光效
  • 45 风格池(frontend/utils/laser-card/stylePool.js + frontend/utils/laser-card/gacha.js)整套 dead code无业务方调用已删除
  • 后端 compositor 服务(backend/gateway/service/compositor/)有完整的 6 层合成能力,但当前链路不用

1.2 整体实现路径

阶段 时长 内容
Phase 1: 锁定 provider 0.5d 前端 useLaserBatchGenerategenMode = 'dify';后端默认 LASER_GEN_PROVIDER=dify
Phase 2: 搭建 Dify 工作流 1d 按本文档第 5 节配置 laser_card_v2_5series 工作流
Phase 3: 后端适配 0.5d handleDifyBlocking 已存在,只需适配新 variant 字段
Phase 4: 测试 + 灰度 1d 端到端测试 → 灰度切流 → 全量

总工作量:约 3 人日(含 Dify 配置调试)

1.3 关键决策

决策 选择 理由 详细
生成链路 Dify 工作流统一编排 用户选择路径 C编排逻辑可视化便于调优 §3
5 张差异化 5 个卡牌系列 × 主题叙事 工艺 + 主题双重差异;用户视觉一眼可辨 §4
AI 模型(文本) gpt-4o-mini推荐或 minimax-m3备选 gpt-4o-mini 便宜稳定minimax-m3 国内可用但对文化符号理解弱 §5.3
AI 模型(图像) gpt-image-1推荐或 Dify 平台支持的图像模型 gpt-image-1 质量高;如不支持换 SDXL/Replicate Flux/通义万相 §5.4
AI 角色 AI 出"金属底卡"(金属色 + 风格场景,不含彩虹扫光) 彩虹由前端 WebGL 实时叠,保证"不同角度不同色彩"可控、稳定 §3 + §12
前端叠层 WebGL tilt 实时渲染(基于 LenticularEngine 复用现有引擎;用户倾斜手机时色彩随 viewAngle 偏移 §12
后端 compositor 必须保留并增强 AI 出金属底 + 后端 compositor 合成"人像 + 镭射光效",最终成品图 §6.3 + §13
数据库 render_config JSONB 沿用 backdrop_tone 字段(不做 schema 变更) 前端读 backdrop_tone 决定 metal 颜色;新增数据通过现有字段表达 §7

1.4 核心架构图TL;DR

用户上传原图 + 提示词
        ↓
[前端] useLaserDifyGenerate.submit()
        ↓ POST /api/v1/laser/generate
        ↓ body: {cutout_url, preset_codes:[v1..v5], render_configs:[5]}
        ↓ (用户风格描述词由 Dify 工作流 #context# 注入,无需前端传)
[后端] handleDifyBlocking()
        ↓ 调 Dify /v1/workflows/run
[Dify] 工作流 laser_card_v2_5series:
        Step 1: LLM 节点 (gpt-4o-mini) — 扩写 5 个系列化子 prompt
        Step 2: Code 节点 — 拆分 JSON 为 5 变量
        Step 3: 5 路并发生图 (gpt-image-1) — 每路 1 张"金属底 + 主题"图
        Step 4: 结束节点 — 返回 variants 数组
        ↓
[后端] 解析 variants + persistGeneratedInstance
        ↓ 写 laser_card_instances.materials_snapshot
[前端] 5 张图展示 + 用户选 1
        ↓
        ┌────────────────────────────┐
        │ LenticularCard.vue          │
        │  ↓                          │
        │ useLenticularCraftTiltPreview │ ← 陀螺仪 / 触摸 输入
        │  ↓                          │
        │ LenticularEngine (WebGL)    │ ← 计算每帧 rainbow offset
        │  ↓                          │
        │ holographic-shaders         │ ← shader 输出"色带随 viewAngle 漂"
        └────────────────────────────┘
        ↓ 倾斜手机 → 看到不同色彩
[铸造] craftMintSubmit → 标准铸造流程

2. 当前状态分析

2.1 现有代码资产(保留)

文件 用途 处置
backend/gateway/controller/laser_generate_controller.go POST /api/v1/laser/generate 保留,仅调整 handleDifyBlocking 解析逻辑
backend/gateway/repository/laser_card_repository.go DB CRUD 保留
backend/pkg/models/laser_card.go 模型定义 保留
frontend/composables/useLaserDifyGenerate.js 前端主调用 保留,调整 render_configs 数量
frontend/utils/laser-card/laserPresets.js 5 套 preset 定义 保留,但改名/调整
frontend/utils/laser-card/laserGrating.js grating 参数 保留grating_config 字段兼容)

2.2 现有代码资产(废弃)

文件 处置 commit
4 个 provider 中的 minimax/openai/relay-dify 分支 降级为注释,保留 dify 本次 PR
backend/gateway/service/compositor/*.go 标记 DEPRECATED 后续 PR
backend/gateway/controller/compositor_controller.go 标记 DEPRECATED 后续 PR
backend/gateway/service/openai_client.go 保留(兼容字段),但激光卡不再调 本次 PR

3. Dify 工作流设计

3.1 工作流基本信息

字段
名称 laser_card_v2_5series
类型 WorkflowAPI 触发)
触发方式 HTTP POST /v1/workflows/run
预计耗时 30-60 秒5 路 AI 生图并发)
输出 JSON 数组 variants[5] + warnings + cutout_url

3.2 工作流节点全景图

完整可视化流程图见:dify-laser-workflow.drawio

[开始] → [LLM: 扩写 prompt] → [Code: 拆分 JSON]
                                         ↓
                            并发 5 路(每路 1 个系列)
                                         ↓
        ┌────────┬────────┬────────┬────────┐
        ↓        ↓        ↓        ↓        ↓
     [生图1] [生图2] [生图3] [生图4] [生图5]
     Prizm  Pokemon Topps  YuGiOh  Weiss
     Blue   Rainbow Refr.  Secret  SSP
        ↓        ↓        ↓        ↓        ↓
        └────────┴────────┴────────┴────────┘
                         ↓
                  [结束: 输出 variants]

3.3 节点清单10 个)

# 类型 名称 输入 输出
1 开始 start cutout_url用户主题由 Dify 原生 #context# 注入)
2 LLM llm_rewrite #context#user message JSON: 5 个系列 prompt
3 跳过Dify 自动拆 JSON见 §5.3
4 生图 gen_prizm_blue {{ llm_rewrite.prizm_blue}} + cutout_url img_1 (signed_url)
5 生图 gen_pokemon_rainbow {{ llm_rewrite.pokemon_rainbow}} + cutout_url img_2
6 生图 gen_topps_refractor {{ llm_rewrite.topps_refractor}} + cutout_url img_3
7 生图 gen_yugioh_secret {{ llm_rewrite.yugioh_secret}} + cutout_url img_4
8 生图 gen_ws_ssp {{ llm_rewrite.ws_ssp}} + cutout_url img_5
9 代码 code_merge 5 个 img URL variants JSON 数组
10 结束 end variants, warnings, cutout_url

4. 5 个卡牌系列定义

4.1 系列与 preset_id 对应

preset_id 中文名 family 工艺特征 metal tone sheen angle foil
prizm_blue 蓝棱镜 panini_prizm 电光蓝宝石 + 锐利反射 冷银 #A8ACB2 120° 0.70
pokemon_rainbow 宝可梦彩虹 pokemon_tcg 全光谱彩虹 + 雪花点 珍珠银 #B0ACA6 100° 0.90
topps_refractor Topps 经典 topps_chrome 单彩虹带 + 复古胶片 冷银 #A8ACB2 125° 0.68
yugioh_secret 游戏王 Secret yugioh 全卡横纹扫光 珍珠银 #B0ACA6 90° 0.90
ws_ssp WS SSP weiss 日系终极收藏 + 全卡 rainbow 冷银 #A8ACB2 95° 0.95

4.2 Dify 数据库初始化

Dify 工作流的 laser_card_templates 表需要同步 5 条记录(与前端 preset_id 对齐。SQL 见 §7。


5. Dify 工作流节点配置(小白填写指南)

重要:以下每个节点都标注了"在 Dify 平台怎么找这个节点"。如果你找不到对应类型,问 Dify 平台客服或看他们的官方文档。

5.1 节点 1开始节点

在 Dify 平台:新建工作流时默认就有;如果已有工作流,在画布左上角点"+",选"开始"。

填写步骤

  1. 节点标题:开始(默认)
  2. 在"输入变量"区域,点"+ 添加变量"只添加 1 个自定义变量(用户主题用 Dify 原生 #context# 注入,不要添加自定义变量):
变量名 类型 必填 默认值 说明
cutout_url 文本 (string) 用户原图的 OSS signed URL
  1. 用户风格描述词不要做变量Dify 工作流运行时,平台会自动把"用户当前输入消息"注入为 #context# 特殊变量。在 LLM 节点 user prompt 字段直接填 #context# 即可(无需自定义变量)。

  2. 点"保存"按钮

常见错误

  • 添加了 user_prompt 自定义变量 → 多余且容易混淆LLM 会拿到重复信息)
  • 必填勾错:所有变量都勾必填会阻止测试运行

5.2 节点 2LLM 节点(关键节点)

在 Dify 平台:点画布"+",搜索"LLM",选第一个(带大模型图标的)。

填写步骤

  1. 节点标题llm_rewrite(任意,方便引用)
  2. 模型:下拉选 gpt-4o-mini(便宜、快速;如果你想质量更高可改 gpt-4o
  3. 系统提示 (System Prompt):粘贴下面整段 ↓
你是镭射卡视觉设计师 + 卡牌工艺专家。

【输入说明】用户输入的"风格描述词"会通过 Dify 的 #context# 特殊变量传入到下方 user message 字段。
用户的输入是"镭射卡想要的氛围/风格",例如"梦幻樱花"、"星空极光"、"赛博朋克"、"蒸汽朋克"。
你的任务是:读 user message 中的风格词,把它嵌入到下方 5 个系列的模板中(替换 `{主题}` 占位符)。
每个 prompt 对应一种真实存在的卡牌工艺系列5 张图都是同一风格、不同工艺底,让用户对比"哪个光效最适合"。

【关键】每张图只需要出"金属底 + 风格氛围",不要出彩虹扫光带!
彩虹扫光带由前端 WebGL 根据用户倾斜角度实时叠加,与 AI 出的图无关。
AI 出图只需要保证:金属底色 + 风格氛围(场景/色彩/质感),让前端去做"光感"。

【模板占位符约定】下方 5 个系列模板中的 `{主题}` 是占位符,**你在输出时必须把 `{主题}` 替换为 user message 中的实际风格词**,不要保留 `{主题}` 字面量。

## 【系统 prompt 内部小节】5 个卡牌系列(每张的金属底 + 工艺特征严格对齐下方)

> **针对 MiniMax image-01 优化版**:由于 MiniMax 不支持 Negative Prompt、且对"金属 + 镭射"理解较弱,模板增加了**视觉强约束词**和**摄影术语**以提升出图质量。

### Series 1: Prizm Blue (美式运动卡蓝棱镜)
- 金属底:冷银 #A8ACB2偏蓝宝石色
- 主题特征:锐利高对比、运动卡质感、人像居中、运动场景元素
- bg_prompt 模板:"{主题}, on cool silver #A8ACB2 brushed metal card background, electric blue sapphire accent, sharp reflective sport card aesthetic, centered subject, 50mm lens, photorealistic, 4k, detailed, no text, no people, no watermark"

### Series 2: Pokemon Rainbow Rare (宝可梦全谱彩虹)
- 金属底:珍珠银 #B0ACA6柔和高亮
- 主题特征:日系动漫风、可爱人物、闪闪发光背景、精致细节
- bg_prompt 模板:"{主题}, on pearl silver #B0ACA6 lustrous metal card background, soft luminous anime glow, premium Japanese TCG aesthetic, centered chibi-style subject, vibrant pastel sparkles, 4k illustration, detailed, no text, no people, no watermark"

### Series 3: Topps Refractor (棒球经典折射)
- 金属底:冷银 #A8ACB2复古胶片颗粒
- 主题特征:复古运动卡、肖像构图、专业感、暗角
- bg_prompt 模板:"{主题}, on cool silver #A8ACB2 vintage chrome card background, vintage baseball card aesthetic, soft film grain texture, classic portrait composition, vignette edges, analog warmth, professional studio lighting, 4k, photorealistic, no text, no people, no watermark"

### Series 4: Yu-Gi-Oh Secret (游戏王 Secret 横纹)
- 金属底:珍珠银 #B0ACA6全卡面爆闪
- 主题特征:日系动漫、神秘角色、魔法阵元素、动态构图
- bg_prompt 模板:"{主题}, on pearl silver #B0ACA6 full-card luminous metal background, dynamic horizontal stripe holographic shine, magic circle sigils beneath, dynamic action pose, anime TCG secret-tier aesthetic, 4k illustration, detailed, no text, no people, no watermark"

### Series 5: Weiss SSP (WS 终极签名)
- 金属底:冷银 #A8ACB2极致金属光泽
- 主题特征:日系动漫终极收藏、签名感、豪华边框感
- bg_prompt 模板:"{主题}, on cool silver #A8ACB2 premium polished metal card background, signature-style framed composition, anime collectible apex aesthetic, luxurious engraved border details, ultra-polished mirror surface, 4k illustration, detailed, no text, no people, no watermark"

### 优化点说明MiniMax 适配)

| 改动 | 原因 |
|---|---|
| 加 `brushed metal / lustrous / vintage chrome / polished` | 强调物理金属质感MiniMax 对此敏感) |
| 加 `50mm lens / photorealistic / 4k / detailed` | 摄影/质量术语提升写实度 |
| 加 `centered subject` | 强制主体居中(避免 AI 随意构图) |
| 加 `vignette edges / analog warmth`Topps | 复古感细节 |
| 加 `vibrant pastel sparkles`Pokemon | 闪光粒子细节 |
| 加 `dynamic horizontal stripe`YGO | 横纹扫光AI 能直接渲,不用前端叠) |
| 加 `engraved border details`WS | 雕刻边框细节 |

**前提**:因为 MiniMax 不支持 Negative Prompt所以"no text, no people, no watermark"必须塞进**每个 prompt 末尾**作为正向约束LLM 强制约束已要求)。

## 【prompt 内部】输出格式(严格 JSON
{
  "prizm_blue":      "<英文 prompt>",
  "pokemon_rainbow": "<英文 prompt>",
  "topps_refractor": "<英文 prompt>",
  "yugioh_secret":   "<英文 prompt>",
  "ws_ssp":          "<英文 prompt>"
}

## 【prompt 内部】强制约束
1. 用户输入的"风格描述词"必须出现在 5 个 prompt 里(替换 `{主题}` 占位符)
2. 中文风格词翻译成地道的英文后再嵌入(如"梦幻樱花" → "dreamy cherry blossom""星空极光" → "starry aurora""赛博朋克" → "cyberpunk neon city"
3. 每个 prompt 末尾必须包含 "no text, no people, no watermark"
4. 金属底色必须严格对齐上方 hex 数值(#A8ACB2 / #B0ACA6
5. 5 个 prompt 之间必须有明显的视觉差异(不能换汤不换药)
6. 【严禁】出现 "holographic / rainbow / sheen / foil / refractive / prismatic / iridescent" 等词
   (前端 WebGL 会处理这些AI 不要画蛇添足)
7. 禁止 "flat / matte / dull / cartoon / drawing / 2D / sketch" 等削弱金属感的词
8. 必须强调金属感:使用 "metallic / lustrous / polished / brushed metal / chrome / silver substrate" 等词
  1. 用户提示 (User Prompt):填 #context#Dify 原生特殊变量,不是 {{...}} 形式)
    • 怎么填:直接在用户提示框打 #context#Dify 平台会识别这个特殊变量,自动注入用户当前消息)
    • 也可写:用户的主题是:#context#(更明确)
  2. 输出格式:选 JSON(重要!不选 JSON 后续 Code 节点没法解析)
  3. 点"保存"

常见错误

  • 输出格式忘记选 JSON → 后面的 Code 节点解析失败
  • 模板占位符没有让 LLM 替换(如保留 {主题} 字面量输出)→ 生图节点会拿到 {主题} 字符串而报错
  • 模型选了太贵的(如 gpt-4-vision-preview→ 费用爆炸
  • 没限定用户输入是"风格/氛围词" → 用户可能输入 IP 名(如 PokemonAI 不知道怎么处理

5.3 节点 3跳过(当前生产方案)

当前生产方案:不创建代码节点 3让 5 个生图节点直接引用 LLM 的 JSON 子字段。完全避开 Dify sandbox(当前线上 sandbox 0.2.15 有 DifySeccomp abort bugPython 代码节点无法运行)。

连线

[LLM llm_rewrite] (输出格式=JSON)
   ↓
   ├─→ [生图 4] prompt = {{ llm_rewrite.prizm_blue}}
   ├─→ [生图 5] prompt = {{ llm_rewrite.pokemon_rainbow}}
   ├─→ [生图 6] prompt = {{ llm_rewrite.topps_refractor}}
   ├─→ [生图 7] prompt = {{ llm_rewrite.yugioh_secret}}
   └─→ [生图 8] prompt = {{ llm_rewrite.ws_ssp}}

Dify 平台操作步骤

  1. 配置 LLM 节点:在 llm_rewrite 节点编辑界面:

    • 系统提示字段:粘贴 spec §5.2 完整中文 prompt

    • 用户提示字段:填 #context#

    • 滚到下方"输出变量"区域,打开"结构化输出"开关

    • 在 JSON Schema 输入框粘贴:

      {
        "type": "object",
        "properties": {
          "prizm_blue":      { "type": "string" },
          "pokemon_rainbow": { "type": "string" },
          "topps_refractor": { "type": "string" },
          "yugioh_secret":   { "type": "string" },
          "ws_ssp":          { "type": "string" }
        },
        "required": ["prizm_blue", "pokemon_rainbow", "topps_refractor", "yugioh_secret", "ws_ssp"]
      }
      
  2. 修改每个生图节点的 Prompt 字段(用变量选择器选,不要手敲):

节点 Prompt 字段
gen_prizm_blue / 选 → llm_rewrite.structured_output.prizm_blue
gen_pokemon_rainbow llm_rewrite.structured_output.pokemon_rainbow
gen_topps_refractor llm_rewrite.structured_output.topps_refractor
gen_yugioh_secret llm_rewrite.structured_output.yugioh_secret
gen_ws_ssp llm_rewrite.structured_output.ws_ssp
  1. 不要创建任何代码节点
  2. 保存 + 测试

为什么这样能跑通

  • ✓ 完全不调 Python sandbox → 避开 DifySeccomp abort bug
  • ✓ Dify 1.x 结构化输出自动 parse → 用平台验证过的功能
  • ✓ 不需要 import json / json.loads → 减少代码出错面

前提条件

  • LLM 节点系统提示是 spec §5.2 完整中文 prompt
  • LLM 节点打开"结构化输出"开关
  • 用户提示字段填 #context#

常见错误

  • LLM 节点没打开"结构化输出" → 下游拿不到字段,路径语法失败
  • LLM 系统提示用了对话式英文模板(不是 spec §5.2 中文版)→ LLM 输出问句而不是 JSON
  • 路径语法写错:llm_rewrite.structured_output.prizm_blue 是正确的,不是 .json.
  • JSON Schema 字段名拼错:必须严格是 prizm_blue / pokemon_rainbow

5.3.1 备用方案:用代码节点(沙箱修复后启用)

何时启用:等 Dify sandbox 升级到 0.2.16+ 或 Dify 平台提供新 sandbox 镜像Python 代码节点能稳定运行后,再启用此方案作为冗余。

当前状态:仅作参考,不要在生产中部署(线上 sandbox 0.2.15 会 abort

实现思路

在 LLM 节点和生图节点之间加一个 code_split 代码节点:

import json

def main(llm_output: str) -> dict:
    cleaned = llm_output.strip()
    if cleaned.startswith("```"):
        cleaned = cleaned.split("```")[1]
        if cleaned.startswith("json"):
            cleaned = cleaned[4:]
        cleaned = cleaned.strip()
    data = json.loads(cleaned)
    return {
        "p1": data.get("prizm_blue", ""),
        "p2": data.get("pokemon_rainbow", ""),
        "p3": data.get("topps_refractor", ""),
        "p4": data.get("yugioh_secret", ""),
        "p5": data.get("ws_ssp", ""),
    }

输出 5 个变量 p1 ~ p5,生图节点引用 {{ code_split.p1}} ... {{ code_split.p5}}

操作

  1. 在 LLM 节点配置界面勾选**"输出格式 = JSON"**Dify 会自动 parse
  2. 在生图节点 4 的 Prompt 字段打:{{ llm_rewrite.prizm_blue}}
  3. 其他 4 个生图节点类似,分别引用不同 key
  4. 删掉原来的 code_split 节点
  5. 测试运行

为什么能跑通

  • 完全不调 Python sandbox → 避开 seccomp bug
  • 不需要 import json / json.loads → 减少代码出错面
  • Dify 平台自身负责 JSON parse → 用平台经过验证的功能

常见错误

  • 路径语法写错:llm_rewrite.json.prizm_blue 是正确的,不要写 llm_rewrite.prizm_blue
  • LLM 节点没勾"输出格式=JSON"→ Dify 不会自动 parse路径语法失败
  • LLM 偶尔返回 null 字段 → 生图节点拿到空字符串AI 出图失败;建议在 system prompt 加强约束

5.4 节点 4-85 个生图节点HTTP 请求节点方案)

为什么用 HTTP 请求节点而不是 Dify 图像生成插件

  • Dify 的 langgenius/minimax 插件只支持 llm/text-embedding/tts不支持 image_generation
  • 即使插件能调 MiniMax LLM也调不到 MiniMax image_generation 端点(图生图 API
  • HTTP 请求节点 直接 POST https://api.minimaxi.com/v1/image_generation,传 subject_reference 参数实现图生图

在 Dify 平台:点"+",搜索 "HTTP 请求" / "HTTP Request",选第一个插件。

通用填写步骤(以节点 4 为例,节点 5-8 改 prompt 字段):

节点 4HTTP 请求节点 1Prizm Blue

  1. 节点标题gen_prizm_blue
  2. 方法POST
  3. URLhttps://api.minimaxi.com/v1/image_generation
  4. HeadersJSON
{
  "Authorization": "Bearer {{ env.MINIMAX_API_KEY}}",
  "Content-Type": "application/json"
}

⚠️ 在 Dify 控制台"工具 → MiniMax 凭据"页面把 API Key 配置为环境变量 MINIMAX_API_KEY,不要硬编码到节点里。

  1. BodyJSON
{
  "model": "image-01-live",
  "prompt": "{{ llm_rewrite.prizm_blue}}",
  "subject_reference": [
    {
      "type": "character",
      "image_file": "{{ start.cutout_url}}"
    }
  ],
  "aspect_ratio": "1:1",
  "n": "1",
  "response_format": "url"
}

关键参数:subject_reference[].type = "character" + image_file = 抠图 URL,这是 MiniMax 图生图 API 的标准用法。

  1. 超时60 秒
  2. 错误处理保持默认Stop on Error
  3. 点"保存"

节点 5-8复制节点 4 修改

每个节点的差异只改 prompt 字段

节点 标题 Body prompt 字段
5 gen_pokemon_rainbow {{ llm_rewrite.pokemon_rainbow}}
6 gen_topps_refractor {{ llm_rewrite.topps_refractor}}
7 gen_yugioh_secret {{ llm_rewrite.yugioh_secret}}
8 gen_ws_ssp {{ llm_rewrite.ws_ssp}}

Dify 平台操作

  • 节点 4 配置完 → 节点右键 → "复制"
  • 改标题 + 改 prompt 字段(共复制 4 次)

响应解析(用于下游汇总节点)

MiniMax API 响应格式:

{
  "data": {
    "image_urls": ["https://...signed_url...", "..."]
  },
  "id": "03ff3cd0820949eb8a410056b5f21d38",
  "base_resp": {"status_code": 0, "status_msg": "success"}
}

取第 1 张图 URL 的引用(用于下游汇总节点):

{{ gen_prizm_blue.data.image_urls[0] }}

常见错误

  • API Key 写错 → 报 2049 / 1004 状态码(鉴权失败)
  • subject_reference 类型写成 "human" 而非 "character" → 报 2013 状态码(参数错误)
  • prompt 长度 > 1500 字符 → 报 2013 状态码
  • image_file URL 失效OSS signed URL 过期)→ AI 看图失败

5.4.1 备选:使用其他支持图生图的模型

如果不用 MiniMax HTTP 请求节点方案,可以用:

模型 适配 Dify 插件 配置方式
gpt-image-1OpenAI langgenius/openai 或官方 OpenAI 插件 节点用 Dify 图像生成插件直接选 gpt-image-1prompt 字段 + image 字段(传 cutout_url
Replicate Flux langgenius/replicate 配置 Replicate API token模型选 flux-dev,支持图生图
SDXL + img2img langgenius/sdxl 插件 选 img2img 模型,加 init_image 字段

5.4.2 不推荐:保留 MiniMax 纯文生图(无图生图)

如果不想改 Dify 工作流、不想装新插件,可以接受:

  • AI 只出"金属底 + 风格氛围"图(纯背景,无人像)
  • 用户上传的人像不会被嵌入
  • 前端 LenticularCard 显示时用人像覆盖背景图(临时方案)

这种方案用户体验差,不建议生产使用。


5.5 节点 9代码节点汇总

在 Dify 平台:同 §5.3,再加一个 Python 节点。

填写步骤

  1. 节点标题code_merge
  2. 输入变量:依次添加 5 个生图节点的输出变量(每个节点的图片 URL
  3. 代码
import json

def main(gen1: str, gen2: str, gen3: str, gen4: str, gen5: str) -> dict:
    """
    输入: 5 个生图节点的图片 URL
    输出: variants 数组(与后端契约对齐)
    """
    variants = [
        {"preset_id": "prizm_blue",      "oss_key": "", "signed_url": gen1},
        {"preset_id": "pokemon_rainbow", "oss_key": "", "signed_url": gen2},
        {"preset_id": "topps_refractor", "oss_key": "", "signed_url": gen3},
        {"preset_id": "yugioh_secret",   "oss_key": "", "signed_url": gen4},
        {"preset_id": "ws_ssp",          "oss_key": "", "signed_url": gen5},
    ]
    return {
        "variants": json.dumps(variants, ensure_ascii=False),
        "warnings": json.dumps([], ensure_ascii=False),
    }
  1. 点"保存"

5.6 节点 10结束节点

在 Dify 平台:点"+",选"结束"。

填写步骤

  1. 节点标题end(默认)
  2. 输出变量
变量名 类型 引用
variants 文本 (string) {{ code_merge.variants}}
warnings 文本 (string) {{ code_merge.warnings}}
cutout_url 文本 (string) {{ start.cutout_url}}
  1. 点"保存"

5.7 节点连线

在 Dify 画布上拖拽连线:

start → llm_rewrite → (5 路分支)
                                       ├──→ gen_prizm_blue ─┐
                                       ├──→ gen_pokemon_rainbow ─┤
                                       ├──→ gen_topps_refractor ─┼→ code_merge → end
                                       ├──→ gen_yugioh_secret ─┤
                                       └──→ gen_ws_ssp ─┘

执行模型(重要)

  • 5 个生图节点是 DAG 上的独立分支Dify 调度器并行执行它们(不是顺序)
  • 总耗时 ≈ max(5 路) ≈ 10-30 秒(不是累加 50-150 秒)
  • 任一节点失败 → 整个工作流立即终止fail-fast未配置 Continue
  • 工作流总超时 = 默认 60 秒(任意节点超过即挂)

Dify 平台操作

  • 鼠标悬停在节点边缘,会出现连线点,拖到下一个节点即可
  • 5 个生图节点都连到 code_merge 的输入

5.8 测试运行

  1. 点画布右上角"调试"按钮
  2. 在"用户输入"框直接输入风格描述词(如"梦幻樱花"Dify 会自动注入为 #context#
  3. 如果用 JSON 输入测试数据:
{
  "#context#": "梦幻樱花",
  "cutout_url": "https://example.com/test-cutout.png"
}
  1. 点"开始运行"

  2. 查看每个节点的输出:

    • LLM 节点:应该是 5 个 prompt 的 JSON耗时 ~3s
    • Code 拆分:应该是 5 个独立变量(耗时 < 1s
    • 5 个生图节点:并行执行,每张图大概需要 10-30 秒,总耗时 = 最慢那一路
    • 结束节点variants 数组里有 5 个 URL
  3. 如果某节点报错,看节点上的红色叹号,查看错误信息:

    • LLM/Code 节点报错 → 工作流立即挂掉,无 variants 输出
    • 任一生图节点报错 → 整个工作流立即挂掉fail-fast即使其他 4 路成功了也算白跑
    • 失败原因记录在工作流日志里,可点击节点查看详细错误

5.9 参数附录(小白填表清单)

本节把 §5.1-5.8 里散落的参数汇总成一张"填表清单",照着填就行。

5.9.1 AI 在哪步填写(关键)

整个工作流里两次填 AI,分别在两个不同节点:

节点 填什么 AI 任务
节点 2LLM 节点 gpt-4o-mini(文本模型) 把用户 1 个 prompt 扩写为 5 个英文子 prompt
节点 4-85 个生图节点 gpt-image-1(图像模型) 每个节点根据 1 个子 prompt 生成 1 张图

两次 AI 不能合并:文本模型不会画图,图像模型不会读 prompt 拆解。所以必须 2 类节点。

5.9.2 节点 1 · 开始Start

无需参数配置,只设输入变量

参数 填什么 说明
变量名 cutout_url 类型"文本",勾必填
变量名 user_prompt 不添加——Dify 原生 #context# 会自动注入用户风格描述词

5.9.3 节点 2 · LLM 节点(填文本 AI

参数 说明
模型 (Model) gpt-4o-mini 【AI 在这填】 便宜 + 够用;想更稳可改 gpt-4o(贵 30 倍)
温度 (Temperature) 0.7 让 5 个 prompt 有差异化太低0.1)会 5 个几乎一样
最大 Token (Max Tokens) 2000 5 个 prompt 共 ~1500 字符 + buffer
Top P 0.9
频率惩罚 0.0
存在惩罚 0.0
系统提示 (System Prompt) [粘贴 §5.2 的整段 prompt] 关键
用户提示 (User Prompt) #context#Dify 原生特殊变量) 直接打 #context# 字面量Dify 平台自动注入用户风格描述词
输出格式 (Output Format) JSON 必选 JSON,否则 Code 节点解析失败

5.9.4 节点 3 · 代码节点(拆分 JSON

参数 说明
执行语言 Python 3 不要选 Node.js / Go
输入变量 (Input Variables) llm_output 引用 llm_rewrite 节点输出(一般叫 llm_rewrite.textllm_rewrite.output
代码 (Code) [粘贴 §5.3 的 Python 代码]

5.9.5 节点 4-8 · 5 个生图节点(填图像 AI

每个节点结构相同,参数差异只在 Prompt 引用。

节点 4gen_prizm_blue配置示例

参数 说明
模型 (Model) gpt-image-1 【AI 在这填】
图片尺寸 (Size) 1024 × 1024 方形,裁切到 450×600 不变形
生成数量 (N) 1 每路 1 张
质量 (Quality) high vivid 比 natural 更适合"彩虹金属"风格
风格 (Style) vivid
提示词 (Prompt) {{ llm_rewrite.prizm_blue}} / 选变量
负向提示 (Negative Prompt) text, people, watermark, logo, signature, flat, matte, 2D, cartoon 必填,否则 AI 容易出文字/水印

5 个节点的差异(只改 Prompt 引用)

节点 标题 Prompt 引用 preset_id
4 gen_prizm_blue {{ llm_rewrite.prizm_blue}} prizm_blue
5 gen_pokemon_rainbow {{ llm_rewrite.pokemon_rainbow}} pokemon_rainbow
6 gen_topps_refractor {{ llm_rewrite.topps_refractor}} topps_refractor
7 gen_yugioh_secret {{ llm_rewrite.yugioh_secret}} yugioh_secret
8 gen_ws_ssp {{ llm_rewrite.ws_ssp}} ws_ssp

Dify 平台操作:节点 4 配置完 → 右键 → 复制 → 改标题和 Prompt 引用(共复制 4 次)

备选图像模型Dify 平台没有 gpt-image-1 时):

模型 参数差异 备注
dall-e-3 size 改 1024×1024 / 1792×1024 / 1024×1792;无 Style OpenAI 经典款;不支持图生图
stable-diffusion-xl negative_prompt 字段;加 guidance_scale: 7.5 开源;支持图生图
Replicate flux-dev num_inference_steps: 28width / height 开源高质量;支持图生图
MiniMax image-01 / image-02 模型字段选 image-01size 支持 1024×1024 / 768×1280Negative Prompt 不支持(隐含规则);不支持图生图 国内主流(你当前用)
腾讯混元 / 阿里通义万相 按平台文档填 国内替代

gpt-image-1 配置(如果你有 OpenAI key推荐

模型gpt-image-1
Prompt{{ llm_rewrite.prizm_blue}}
图片尺寸1024x1024
生成数量1
【关键】image 字段:{{ start.cutout_url}}    ← 这个让 AI 把人像融入背景

为什么推荐 gpt-image-1

  • ✓ 同时支持文生图 + 图生图image field 直接传原图 URL
  • ✓ 质量最高OpenAI 训练数据最强)
  • ✓ 输出包含人像 + 金属底 + 风格氛围(一次成型)
  • ✗ 但需要 OpenAI API key国内访问需要代理

MiniMax image-01 配置示例(你当前用,纯文生图):

模型image-01        ← 不是 gpt-image-1
Prompt{{ llm_rewrite.prizm_blue}}
图片尺寸1024x1024
生成数量1
Negative PromptMiniMax 不支持,留空)
【没有 image 字段】← MiniMax 是纯文生图

Dify 插件选择

  1. 点"+"搜索"图像生成"或"image generation"
  2. MiniMax 文生图langgenius/minimax/minimax-image 插件)—— 不是 langgenius/minimax/minimaxLLM 插件)
  3. 配置 MiniMax API key在 MiniMax 开放平台 https://api.minimax.chat 申请)
  4. 模型选 image-01 或更新的 image-02

与 gpt-image-1 的差异

gpt-image-1 MiniMax image-01
尺寸选项 1024x1024 / 1024x1536 / 1536x1024 / auto 1024x1024 / 768x1280
Negative Prompt 支持 不支持(隐含规则)
风格 (style) vivid / natural 无(不支持)
出图速度 ~10-30s/张 ~15-40s/张
质量 高(写实/动漫都好) 中(偏动漫风格化)
费用 ~¥0.5-1/张 ~¥0.1-0.3/张
图生图(图+文) 支持image field 不支持
人像融合 AI 直接处理 需后端 compositor 后期叠加

注意 1:因为 MiniMax 不支持 Negative Promptspec §5.4 里说的"Negative Prompt 字段填 text, people, watermark..." 实际对 MiniMax 无效。需要在 LLM 系统提示的强制约束里强调这些词LLM 会让生成的 prompt 本身符合"无文字无人"要求。

注意 2:因为 MiniMax 不支持图生图,当前链路生成的图没有人像。如果要"人像 + 镭射背景"融合效果,必须:

  • 换 gpt-image-1推荐
  • 或保留 MiniMax + 用后端 compositor 服务把"AI 出图 + 用户人像"合成(要重启 compositor

5.9.6 节点 9 · 代码节点(汇总)

参数
执行语言 Python 3
输入变量 (Input Variables) gen1 / gen2 / gen3 / gen4 / gen5(引用 5 个生图节点输出)
代码 (Code) [粘贴 §5.5 的 Python 代码]

5.9.7 节点 10 · 结束End

输出变量 类型 引用
variants 文本 (string) {{ code_merge.variants}}
warnings 文本 (string) {{ code_merge.warnings}}
cutout_url 文本 (string) {{ start.cutout_url}}

5.9.8 总览:你要在 Dify 平台填的所有内容

必填 AI2 类)

节点 AI 模型 必填项
节点 2 gpt-4o-mini 模型 + 系统 prompt + 用户 prompt + 输出格式=JSON
节点 4-8×5 gpt-image-1 模型 + 尺寸 + 数量 + Prompt + 负向 Prompt

必填变量引用10 个 → 改为 7 个,因为不再用 user_prompt 自定义变量)

节点 引用变量
开始 cutout_url(只定义 1 个;用户主题由 Dify 原生 #context# 注入)
LLM #context#(在 user prompt 字段直接打 #context#
代码 split llm_rewrite.text(或 .output
生图 1-5 {{ llm_rewrite.prizm_blue}} ... {{ llm_rewrite.ws_ssp}}
代码 merge gen1-gen55 个生图节点输出)
结束 {{ code_merge.variants}} / {{ code_merge.warnings}} / {{ start.cutout_url}}

必填代码2 段 Python

  • §5.3 JSON 拆分代码(节点 3
  • §5.5 variants 汇总代码(节点 9

5.9.9 最常见的 3 个坑

  1. 模型选错
    • LLM 节点选了 gpt-image-1(图像模型不会输出 JSON
    • 生图节点选了 gpt-4o(文本模型不会画图)
  2. Prompt 引用忘记改5 个生图节点复制后Prompt 引用没改 → 5 张图一模一样
  3. 输出格式忘选 JSONLLM 返回 markdown ```json ``` 包起来的字符串Code 节点解析失败

6. 后端适配

6.1 handleDifyBlocking 改动

文件backend/gateway/controller/laser_generate_controller.go

现状L270-383 已实现 Dify 阻塞路径,能解析 variants 数组。

改动

  • 兼容新 preset_idprizm_blue / pokemon_rainbow / topps_refractor / yugioh_secret / ws_ssp
  • attachMaterialsSnapshot 中的 Role: "composite" 逻辑保持不变
  • 不再依赖 req.RenderConfigs[].GratingConfigDify 自己渲,不需要后端 grating 参数)

6.2 前端 useLaserDifyGenerate 改动

文件frontend/composables/useLaserDifyGenerate.js

改动

  • resolveRenderConfigs 从返回 4 项改成 5 项
  • preset_codes 从 ['v1','v2','v3','v4'] 改成 ['prizm_blue','pokemon_rainbow','topps_refractor','yugioh_secret','ws_ssp']
  • applySucceeded 不再追加 original 为第 5 张Dify 已经返回 5 张完整的)

6.3 后端 compositor 服务(核心,必须保留

架构定位compositor 是最终成品图合成器AI 出"金属底背景图" → compositor 叠"用户人像 + 镭射光效" → 最终镭射卡。

职责6 层合成)

内容 来源
1 AI 艺术背景 Dify 工作流 AI 节点输出
2 金属反光梯度 程序生成
3 彩虹衍射光栅色带 程序生成(核心镭射效果)
4 AI 装饰层(可选) Dify 工作流 AI 节点输出
5 光栅 finish胶片颗粒 + 磨砂哑光) 程序生成
6 人物抠图(最顶层) 用户上传的原图cutout_url

关键改动

  • backend/gateway/service/compositor/*.go —— 保留并增强(之前标 DEPRECATED 是错的)
  • backend/gateway/controller/compositor_controller.go —— 保留
  • backend/gateway/router.go:299 路由保留

实施(在 handleDifyBlocking 中调用):

// 伪代码(具体实现见后续 PR
for i, variant := range dVariants {
    composited, err := compositor.Compose(compositor.ComposeRequest{
        BackgroundURL: variant.SignedURL,
        CutoutURL:     req.CutoutURL,        // 用户上传的人像
        GratingConfig: gratingConfigs[i],     // 5 套光栅参数
        ExportWidth:   450,
        ExportHeight:  600,
        VariantIndex:  i,
    })
    if err != nil {
        // 单张失败 → 整批失败(按 §9.1 fail-fast
        return handleError(c, err)
    }
    // 上传合成图到 OSS
    finalURL := uploadToOSS(composited, variant.OssKey)
    dVariants[i].SignedURL = finalURL
    dVariants[i].Role = "composite"  // 标记是合成图
}

注意:之前 spec 把 compositor 标为 DEPRECATED 是错误的决定——AI 不会完美生成"人像 + 镭射底",必须后端 compositor 合成。

6.4 LASER_GEN_PROVIDER 默认值

文件backend/gateway/config/config.go:205

改动

Provider: getEnv("LASER_GEN_PROVIDER", "dify"),  // 原值 "minimax"

7. 数据库

7.1 表结构变更

无变更laser_card_templates / laser_card_instances / laser_card_operation_logs 三张表的 schema 保持不变。

7.2 数据变更5 条 preset 同步)

把现有的 5 条 laser_card_templates 记录从 dream/classic/holoFull/ice/sunset 更新为 prizm_blue/pokemon_rainbow/topps_refractor/yugioh_secret/ws_ssp

BEGIN;

-- 删除旧 5 条
UPDATE public.laser_card_templates
SET deleted_at = (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
WHERE template_code IN ('dream', 'classic', 'holoFull', 'ice', 'sunset')
  AND deleted_at IS NULL;

-- 同步序列
SELECT setval('laser_card_templates_id_seq', (SELECT MAX(id) FROM public.laser_card_templates));

-- 插入新 5 条
INSERT INTO public.laser_card_templates (
    template_code, name, status, version,
    backdrop_options, render_config,
    engine_min_version, sort_order,
    star_id, created_by, created_at, updated_at
) VALUES
    ('prizm_blue', '蓝棱镜', 'published', 1,
     '[{"id":"prizm_blue","label":"蓝棱镜","oss_key":""}]'::jsonb,
     '{"style":"prizm_blue","family":"panini_prizm","sheen_band_angle":120,"sheen_intensity":0.38,"sheen_speed":0.42,"foil_coverage":0.70,"backdrop_tone":"#A8ACB2"}'::jsonb,
     'compositor-1.0.0', 0, 0, 0,
     (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
     (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint),
    ('pokemon_rainbow', '宝可梦彩虹', 'published', 1,
     '[{"id":"pokemon_rainbow","label":"宝可梦彩虹","oss_key":""}]'::jsonb,
     '{"style":"pokemon_rainbow","family":"pokemon_tcg","sheen_band_angle":100,"sheen_intensity":0.46,"sheen_speed":0.46,"foil_coverage":0.90,"backdrop_tone":"#B0ACA6"}'::jsonb,
     'compositor-1.0.0', 1, 0, 0,
     (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
     (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint),
    ('topps_refractor', 'Topps 折射', 'published', 1,
     '[{"id":"topps_refractor","label":"Topps 折射","oss_key":""}]'::jsonb,
     '{"style":"topps_refractor","family":"topps_chrome","sheen_band_angle":125,"sheen_intensity":0.36,"sheen_speed":0.40,"foil_coverage":0.68,"backdrop_tone":"#A8ACB2"}'::jsonb,
     'compositor-1.0.0', 2, 0, 0,
     (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
     (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint),
    ('yugioh_secret', '游戏王 Secret', 'published', 1,
     '[{"id":"yugioh_secret","label":"游戏王 Secret","oss_key":""}]'::jsonb,
     '{"style":"yugioh_secret","family":"yugioh","sheen_band_angle":90,"sheen_intensity":0.44,"sheen_speed":0.44,"foil_coverage":0.90,"backdrop_tone":"#B0ACA6"}'::jsonb,
     'compositor-1.0.0', 3, 0, 0,
     (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
     (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint),
    ('ws_ssp', 'WS SSP', 'published', 1,
     '[{"id":"ws_ssp","label":"WS SSP","oss_key":""}]'::jsonb,
     '{"style":"ws_ssp","family":"weiss","sheen_band_angle":95,"sheen_intensity":0.48,"sheen_speed":0.48,"foil_coverage":0.95,"backdrop_tone":"#A8ACB2"}'::jsonb,
     'compositor-1.0.0', 4, 0, 0,
     (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
     (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint);

-- 同步序列
SELECT setval('laser_card_templates_id_seq', (SELECT MAX(id) FROM public.laser_card_templates));

COMMIT;

7.3 现有 11 条 instance 处理

决策:不动。历史 instance 的 materials_snapshot.preset_id 还是 dream/classic 等旧值,仅作为历史记录保留。新生成的 instance 会用新 preset_id。


8. 前端适配

8.1 useLaserDifyGenerate.resolveRenderConfigs 改动

文件frontend/composables/useLaserDifyGenerate.js:103-117

新代码

const PRESET_CODES = [
  'prizm_blue',
  'pokemon_rainbow',
  'topps_refractor',
  'yugioh_secret',
  'ws_ssp',
]

async function resolveRenderConfigs(_presetIds, userPrompt) {
  let prompt = (userPrompt || '').trim().slice(0, 1000)
  if (prompt && containsChinese(prompt)) {
    console.log('[translate] Chinese detected, translating...')
    prompt = await translateToEnglish(prompt)
  }
  // 5 路并发 AI 生图,每路 1 个卡牌系列Dify 工作流内部按 preset_id 路由)
  return PRESET_CODES.map((code) => ({
    preset_id: code,
    bg_prompt: prompt || 'Transform this into a premium holographic artwork',
  }))
}

8.2 applySucceeded 改动

文件frontend/composables/useLaserDifyGenerate.js:192-207

新逻辑

  • 不再追加 original 为第 5 张Dify 已经返回完整 5 张)
  • variants 数组直接用 Dify 返回的 5 项

8.3 useLaserBatchGenerate 改动

文件frontend/composables/useLaserBatchGenerate.js:115

改动

const genMode = 'dify'  // 原值 'openai'

8.4 LenticularCard 接入 WebGL tilt 叠层(关键)

目的:用户在前端查看生成的 5 张镭射卡时,倾斜手机能看到"色彩随角度变化"的效果。

现有资产(全部保留):

文件 作用
frontend/components/lenticular/LenticularCard.vue 卡片显示容器
frontend/composables/useLenticularCraftTiltPreview.js tilt 输入 + WebGL 引擎包装
frontend/composables/useLenticularPreview.js 引擎 + 模拟 tilt
frontend/utils/lenticular-engine.js LenticularEngine 核心
frontend/utils/webgl/holographic-engine.js WebGL 渲染引擎
frontend/utils/webgl/holographic-shaders.js shader 源码

改动点

  1. LenticularCard.vue 已支持 image prop5 张图作为 images 数组传入即可)
  2. 新生成的图5 个 preset_id需要映射到引擎所需的 backdrop_tone / sheen_band_angle 参数:
// 新增映射表lenticular-engine.js 或独立 config 文件)
const PRESET_PHYSICS = {
  prizm_blue:      { metal: '#A8ACB2', angle: 120, intensity: 0.38, speed: 0.42, foil: 0.70 },
  pokemon_rainbow: { metal: '#B0ACA6', angle: 100, intensity: 0.46, speed: 0.46, foil: 0.90 },
  topps_refractor: { metal: '#A8ACB2', angle: 125, intensity: 0.36, speed: 0.40, foil: 0.68 },
  yugioh_secret:   { metal: '#B0ACA6', angle: 90,  intensity: 0.44, speed: 0.44, foil: 0.90 },
  ws_ssp:          { metal: '#A8ACB2', angle: 95,  intensity: 0.48, speed: 0.48, foil: 0.95 },
}
  1. LenticularCard.vue 根据当前显示图对应的 preset_id 取物理参数,喂给 engine

详细 WebGL shader 工作原理和 uniform 配置见 §12。


8.5 用户输入规范(前端 UI 引导)

核心问题:用户在"生成镭射卡"页面输入什么?

8.5.1 输入语义

用户输入的应该是**"想要的镭射卡氛围/风格"**,不是 IP 名、人物名、长句子。

8.5.2 输入示例(推荐 1-15 字)

类型 示例
季节氛围 梦幻樱花 / 夏日海风 / 冬日雪景 / 秋日金黄
自然现象 星空极光 / 银河流星 / 极光之夜 / 晨雾森林
风格美学 赛博朋克 / 蒸汽朋克 / 复古胶片 / 极简主义
色彩情绪 玫瑰金之夜 / 午夜蓝调 / 香槟暖阳 / 翡翠冷光
节日主题 圣诞夜空 / 新年烟花 / 情人节心动

8.5.3 完整示例:输入"梦幻樱花"会得到什么

用户输入梦幻樱花

5 张图都是"梦幻樱花"主题,但工艺不同(用户对比"哪种光效最适合这张图"

# preset_id 英文 prompt喂给 gpt-image-1 视觉特征
1 prizm_blue dreamy cherry blossom, on cool silver #A8ACB2 metallic card substrate, electric blue sapphire accent, sharp reflective sport card mood, centered subject, no text, no people, no watermark 锐利反射 + 电光蓝 + 樱花元素
2 pokemon_rainbow dreamy cherry blossom, on pearl silver #B0ACA6 metallic card substrate, soft luminous anime glow, premium Japanese TCG mood, centered chibi-style subject, no text, no people, no watermark 日系动漫 + 闪光粒子 + kawaii 樱花
3 topps_refractor dreamy cherry blossom, on cool silver #A8ACB2 metallic card substrate, vintage baseball card mood, soft film grain texture, classic portrait composition, no text, no people, no watermark 复古肖像 + 胶片颗粒 + 樱花
4 yugioh_secret dreamy cherry blossom, on pearl silver #B0ACA6 metallic card substrate, full-card luminous anime art, magic circle elements, dynamic action pose, no text, no people, no watermark 全卡横纹 + 魔法阵 + 樱花
5 ws_ssp dreamy cherry blossom, on cool silver #A8ACB2 metallic card substrate, premium anime collectible apex mood, signature-style framed composition, no text, no people, no watermark 签名框 + 樱花 + 终极收藏感

前端展示效果5 张图横向排列在同一页面,用户能一眼对比"哪个光效最适合"。点选 1 张 → 走铸造流程。

关键设计5 张图主题完全一致,只是工艺/金属底/光线/构图不同。这才是"5 张差异化"——而不是 5 张完全不同主题的图。

8.5.4 反例(不应该输入)

反例 为什么不合适
"宝可梦" 这是 IP 名,不是风格
"我的照片" 不是风格描述
"生成一张好看的卡" 太抽象,没具体氛围
"樱花、海浪、星空混合" 太长(> 15 字AI 难聚焦
"Pikachu 在草地上玩耍" 叙事场景,不是风格

8.5.5 前端 UI 实现

lenticular-create.vue 页面:

<template>
  <view class="prompt-input">
    <textarea
      v-model="userPrompt"
      :placeholder="placeholder"
      :maxlength="15"
      @input="onInput"
    />
    <view class="hot-chips">
      <view
        v-for="chip in hotChips"
        :key="chip"
        class="chip"
        @click="userPrompt = chip"
      >
        {{ chip }}
      </view>
    </view>
  </view>
</template>

<script setup>
const placeholder = '输入想要的镭射卡风格,如「梦幻樱花」'
const userPrompt = ref('')
const hotChips = ['梦幻樱花', '星空极光', '赛博朋克', '玫瑰金之夜', '复古胶片']

function onInput() {
  // 限制 1-15 字
  if (userPrompt.value.length > 15) {
    userPrompt.value = userPrompt.value.slice(0, 15)
  }
}
</script>

关键 UI 元素

  1. placeholder 文案:明确告诉用户"输入风格,不是 IP 名"
  2. 5 个热门 chip:点击即填入,覆盖 80% 场景
  3. maxlength=15:硬限制,避免超长输入
  4. 后端兜底:服务端二次校验(见 §8.5.5

8.5.6 后端校验(兜底)

如果用户绕过前端直接调 API输入了不合适的内容如"Pokemon"或长段落),后端 handleDifyBlocking 不做特殊处理——直接转发给 Dify。LLM 会按 system prompt 处理,可能产生不可预期输出但不报错。

不做强校验:因为:

  • 限制太死会影响用户体验
  • 不可预期输出也是"风格化"的一种表现
  • D2 灰度时观察用户实际输入数据再决定是否加严

9. 错误处理

9.1 整体策略fail-fast任一节点失败 → 整个工作流失败)

Dify 工作流默认错误传播模型:任何节点报错 → 整个工作流立即中断 → 后端拿到 status: failed

5 个生图节点不开启 "Continue on Error"——任一生图节点失败就整体失败,前端引导用户重试。

场景 行为 前端展示
LLM 节点返回非 JSON Code 节点抛异常 → 工作流挂掉 整体失败,弹"AI 主题扩写失败,请重试"
任一生图节点失败4 种原因) 工作流立即挂掉5 路全废 整体失败,弹"AI 生图失败X/Y 张),请重试"
Code merge 节点失败 工作流挂掉 整体失败,弹"图床合并失败,请重试"
整体超时(> 60s Dify 平台 timeout 整体失败,弹"生成超时,请重试"
后端 OSS 上传失败(理论上不发生) controller 写 ERROR 日志 弹"图床上传失败,请重试"

9.2 单路失败的 4 种原因

原因 处理
gpt-image-1 API 限流 Dify 节点内置重试 1 次;不解决则整体失败
gpt-image-1 内容审核拒绝 整体失败,前端引导"换个主题试试"
网络抖动 Dify 节点内置重试 1 次
AI 出图时长 > 60s Dify 节点 timeout整体失败

9.3 与前端用户体验的配合

  • 整体失败 = 重试按钮(不是"用成功的那几张"
  • 重试 = 调 useLaserDifyGenerate.submit() 重新走工作流
  • 不会"部分成功",用户每次看到 5 张完整图 或 0 张完整图

10. 测试

10.1 单元测试

测试 工具
useLaserDifyGenerate.resolveRenderConfigs 返回 5 项 Vitest
useLaserDifyGenerate.applySucceeded 不再追加 original Vitest
handleDifyBlocking 解析新 preset_id Go test

10.2 集成测试

场景 步骤
完整链路 上传图 → 输入 prompt → 调 Dify → 收到 5 张 → 选 1 → 铸造
整体失败fail-fast 验证) Mock Dify 让任一生图节点返回 error → 整个工作流失败 → 后端 status=failed → 前端展示"AI 生图失败,请重试"
重试生效 Mock Dify 让生图节点首次 fail + 重试 succeed → 5 张图正常返回
主题差异 同一图不同 prompt樱花 vs 海洋 vs 城市)→ 5 张主题不同
并行执行验证 5 路生图节点总耗时 ≈ 最慢一路(不是累加 50s+

10.3 灰度

  1. D1:内部测试账号 100% 流量
  2. D2:白名单 5% 真实用户
  3. D3:全量(观察 1 周)

11. 风险与回滚

风险 缓解 回滚
Dify 工作流配置错误 详细小白指南 + 调试模式 LASER_GEN_PROVIDER 改回 openai
AI 出图质量不稳定 D2 灰度观察 同上
Dify 平台不稳定 监控告警 + 备用 OpenAI provider 同上
5 个 preset_id 不被前端识别 保留兼容逻辑(旧 preset_id 也映射) 暂时回退到旧 preset
Dify 代码节点 sandbox 连不上Name or service not known Dify 部署时确保 sandbox 容器在跑 见下方 §11.1 排查步骤

11.1 Dify sandbox 服务排查

现象 Asandbox 容器连不上

报错Failed to execute code ... Error: [Errno -2] Name or service not known

原因Dify 代码沙箱sandbox 容器)连不上或没启动

排查命令

# 1. 检查 sandbox 容器状态
docker ps | grep sandbox

# 2. 查看日志
docker logs dify-sandbox-1
# 或
docker logs sandbox-1

# 3. 测试网络连通
docker exec dify-api-1 ping sandbox

# 4. 重启 sandbox
docker compose -f docker/docker-compose.yaml restart sandbox

# 5. 如果还是不行,看 docker-compose 是否漏写 sandbox
grep -A5 "sandbox:" docker/docker-compose.yaml

现象 BPython 启动崩溃seccomp abort

报错

main.DifySeccomp(...ate filter
goroutine 17 [running, locked to thread]:
main.DifySeccomp(...)
    /home/runner/work/dify-sandbox/dify-sandbox/cmd/lib/python/main.go:11
error: signal: aborted (core dumped)

根因docker/dify-deploy.sh 默认下载 Dify v0.15.0,其 sandbox 镜像有 seccomp 限制 bug启动 Python 子进程时 abort。

修复:升级 Dify 到 v1.x已修复 seccomp

完整升级步骤(在 101.132.250.62 上):

# 0. SSH 登录
ssh root@101.132.250.62
cd /opt/dify/docker

# 1. 备份关键v1.x 不兼容 v0.15.x workflow
cp .env .env.bak.$(date +%Y%m%d)
docker exec dify-postgres-1 pg_dump -U postgres dify > /tmp/dify_db_$(date +%Y%m%d).sql

# 2. 停掉旧服务(不要加 -v会删数据卷
docker compose down

# 3. 拉新版本 docker-compose
curl -L https://raw.githubusercontent.com/langgenius/dify/1.4.0/docker/docker-compose.yaml \
  -o docker-compose.yml

# 4. 拉新 .env.example保留现有 .env 不动)
curl -L https://raw.githubusercontent.com/langgenius/dify/1.4.0/docker/.env.example \
  -o .env.example

# 5. 把 .env.example 新增的变量补到 .env保留旧值
diff .env .env.example | head -50
cat .env > .env.new
# 编辑 .env.new把 .env.example 新增的变量加进去(默认值即可)
mv .env.new .env

# 6. 拉新镜像
docker compose pull

# 7. 启动首次启动会跑数据库迁移1-3 分钟)
docker compose up -d

# 8. 看迁移日志
docker compose logs -f api 2>&1 | head -100

# 9. 健康检查
curl -s -o /dev/null -w '%{http_code}' http://localhost:5001/health
# 应该返回 200

# 10. 检查 sandbox 修复
docker ps | grep sandbox
docker logs dify-sandbox-1 --tail 50
# 不应该有 DifySeccomp abort 报错

v1.4.0 新增关键环境变量(如果 .env 缺失需手动补):

# 加到 .env 文件末尾
echo "CONSOLE_API_URL=http://localhost:8084/v1" >> .env
echo "CONSOLE_WEB_URL=http://localhost:8084" >> .env
echo "SERVICE_API_URL=http://localhost:8083/v1" >> .env
echo "APP_API_URL=http://localhost:8083/v1" >> .env
echo "APP_WEB_URL=http://localhost:8083" >> .env
echo "FILES_URL=http://localhost:8083" >> .env
echo "MIGRATION_ENABLED=true" >> .env
echo "SANDBOX_PORT=8194" >> .env
echo "SANDBOX_API_KEY=dify-sandbox" >> .env

升级后必须做

  1. 重新导入 workflowv0.15.x yml 不兼容 v1.x

    • 在 Dify 控制台 http://101.132.250.62:8084 手动搭建 laser_card_v2_5series 工作流
    • 或让 Claude 按 §3 / §5 步骤远程协助配置
  2. 验证 sandbox 修复

    • Dify 控制台"调试"模式跑工作流
    • 看 5 个生图节点是否各拿到 1 张图
  3. 测试后端调用

    curl -X POST http://localhost:8083/v1/workflows/run \
      -H "Authorization: Bearer <API_KEY>" \
      -H "Content-Type: application/json" \
      -d '{"inputs": {"#context#": "梦幻樱花"}, "user": "test-user"}'
    

回滚步骤(如果迁移失败):

cd /opt/dify/docker
docker compose down

# 恢复旧版本
cp docker-compose.yml.bak docker-compose.yml
cp .env.bak.20260101 .env

# 启动旧版本
docker compose up -d

# 数据库回滚
docker exec -i dify-postgres-1 psql -U postgres dify < /tmp/dify_db_20260101.sql

现象 CDify 平台未部署 / 找不到 sandbox

排查Dify 可能在另一台服务器部署。

# 1. 确认 Dify 实际部署在哪
grep -E "SERVER_HOST|SERVER_PATH" /Users/liulujian/Documents/code/TopFansByGithub/docker/dify-deploy.sh
# 默认是 SERVER_HOST="101.132.250.62"

# 2. SSH 到 Dify 服务器排查
ssh root@101.132.250.62
docker ps | grep -iE "sandbox|dify"

修复后:在 Dify 工作流"调试"按钮重新跑一次,确认 5 个生图节点各输出 1 张图。


12. 前端展示层WebGL tilt 实时渲染)

本节为新增加章节。spec §3 / §8 仅覆盖"AI 出图 + 落库",本节专门讲"前端展示时的彩虹随角度变化怎么实现"。

12.1 设计目标

维度 目标
倾斜角度 用户左/右/上/下倾斜手机(-1..+1 归一化)
色彩响应 彩虹色带方向、高光位置、强度随 viewAngle 偏移
性能 60fps移动端 GPU 需 < 5ms / 帧)
兼容 iOS Safari / Android Chrome / 微信内置 / H5

12.2 技术栈(已存在 + 复用)

文件 角色
输入 useLenticularCraftTiltPreview.js 陀螺仪 / 触摸 → 归一化 tilt 输入
引擎 utils/lenticular-engine.js LenticularEngine 类,物理参数 + sensor 输入
渲染 utils/webgl/holographic-engine.js WebGL 上下文 + uniform 写入
Shader utils/webgl/holographic-shaders.js GLSL 源码(含 rainbow 计算)
容器 components/lenticular/LenticularCard.vue Vue 组件,绑定 canvas

所有层已存在,本次只做参数接线,不重写引擎。

12.3 Shader 关键 uniform

文件frontend/utils/webgl/holographic-shaders.js

uniform 类型 来源 作用
u_viewAngle vec3 tilt (dx, dy, dz) 视角方向 → 决定色带偏移
u_lightDir vec3 固定 (0, 0, 1) 光源方向(屏幕外)
u_dispersionStrength float preset.foil_coverage 色散强度(彩虹宽度)
u_highlightSpeed float preset.sheen_speed 高光移动速度
u_highlightWidth float preset.sheen_intensity 高光带宽度
u_baseImage sampler2D AI 出的静态图 底图
u_spectrumRamp sampler2D 预生成彩虹 LUT 彩虹色查找表

关键公式fragment shader 内):

// 把 viewAngle 投影到屏幕空间 → 计算色带偏移量
vec2 offsetDir = normalize(u_viewAngle.xy);
float offsetAmt = length(u_viewAngle.xy) * u_dispersionStrength;

// 在 uvs 上加偏移 → 采样 spectrum ramp
vec2 rampUV = v_uv + offsetDir * offsetAmt;
vec3 rainbow = texture(u_spectrumRamp, rampUV).rgb;

// 高光带1D 投影
float highlightPos = dot(v_uv, offsetDir);
float highlight = smoothstep(u_highlightWidth, 0.0, abs(highlightPos - 0.5));
rainbow += vec3(highlight) * u_highlightSpeed;

// 屏幕混合模式
vec3 finalColor = baseColor + rainbow * 0.5;
gl_FragColor = vec4(finalColor, 1.0);

12.4 5 个 preset 的物理参数映射

preset_id metal hex sheen angle intensity speed foil
prizm_blue #A8ACB2 120° 0.38 0.42 0.70
pokemon_rainbow #B0ACA6 100° 0.46 0.46 0.90
topps_refractor #A8ACB2 125° 0.36 0.40 0.68
yugioh_secret #B0ACA6 90° 0.44 0.44 0.90
ws_ssp #A8ACB2 95° 0.48 0.48 0.95

映射表存放位置frontend/utils/lenticular-engine.js 内新增 PRESET_PHYSICS_MAP 常量。

12.5 tilt 输入流

[陀螺仪/触摸]
       ↓
DeviceOrientationEvent / touchstart/touchmove
       ↓
useLenticularCraftTiltPreview.simulateTilt(dx, dy)
       ↓ (归一化 -1..+1)
LenticularEngine.feedSensor({x, y, z})
       ↓ (物理平滑 + 滤波)
LenticularEngine.computeRenderState()
       ↓
{ viewAngle: vec3, displacement: vec2 }
       ↓
holographic-engine.setUniforms({ u_viewAngle, ... })
       ↓
WebGL draw call → 60fps 输出

12.6 与既有 LenticularCard 集成

当前状态LenticularCard.vue 已支持 tilt 渲染,但绑定的物理参数是写死的。

改动:让 LenticularCard.vue 接受 presetId prop根据 presetId 从 PRESET_PHYSICS_MAP 取物理参数,喂给 LenticularEngine。

<!-- LenticularCard.vue -->
<script setup>
import { useLenticularCraftTiltPreview } from '@/composables/useLenticularCraftTiltPreview'
import { PRESET_PHYSICS_MAP } from '@/utils/lenticular-engine'

const props = defineProps({
  image: { type: String, required: true },
  presetId: { type: String, default: 'prizm_blue' },
})

const physics = PRESET_PHYSICS_MAP[props.presetId] || PRESET_PHYSICS_MAP.prizm_blue
const preview = useLenticularCraftTiltPreview({
  canvasId: props.canvasId,
  baseImage: props.image,
  physics,  // ← 新增 prop
})
</script>

12.7 失败兜底

场景 兜底
WebGL 上下文创建失败(老旧设备) 降级到 <img> 静态展示
陀螺仪 API 不可用H5 / 微信内置) 触摸滑块控制 tilt
shader 编译失败 控制台日志 + 静态展示

12.8 性能预算

  • 单帧时间< 5ms移动端中端机型
  • shader 复杂度:限制在 20 条 ALU 指令以内
  • 纹理数量:≤ 3baseImage + spectrumRamp + 可选 scratchMap

13. 待用户审 spec 的问题

在开始 writing-plans 前,需要你确认:

  1. 设计方向:路径 CDify 工作流)+ 5 系列 × 风格描述词 + AI 出金属底卡 + 前端 WebGL tilt 实时叠彩虹,是否符合预期?
  2. preset_id 命名prizm_blue / pokemon_rainbow / topps_refractor / yugioh_secret / ws_ssp可以接受吗
  3. 数据库 5 条模板:用 §7.2 的 SQL 直接覆盖现有的 dream/classic/holoFull/ice/sunset可以接受吗
  4. 后端 compositor:本次只标记 DEPRECATED保留供调试删除放在后续 PR。可以接受吗
  5. 灰度策略:先 100% 内部 → 5% 白名单 → 全量。可以接受吗?
  6. Dify 平台 + 模型
    • 你的 Dify 平台是否已部署?是否支持 gpt-image-1如不支持告诉我备选
    • LLM 节点用 gpt-4o-mini 还是 minimax-m3(你实际在用的)?
  7. 前端 WebGL tilt:是否接受 §12 的方案——LenticularEngine 复用 + PRESET_PHYSICS_MAP 映射 + LenticularCard 改造?
  8. AI prompt 调整:是否接受 §5.2 新的 LLM prompt——强调金属底、严禁 holographic/rainbow 等词?
  9. 用户输入规范:是否接受 §8.5 的方案——前端输入框 placeholder + 5 个热门风格 chip + maxlength=15