视觉情绪板
Mood Board Skill — 把 Journey / Stories / Brief 的产品语境翻译为 3-4 套可对比的视觉方案,用户选定即固化为 design-tokens,向下游设计 / 动效 / 图表 Skill 提供统一视觉语言。
能力矩阵
本 Skill 的三种运行模式,可单独运行也可叠加。最常见路径:链式模式(从 /用户旅程 + /用户故事 + /设计简报 来)。
| 模式 | 触发条件 | 产出特征 |
|---|---|---|
| 🟢 独立模式 | 无前序上下文,直接调用 | Phase 1/2 引导用户口述触点 + 产品语境 → 3-4 套方案 HTML |
| 🔵 链式模式 | 检测到 spark-output/context/brief.json 或 journey.json 或 stories.json |
跳过 Phase 1 触点提取(直接从 journey.touchpoints 读)+ 跳过 Phase 2 行业/受众追问(从 brief 读) |
| 🟣 增强模式 | 当前会话可用 ImageGen 工具 | 每方案 3 张 1792×1024 真实氛围图;不可用时 4 格全部用 CSS linear-gradient 兜底 |
三种模式下产出形态完全一致(双通道:
spark-output/context/board.json+ 自包含 HTML),区别仅在"提问多少 / 字段从哪里来 / 氛围图是真图还是渐变"。
输入要求
| 输入项 | 必填? | 来源优先级 | 缺失时行为 |
|---|---|---|---|
project_name |
✅ | 链式 brief.project_name > journey.project_name > 用户输入 | Phase 2 追问 |
| 触点列表(3-8 个) | ✅ | 链式 journey.stages[].touchpoints > stories.stories[].scenario > 用户输入 | Phase 1 引导 |
product_type + 行业 |
✅ | 链式 brief.project_type / project_subtype > 用户输入 | Phase 2 追问 |
| 目标受众 | ✅ | 链式 brief.user > stories.persona > 用户输入 | Phase 2 追问 |
| 已有品牌约束 | ⭕ | 链式 brief.constraints(含品牌色 / 字体)> 用户输入 | 标注"未提供",方案自由发散 |
| 风格参考 | ⭕ | 用户输入(图片 / 关键词) | 跳过,按情绪映射表自动推导 |
| 是否生成真实氛围图 | ⭕ | 工具可用性自动判断 | ImageGen 不可用时全部用 CSS gradient |
信息完整度判断:必填项任一缺失 → 进入对应 Phase 引导追问;仅可选项缺失 → 按映射表默认推导,HTML 内不做任何"待补充"占位。
Chain Context
上游读取(Phase 0.5 执行)
Step 1 · 扫描上下文来源(按顺序,找到任一即读取,多个可叠加):
- 会话内 marker:
<!-- spark-context:brief -->/journey/stories - 项目文件:
spark-output/context/brief.json/journey.json/stories.json - 都没有 → 跳过 Phase 0.5,按无上下文流程执行
Step 2 · 字段映射 checklist:
来自 brief(最常见,对齐过设计方向的项目)
| Board 字段 | ← Brief 字段 | 处理方式 |
|---|---|---|
project_name |
brief.project_name |
直接沿用 |
| 产品类型 + 行业 | brief.project_type + brief.project_subtype |
跳过 Phase 2 行业追问 |
| 目标受众 | brief.user |
跳过 Phase 2 受众追问 |
| 品牌约束 | brief.constraints(筛选含"品牌色 / 字体 / Logo / 视觉规范"等关键词) |
作为 Phase 3 的硬约束,方案必须遵守 |
| 风格倾向(候选) | brief.style(chalk / parchment / noir 等模板风格) |
作为方案 A 的色彩起点参考 |
| 设计标准(定性) | brief.design_criteria.qualitative |
作为方案命名 / 描述的语气基线 |
来自 journey(最强信号,触点情绪直接驱动方案推导)
| Board 字段 | ← Journey 字段 | 处理方式 |
|---|---|---|
| 触点列表 | journey.stages[*].touchpoints |
跳过 Phase 1 触点提取,直接进入情绪关键词标注 |
| 情绪关键词 | journey.stages[*].emotion + key_moments[*].emotion |
注入 Phase 3 emotion-visual-mapping 查询 |
| pain level | journey.stages[*].pain_points 数量 / severity |
决定该触点在 mood-row 标签的优先级 |
touchpoints[].override |
journey.key_moments[type=peak/dropout] |
写入 board.json 的 touchpoints[] 数组,下游 Flow 可对该触点单独覆写颜色 |
来自 stories(增量来源,补充人物锚点)
stories.persona.description + jtbd→ HTML header 的persona_line/jtbd_line直接渲染stories.stories[*].scenario→ 当 journey 缺失时,作为触点提取的 fallback 来源
Step 3 · 告知用户沿用情况(必做):
读到上下文后必须明确告诉用户:
"已读到 [N 个] 上游 Skill 上下文:[列出 brief/journey/stories]。已自动预填以下字段,跳过对应提问:
- project_name = [...]
- 触点列表(来自 journey)= [N 个]
- product_type = [...]
- 目标受众 = [...]
接下来从 [第一个未自动预填的字段] 开始问你(或如全部填满,直接进入 Phase 3 推导方案)。"
下游输出(Phase 7 执行,按 chain-protocol §2.1 v1.1.1 顺序)
完成产出后严格按以下顺序:
先写盘
spark-output/context/board.json(schema 见 frontmatter)自检行:chat 输出
✅ board.json 已写盘到 spark-output/context/board.jsonpresent_files 输出 HTML 给用户预览
紧凑 marker:
<!-- spark-context:board ref="spark-output/context/board.json" --> Board 已保存:project=xxx,selected_scheme=xxx,primary=#xxx,font=xxx <!-- /spark-context:board -->Handoff 引导:建议跑
/Web页面设计(消费 board.color + component)或/动效规划(消费 board.typography 调性)或/数据可视化(消费 board.color.primary/secondary)更新链路面板
spark-output/dashboard.html(详见 chain-protocol §9)
字段流向下游
Board 是视觉锚点,下游消费:
board.color.primary/secondary/neutral→ flow-web / flow-mobile 的页面配色;chart 的图表配色board.typography.heading_font/body_font→ flow-web / flow-mobile 的字体规范;motion-plan 的字体调性反推 Personality archetypeboard.component.button/card/input/tag→ flow-web / flow-mobile 的组件落地基准board.style_keywords→ motion-plan 推导 archetype(Playful / Premium / Corporate / Energetic)的关键输入board.spacing/radius→ flow-web / flow-mobile 的间距 / 圆角系统board.touchpoints[].override→ flow-web / flow-mobile 在该触点对应页面的局部色彩覆写
⚠️ CRITICAL RULES — 违反任何一条即为失败
- HTML 的 CSS 必须逐字复用本文件中
LOCKED_CSS变量的内容。禁止新增、删除、修改任何选择器或属性值。 - HTML 必须完全自包含——无外部 CSS/JS/图片 URL。图片用
data:image/jpeg;base64,...内嵌。 - 每套方案必须包含全部 4 行:氛围图行(4格) → 字体+介绍行 → 色条行(Primary+Secondary) → 组件行(4列)。
- 色条 flex 比例必须是
8:4:3:2:2。 - 组件行必须渲染 4 列:Buttons / Input / Card / Tags+Toggle,用
border-right分隔。 - 每套方案 3 张 ImageGen 氛围图 + 1 张 CSS linear-gradient 补位 = 共 4 格。
- 按钮/输入框/卡片/标签的文案必须与产品语境匹配(教育→"开始学习",电商→"立即下单")。
- 输出语言跟随输入语言(中文 journey → 中文标注;英文 → 英文)。
执行流程
Phase 0.5: 上下文检查
按 Chain Context 章节定义读取 spark-output/context/{brief,journey,stories}.json,预填 project_name / 触点 / 产品类型 / 受众 / 品牌约束。告知用户沿用情况后进入下一 Phase。
Phase 1: 提取触点
链式模式跳过:直接从 journey.stages[*].touchpoints 读取触点列表与情绪关键词。
独立模式:读取用户提供的 journey/stories 文字。提取 3-8 个触点,每个标注:名称、情绪关键词(1-3个)、pain level。列表交给用户确认。
Phase 2: 收集上下文
用 AskUserQuestion 确认(已知的跳过):
- 产品类型+行业(链式从
brief.project_type读) - 目标受众(链式从
brief.user读) - 已有品牌约束(链式从
brief.constraints读,有则遵守) - 风格参考(可选)
Phase 3: 推导 3-4 套方案
基于触点情绪 + 产品性质,推导 3-4 套色彩方向不同的方案。每套方案定义:
name: 方案命名(2-3字)desc: 1-2句色彩哲学描述keywords: 4个关键词font: 推荐字体名primary: 5色数组 [500主色, 300浅, 700深, 100最浅, 900最深]secondary: 5色数组,同结构btn_bg,btn_color,input_border,card_border,tag_bg,tag_color,toggle_bg
参考 emotion-visual-mapping.md 做情绪→色彩翻译。
Phase 4: 生成氛围图
为每套方案生成 3 张 ImageGen(1792x1024),体现该方案色彩方向的产品氛围。
降级:ImageGen 工具不可用时,4 格全部用 CSS linear-gradient 渲染(
primary[0]→secondary[0]),不阻断流程。
Phase 5: 组装 HTML
必须严格执行以下步骤,不可省略或改编:
Step 5.1: 缩略 + base64
mkdir -p spark-output/board/{project}-moodboard
cp vibe_images/{project}-*.png spark-output/board/{project}-moodboard/
cd spark-output/board/{project}-moodboard
for f in *.png; do
sips -Z 400 "$f" --out "${f%.png}_thumb.jpg" -s format jpeg -s formatOptions 60 2>/dev/null
done
然后用 bash 生成 b64data.json:
A1=$(base64 -i {方案A图1}_thumb.jpg | tr -d '\n')
A2=$(base64 -i {方案A图2}_thumb.jpg | tr -d '\n')
# ... 所有12张
printf '{"a1":"%s","a2":"%s",...}' "$A1" "$A2" ... > b64data.json
Step 5.2: 运行 Python 脚本生成 HTML
以下 Python 脚本是模板。执行时只允许修改标注了 # ← EDIT 的行。其余代码(尤其是 LOCKED_CSS 和 render_scheme 函数)必须原封不动复制。
import json, os
with open("b64data.json") as f:
b64 = json.load(f)
# ← EDIT: 填入 Phase 3 推导的方案数据
schemes = [
{
"id": "a",
"name": "方案名", # ← EDIT
"desc": "方案描述", # ← EDIT
"keywords": ["kw1","kw2","kw3","kw4"], # ← EDIT
"font": "Space Grotesk", # ← EDIT
"img_keys": ["a1", "a2", "a3"],
"img_labels": ["标签1","标签2","标签3"], # ← EDIT
"primary": ["#7C4DFF","#B388FF","#5E35B1","#EDE7F6","#311B92"], # ← EDIT
"secondary": ["#00BCD4","#80DEEA","#0097A7","#E0F7FA","#006064"], # ← EDIT
"btn_bg": "#7C4DFF", "btn_color": "#fff", # ← EDIT
"input_border": "#B388FF", # ← EDIT
"card_border": "#EDE7F6", # ← EDIT
"tag_bg": "#EDE7F6", "tag_color": "#5E35B1", # ← EDIT
"toggle_bg": "#7C4DFF", # ← EDIT
},
# ← EDIT: 方案 B, C, D 同结构
]
# ← EDIT: 产品信息
project_title = "产品名"
persona_line = "Persona: xxx"
jtbd_line = "JTBD: xxx"
# ← EDIT: 组件文案(必须匹配产品语境)
BTN_PRIMARY = "主按钮"
BTN_SECONDARY = "次按钮"
INPUT_PLACEHOLDER = "搜索..."
CARD_TITLE = "卡片标题"
CARD_DESC = "卡片描述"
TAG_A = "标签A"
TAG_B = "标签B"
# ─── 以下禁止修改 ───────────────────────────────────────────
LOCKED_CSS = """* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "PingFang SC", -apple-system, sans-serif;
background: #FFFFFF;
color: #1F2937;
padding: 48px 40px;
min-height: 100vh;
}
.container { max-width: 100%; margin: 0 auto; }
.scheme-nav {
display: flex;
gap: 0;
margin-bottom: 48px;
border-bottom: 1px solid #D1D5DB;
}
.scheme-tab {
padding: 10px 24px;
font-size: 13px;
font-weight: 400;
color: #6B7280;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.scheme-tab:hover { color: #1F2937; }
.scheme-tab.active {
color: #1F2937;
font-weight: 600;
border-bottom-color: #1F2937;
}
.scheme-panel { display: none; }
.scheme-panel.active { display: block; }
.mood-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-bottom: 40px;
}
.mood-img {
height: 100px;
display: flex;
align-items: flex-end;
padding: 8px;
}
.mood-img span {
font-size: 9px;
color: #fff;
background: rgba(0,0,0,0.4);
padding: 2px 6px;
}
.row-type {
display: flex;
align-items: center;
}
.type-display {
flex: 0 0 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.type-display .aa {
font-size: 120px;
font-weight: 600;
line-height: 1;
letter-spacing: -2px;
}
.type-display .font-name {
font-size: 11px;
color: #6B7280;
margin-top: 10px;
}
.intro {
flex: 1;
padding-left: 40px;
}
.intro h2 {
font-size: 15px;
font-weight: 600;
color: #1F2937;
margin-bottom: 10px;
}
.intro p {
font-size: 13px;
color: #6B7280;
line-height: 1.7;
margin-bottom: 2px;
}
.intro .keywords {
display: flex;
gap: 6px;
margin-top: 12px;
}
.intro .kw {
font-size: 10px;
padding: 2px 8px;
border: 1px solid #D1D5DB;
color: #4B5563;
}
.divider {
height: 1px;
background: #D1D5DB;
margin: 40px 0;
}
.row-colors {
display: flex;
gap: 40px;
}
.row-colors > div {
flex: 1;
min-width: 25%;
}
.color-group {
display: flex;
gap: 0;
height: 160px;
width: 100%;
}
.color-strip {
height: 100%;
}
.color-module-title {
font-size: 11px;
font-weight: 600;
color: #6B7280;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 1px;
}
.row-comps {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0;
}
.comp-col {
padding: 0 15px;
border-right: 1px solid #D1D5DB;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.comp-col:first-child { padding-left: 0; }
.comp-col:last-child { border-right: none; padding-right: 0; }
.comp-col-title {
font-size: 10px;
color: #6B7280;
text-transform: uppercase;
letter-spacing: 0.8px;
margin-bottom: 4px;
}
.btn {
display: inline-block;
padding: 6px 16px;
font-size: 11px;
font-weight: 600;
border: none;
cursor: pointer;
text-align: center;
white-space: nowrap;
}
.input-demo {
padding: 6px 10px;
border: 1px solid #D1D5DB;
font-size: 11px;
background: #F9FAFB;
width: 120px;
color: #6B7280;
}
.card-demo {
border: 1px solid #D1D5DB;
padding: 10px;
max-width: 140px;
}
.card-demo .card-title {
font-size: 11px;
font-weight: 600;
margin-bottom: 3px;
}
.card-demo .card-desc {
font-size: 10px;
color: #6B7280;
}
.tag {
display: inline-block;
padding: 2px 6px;
font-size: 9px;
font-weight: 500;
}
.toggle {
width: 30px; height: 16px;
border-radius: 8px;
position: relative;
}
.toggle::after {
content: '';
position: absolute;
width: 12px; height: 12px;
background: #fff;
border-radius: 50%;
top: 2px; right: 2px;
}
.header {
margin-bottom: 32px;
}
.header h1 {
font-size: 18px;
font-weight: 600;
margin-bottom: 6px;
}
.header p {
font-size: 12px;
color: #6B7280;
}"""
def render_scheme(s, active=False):
active_cls = " active" if active else ""
mood_html = ""
for key, label in zip(s["img_keys"], s["img_labels"]):
mood_html += (
f'<div class="mood-img" style="background:url(data:image/jpeg;base64,'
f'{b64[key]}) center/cover no-repeat;"><span>{label}</span></div>\n'
)
mood_html += (
f'<div class="mood-img" style="background:linear-gradient(135deg, '
f'{s["primary"][0]}, {s["secondary"][0]});"><span>色彩延伸</span></div>\n'
)
flexes = [8, 4, 3, 2, 2]
pri = "".join(f'<div class="color-strip" style="flex:{fx};background:{c};"></div>' for c, fx in zip(s["primary"], flexes))
sec = "".join(f'<div class="color-strip" style="flex:{fx};background:{c};"></div>' for c, fx in zip(s["secondary"], flexes))
kw = "".join(f'<span class="kw">{k}</span>' for k in s["keywords"])
return f'''<div class="scheme-panel{active_cls}" id="panel-{s['id']}">
<div class="mood-row">{mood_html}</div>
<div class="row-type">
<div class="type-display">
<div class="aa" style="font-family:'{s['font']}',sans-serif;">Aa</div>
<div class="font-name">{s['font']}</div>
</div>
<div class="intro">
<h2>{s['name']}</h2>
<p>{s['desc']}</p>
<div class="keywords">{kw}</div>
</div>
</div>
<div class="divider"></div>
<div class="row-colors">
<div><div class="color-module-title">Primary</div><div class="color-group">{pri}</div></div>
<div><div class="color-module-title">Secondary</div><div class="color-group">{sec}</div></div>
</div>
<div class="divider"></div>
<div class="row-comps">
<div class="comp-col">
<div class="comp-col-title">Buttons</div>
<button class="btn" style="background:{s['btn_bg']};color:{s['btn_color']};">{BTN_PRIMARY}</button>
<button class="btn" style="background:transparent;color:{s['btn_bg']};border:1px solid {s['btn_bg']};">{BTN_SECONDARY}</button>
</div>
<div class="comp-col">
<div class="comp-col-title">Input</div>
<input class="input-demo" style="border-color:{s['input_border']};" value="{INPUT_PLACEHOLDER}" readonly>
</div>
<div class="comp-col">
<div class="comp-col-title">Card</div>
<div class="card-demo" style="border-color:{s['card_border']};">
<div class="card-title">{CARD_TITLE}</div>
<div class="card-desc">{CARD_DESC}</div>
</div>
</div>
<div class="comp-col">
<div class="comp-col-title">Tags</div>
<span class="tag" style="background:{s['tag_bg']};color:{s['tag_color']};">{TAG_A}</span>
<span class="tag" style="background:{s['tag_bg']};color:{s['tag_color']};">{TAG_B}</span>
<div class="toggle" style="background:{s['toggle_bg']};"></div>
</div>
</div>
</div>'''
tabs_html = ""
for i, s in enumerate(schemes):
ac = " active" if i == 0 else ""
tabs_html += f'<div class="scheme-tab{ac}"
panels_html = ""
for i, s in enumerate(schemes):
panels_html += render_scheme(s, active=(i == 0))
html = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{project_title} — Design System Mood Board</title>
<style>
{LOCKED_CSS}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>{project_title} — Design System Mood Board</h1>
<p>{persona_line} · {jtbd_line}</p>
</div>
<div class="scheme-nav">{tabs_html}</div>
{panels_html}
</div>
<script>
function switchTab(id) {{
document.querySelectorAll('.scheme-tab').forEach(function(t) {{ t.classList.remove('active'); }});
document.querySelectorAll('.scheme-panel').forEach(function(p) {{ p.classList.remove('active'); }});
event.currentTarget.classList.add('active');
document.getElementById('panel-' + id).classList.add('active');
}}
</script>
</body>
</html>'''
import os
os.makedirs("spark-output/board", exist_ok=True)
board_path = f"spark-output/board/{project_slug}-moodboard.html"
with open(board_path, "w", encoding="utf-8") as f:
f.write(html)
print(f"Done: {os.path.getsize(board_path)/1024:.0f} KB")
Phase 6: present_files 交付
present_files spark-output/board/{project_slug}-moodboard.html
让用户在 3-4 套方案标签页之间切换比较,回复"我选 X"或"用方案 B"等指令进入 Phase 7。
Phase 7: 用户选定后双通道输出(按 chain-protocol §2.1 v1.1.1 顺序,不得颠倒)
Step 7.1 — 先写盘
把选中方案的色彩 / 字体 / 组件 token 序列化为 design-tokens.json 结构,写入 spark-output/context/board.json(不存在则创建目录):
{
"skill": "board",
"generated_at": "2026-06-01T12:00:00Z",
"project_name": "...",
"selected_scheme": "方案B",
"style_keywords": ["内敛","专业","可信","温润"],
"color": {
"primary": "#7C4DFF",
"secondary": "#00BCD4",
"neutral": { "50":"#F9FAFB","100":"#F3F4F6","500":"#6B7280","900":"#111827" }
},
"typography": { "heading_font": "Space Grotesk", "body_font": "Inter" },
"spacing": { "xs":"4px","sm":"8px","md":"16px","lg":"24px","xl":"32px" },
"radius": { "sm":"4px","md":"8px","lg":"16px","xl":"24px","full":"9999px" },
"component": {
"button": { "bg":"#7C4DFF","color":"#fff","padding":"6px 16px","font_size":"11px" },
"input": { "border":"#B388FF","bg":"#F9FAFB","width":"120px" },
"card": { "border":"#EDE7F6","padding":"10px","max_width":"140px" },
"tag": { "bg":"#EDE7F6","color":"#5E35B1","font_size":"9px" }
},
"touchpoints": [
{ "id":1, "name":"...", "emotions":["..."], "override":{ "color":{ "accent":"#xxx" } } }
]
}
Step 7.2 — 自检行
chat 输出一行:
✅ board.json 已写盘到 spark-output/context/board.json(selected_scheme=方案B,primary=#7C4DFF)
Step 7.3 — present_files 交付 HTML(如 Phase 6 已交付,本步可跳过)
Step 7.4 — 紧凑 marker
<!-- spark-context:board ref="spark-output/context/board.json" -->
Board 已保存:project=xxx,selected_scheme=方案B,primary=#7C4DFF,font=Space Grotesk
<!-- /spark-context:board -->
Step 7.5 — Handoff 引导
按"页面 / 动效 / 数据"三类完整覆盖:
接下来推荐:
- 📄
/Web页面设计或/mobile页面设计:消费 board.color + component,生成符合视觉系统的页面- 🎬
/动效规划:消费 board.typography + style_keywords,推导 Personality archetype(Playful / Premium / Corporate / Energetic)- 📊
/数据可视化:消费 board.color.primary/secondary,生成符合配色的图表规范
Step 7.6 — 更新链路面板
按 chain-protocol §9 流程刷新 spark-output/dashboard.html,并在 Handoff 末尾告知:
📊 链路面板已更新:spark-output/dashboard.html(双击在浏览器打开)
Pitfalls
- Python f-string 中禁止出现中文引号
"",用「」替代 - base64 字符串太长不能直接用 shell 变量传递,必须先写
b64data.json再 Python 读取 sips仅 macOS,Linux 改用convert -resize 400x- 组件文案不能用通用占位符("按钮"/"文本"),必须匹配产品上下文
- Phase 7 严禁先输出 marker 再写盘——OPC + MuleTeam 已验证这个顺序最易导致 board.json 缺失
- 写盘失败时不允许静默跳过:必须在自检行输出
⚠️ 文件系统不可用,已降级为 chat-only marker(含完整 JSON)
References
| File | Purpose |
|---|---|
| emotion-visual-mapping.md | Phase 3: 情绪→色彩/字体映射查询表 |
质量规范
本章节是 Skill 完成度的高层判定标准,与 Phase 5/7 内部的执行级约束互补。Phase 是"该怎么做",本章节是"做对了没有"。
🚫 红线规则(违反即任务失败,无降级空间)
- HTML 必须基于 LOCKED_CSS 逐字克隆——不得新增、删除、修改任何选择器或属性值(详见 CRITICAL RULES #1)
- HTML 必须完全自包含——禁止任何外部 CDN / link / script / image URL,所有图片以
data:image/jpeg;base64,...内嵌 - 必须输出双通道:
spark-output/context/board.json写盘 + chat 内紧凑 marker(含ref=属性) - 必须按 chain-protocol §2.1 v1.1.1 顺序执行:Step 7.1 写盘 → 7.2 自检 → 7.3 present → 7.4 marker → 7.5 handoff → 7.6 dashboard,不得颠倒
- 方案数必须为 3 或 4——少于 3 套用户无对比,多于 4 套决策疲劳
- 每套方案必须含 4 行结构:mood-row(4格) + row-type + row-colors(Primary+Secondary) + row-comps(4列),少一行即失败
⚠️ 反模式(常见错误,需主动规避)
- ❌ 凭记忆重写 LOCKED_CSS 或"简化"为 Tailwind / Bootstrap 版本
- ❌ 链式模式下还重新追问触点 / 行业 / 受众——已在 Phase 0.5 从 brief / journey 读到的字段不得二次提问
- ❌ 组件文案写"按钮"/"文本"等通用占位符——必须匹配产品上下文(教育→"开始学习";电商→"立即下单";金融→"立即投资")
- ❌ 在 chat 内重复输出完整 design-tokens JSON——应只输出紧凑 marker(≤ 80 字摘要),完整 JSON 写盘
- ❌ Phase 7 先 marker 后写盘——必须先写盘
- ❌ 色条 flex 比例不是 8:4:3:2:2、
.toggle之外出现 border-radius > 4px、出现 box-shadow 属性 - ❌ ImageGen 不可用时直接 abort——必须降级为 4 格全 CSS gradient
✅ 质量标准(通过条件,全部满足才算交付)
内容完整性:
- 触点 3-8 个,每个含名称 + 情绪关键词(1-3) + pain level
- 方案 3 或 4 套,每套 8 个必需字段(name/desc/keywords/font/primary/secondary + 7 个组件颜色)齐全
- 每个 primary / secondary 数组恰好 5 色(500主 / 300浅 / 700深 / 100最浅 / 900最深)
- 组件文案与产品语境匹配(非通用占位符)
HTML 产物完整性(Phase 5 自检清单全部通过):
- 无
border-radius> 4px(.toggle的 8px 除外) - 无
box-shadow属性 - 色条 flex 比例 = 8:4:3:2:2
.mood-imgheight = 100px.type-display .aafont-size = 120px.comp-colpadding = 0 15px, gap = 10px.btnpadding = 6px 16px.input-demowidth = 120px.card-demomax-width = 140px- 所有图片 =
data:image/jpeg;base64,...(无外部URL) - body padding = 48px 40px
- 方案数 = 3 或 4,每方案 4 个
.mood-img - 每方案有 Primary + Secondary 两个
.color-group - Tab 切换 JS 存在且功能正确
- 无任何外部依赖(无 CDN link、无 external script)
链路接入正确性:
spark-output/context/board.json文件已写入且 schema 符合 frontmatter 定义- chat marker 含
ref="spark-output/context/board.json"属性 - 下游 Skill(flow-web / flow-mobile / motion-plan / chart)调用时能正确读取本 Board 上下文
- 已输出符合 Step 7.5 模板的 Handoff 引导(覆盖页面 / 动效 / 数据三类)
spark-output/dashboard.html已刷新(链路面板生成失败仅 warning,不阻断交付)