Skill: tinayunyunyun/travel-outfit-planner (version 1.0.0)

--- name: travel-outfit-planner description: "Generate travel capsule-wardrobe plans with outfit formulas, packing lists, layering strategy, and visual lookbook. Use for trip outfits, what to pack, cross-climate wardrobe, or outfit inspiration." metadata: clawdbot: requires: bins: ["python3"] --- # 胶囊衣橱生成器 **重要：所有输出必须使用中文（简体），包括标题、表格内容、说明文字、检索词、图片说明。** 把"我要去哪里玩几天，怎么穿、怎么带"转成一份真正能落地的旅行穿搭方案：衣物尽量少、搭配尽量多、信息尽量实用、展示尽量直观。 ## 目标 同时解决 4 件事：1）根据行程/天气/场景/风格做胶囊衣橱；2）让用户一眼看懂每天怎么穿；3）保留旅行场景里真正有用的提醒（洗衣、打包、防盗、礼仪、购物留位）；4）在能力允许时加入图片示意，否则给出清晰可搜的参考方向。 ## 必须遵守 - **先实用，再好看**：旅行可走、可拍、可应对温差，优先级高于单纯时髦。 - **锦上添花，不要减内容**：加入图片/色彩示意后，不能删掉行李策略、注意事项、礼仪提醒、防盗提示。 - **不编造**：不能编造天气来源、图片链接、品牌、平台帖子或具体图像内容。 ## 核心原则 ### 1. 胶囊衣橱原则 - 70% 基础款 + 30% 点睛单品，控制在 3-4 个主色以内 - 优先防皱、速干、好打理、可压缩面料 - 默认建议把最重的鞋和外套穿在路上 ### 2. 温差应对原则 - 温差超过 10°C 时，优先输出"底层 + 中层 + 外层"分层方案 - 天气表里直接告诉用户是"薄内搭 + 薄外套"还是"厚内搭 + 厚外套/三层叠穿" ### 3. 旅行完整性原则 除了衣服本身，还要主动覆盖：行李分装和收纳策略、洗衣计划（7天+）、目的地礼仪/着装规范、防盗/天气/防晒提醒、购物留位、特殊场景装备。 ## 工作流 ### Step 1：收集并压缩用户信息 必需字段：destinations、start_date、days、scenes、travelers。可选：style_pref。缺失再补问，补问尽量少。 摘要只写 3 行：行程（城市+天数，用 `+` 连接）、风格偏好、出行人。出发日期不要在摘要中出现（体现在天气表和日历中即可）。用"出行人"代替"性别"。多人出行写清楚是谁。 ### Step 2：输出天气概况 优先获取真实天气；做不到就用同期历史均温并标注"仅供参考"。 天气表必须包含 `目的地 | 日期 | 温度/天气 | 天气特点 | 建议穿搭` 五列。"建议穿搭"列必须具体到"什么内搭 + 什么外套"的组合，禁止写"注意保暖"等空话。 ### Step 3：生成胶囊衣橱 按行程长度控制规模：5-7天 7-9件、8-14天 9-11件、15天+ 10-13件（配合洗衣计划）。夫妻/多人每人分别生成。 #### 色彩系统 写明主色、辅助色、点缀色，写出 2-3 条配色公式。色彩系统段落下必须加入配色参考图片示意（有图用图，无图用 `🔍 小红书搜索：[检索词]`）。 #### 单品表 必须使用 5 列结构：`编号 | 单品 | 颜色/材质 | 适用场景 | 图片示意`，"图片示意"列不可省略。无真实图片时用 `🔍 小红书搜索：[风格+颜色+季节+性别+场景]`。 ### Step 4：输出每日穿搭日历 每天至少覆盖：日期、目的地、场景、穿搭公式（用单品编号引用）、鞋履、配件、天气提示、图片示意。每天必须给 1 条图片示意（有图用图，无图用景点+穿搭风格的组合检索词）。 详细示例见 `{baseDir}/references/outfit-examples.md`。 ### Step 5：补全旅行策略与注意事项（不可省略） 这一步是旅行技能区别于纯穿搭推荐的核心价值。必须覆盖： - **5.1 打包清单**：去重汇总所有物品，可勾选格式 - **5.2 行李打包策略**：分装建议、登机穿法、购物留位、收纳技巧 - **5.3 洗衣计划**：7天内可不洗但建议速干轮换；7-14天标注自助洗衣店；14天+必须详细轮换计划 - **5.4 特别注意事项**：着装礼仪、防盗提示、天气应对、步行舒适、特殊场景、购物提醒 ## 欧洲/大温差场景增强规则 当行程在欧洲、跨多个气候带、或温差 > 10°C 时，必须额外增加"温差穿搭策略"小节。温差策略要写成按温度带分的表格（含直观穿法、适用场景、图片示意），不要只说"洋葱式穿搭"。多人出行时男女方案必须分开写，用 👩/👨 区分。 详细的温差表格示例、男女分开写法示例、欧洲旅行提醒清单见 `{baseDir}/references/outfit-examples.md`。 ## 图片示意规则 图片必须出现在 3 个地方：**色彩系统**、**单品表**、**每日穿搭日历**。 - 有联网能力时优先从小红书搜索真实图片 - 无法取图时用 `🔍 小红书搜索：[检索词]` 替代，绝不编造链接 - 检索词要具体到：地点 + 季节 + 性别 + 风格 + 核心单品 + 场景 详细的检索词写法规范和好/差示例见 `{baseDir}/references/outfit-examples.md`。 ## 输出格式 按以下 8 个章节输出，**每个章节都不可省略**：一、天气概况 → 二、温差穿搭策略（大温差时必须有） → 三、胶囊衣橱（色彩系统+单品表） → 四、场景搭配速看 → 五、每日穿搭日历 → 六、打包清单 → 七、行李打包策略 → 八、特别注意事项。 开头用 3 行摘要（行程/风格/出行人），不要重复出发日期。完整的输出格式模板见 `{baseDir}/references/outfit-examples.md`。 ## Step 6：生成 HTML 视觉灵感板（Lookbook）——必须执行 **每次使用本 skill 时，必须同时输出两样东西：** 1. **完整的 Markdown 穿搭方案**（Step 1-5 的全部内容，不可省略任何章节） 2. **HTML 视觉灵感板文件**（本步骤生成的 `wardrobe-lookbook.html`） 生成流程：准备 JSON 数据（结构详见 `{baseDir}/references/lookbook-guide.md`）→ 运行 `python3 {baseDir}/scripts/fetch_lookbook_images.py --input wardrobe_data.json --output wardrobe-lookbook.html`（有 Pexels API Key 时加 `--pexels-key YOUR_KEY`）→ 输出 HTML 文件。 在 Markdown 末尾必须加上： > 📸 我还为你生成了一个**视觉灵感板** `wardrobe-lookbook.html`，包含配色展示、场景氛围图和小红书一键搜索按钮，在浏览器中打开即可查看。 ## Edge Cases | 情况 | 处理方式 | |------|----------| | 拿不到实时天气 | 使用历史均温，标注仅供参考 | | 用户风格笼统 | 默认 1 套稳妥方向 + 1 套可替换风格 | | 行程 > 14 天 | 必须补充洗衣计划和轮换策略 | | 极端跨气候 | 接受件数略增，仍按分层逻辑收敛 | | 用户已有衣物 | 先复用已有，再补足缺口 | | 无法联网取图 | 用检索词替代，绝不编造图链 | | 欧洲多人出行 | 男女方案分开写，用 👩/👨 区分 | | 无 Pexels API Key | CSS 占位符 + 小红书搜索引导 | | 用户不需要灵感板 | 可跳过 Step 6 | ## 下游联动 打包清单天然适合继续给 **flight-baggage-check** 做行李合规检查。结尾可主动补一句： > 如果你愿意，我也可以继续帮你检查这套清单是否符合所乘航司的行李尺寸和重量要求。

path: README.md
content: # Capsule Wardrobe Generator

Generate travel capsule-wardrobe plans with outfit formulas, packing lists, layering strategy, and a visual lookbook HTML.

## Features

- **Capsule wardrobe planning**: Minimize items, maximize outfits across multi-destination trips
- **Daily outfit calendar**: Day-by-day outfit formulas with weather tips
- **Color coordination**: Cohesive palette with mix-and-match formulas
- **Visual lookbook**: Auto-generated HTML with scene photos (via Pexels API), color swatches, and Xiaohongshu search links
- **Packing checklist**: Deduplicated list with luggage strategy
- **Travel reminders**: Etiquette, safety, weather, and shopping tips

## Installation

```bash
clawhub install <username>/travel-outfit-planner
```

Or manually copy the skill folder to `~/.claude/skills/travel-outfit-planner/`.

## Requirements

- **Python 3.8+** (for the lookbook HTML generator script)
- **Pexels API Key** (optional, free at [pexels.com](https://www.pexels.com/api/)) — enables real scene photos in the lookbook

## Usage

Ask your AI assistant about trip outfits:

- "我下周去日本7天，帮我规划穿搭"
- "京都+大阪5天旅行，女生，日系清新风格，怎么带衣服"
- "欧洲三国15天，情侣出行，跨气候穿搭方案"

The skill outputs:
1. A complete Markdown wardrobe plan (outfit formulas, packing list, daily calendar, travel tips)
2. An HTML visual lookbook (`wardrobe-lookbook.html`) with color swatches, scene photos, and search links

## File Structure

```
travel-outfit-planner/
├── SKILL.md                          # Skill definition (frontmatter + instructions)
├── README.md                         # This file
├── scripts/
│   └── fetch_lookbook_images.py      # Lookbook HTML generator (Python, stdlib only)
└── references/
    └── lookbook-guide.md             # JSON schema and module reference for the lookbook
```

## License

MIT-0 (MIT No Attribution)


path: scripts/fetch_lookbook_images.py
content: #!/usr/bin/env python3
"""
Capsule Wardrobe Lookbook Generator — Editorial Edition
========================================================
从穿搭方案 JSON 数据生成编辑式（editorial）风格的 HTML 视觉灵感板。

设计理念：高端旅行杂志排版——大量留白、精确字体层级、不对称布局、有节奏的空间。
参考：Anthropic harness design 前端设计评估标准 + chenglou/pretext 极简排版。

图片来源：
1. Pexels API（免费 200次/小时，推荐）
2. 无 API Key 时：CSS 占位符 + 小红书搜索引导（Unsplash Source URL 已弃用）

用法：
    python fetch_lookbook_images.py --input wardrobe_data.json --output lookbook.html
    python fetch_lookbook_images.py --input wardrobe_data.json --output lookbook.html --pexels-key YOUR_KEY
"""

from __future__ import annotations

import argparse
import json
import sys
import urllib.parse
import urllib.request
from pathlib import Path

# ---------------------------------------------------------------------------
# 中英文关键词映射表
# ---------------------------------------------------------------------------
KEYWORD_MAP: dict[str, str] = {
    "京都": "kyoto japan", "大阪": "osaka japan", "东京": "tokyo japan",
    "富士山": "mount fuji japan", "奈良": "nara japan deer temple",
    "北海道": "hokkaido japan", "冲绳": "okinawa japan beach",
    "巴黎": "paris france", "伦敦": "london england",
    "罗马": "rome italy", "佛罗伦萨": "florence italy",
    "威尼斯": "venice italy", "米兰": "milan italy",
    "瑞士": "switzerland alps", "苏黎世": "zurich switzerland",
    "因特拉肯": "interlaken switzerland", "少女峰": "jungfrau switzerland",
    "巴塞罗那": "barcelona spain", "阿姆斯特丹": "amsterdam netherlands",
    "布拉格": "prague czech", "维也纳": "vienna austria",
    "纽约": "new york city", "洛杉矶": "los angeles",
    "旧金山": "san francisco", "首尔": "seoul korea",
    "曼谷": "bangkok thailand", "新加坡": "singapore",
    "悉尼": "sydney australia", "清迈": "chiang mai thailand",
    "三亚": "sanya hainan beach", "丽江": "lijiang yunnan",
    "成都": "chengdu sichuan", "上海": "shanghai china", "香港": "hong kong",
    "神社": "japanese shrine torii", "寺庙": "temple",
    "樱花": "cherry blossom sakura", "枫叶": "autumn maple leaves",
    "雪景": "snow winter landscape", "海滩": "beach ocean",
    "徒步": "hiking trail mountain", "博物馆": "museum gallery",
    "咖啡厅": "cafe coffee shop", "教堂": "cathedral church",
    "城堡": "castle", "老城区": "old town cobblestone street",
    "夜景": "city night lights", "市集": "market bazaar",
    "花园": "garden park", "湖泊": "lake scenic",
    "雪山": "snow mountain peak", "温泉": "hot spring onsen",
    "和服体验": "kimono japanese traditional",
    "正式晚餐": "fine dining restaurant elegant",
    "街拍": "street style fashion", "逛街": "shopping street",
    "拍照打卡": "photo spot scenic view",
    "风衣": "trench coat", "针织开衫": "cardigan knitwear",
    "衬衫": "shirt blouse", "T恤": "t-shirt tee",
    "毛衣": "sweater knit", "羽绒服": "down jacket puffer",
    "冲锋衣": "windbreaker jacket outdoor", "西装外套": "blazer suit jacket",
    "牛仔裤": "jeans denim", "阔腿裤": "wide leg pants",
    "A字裙": "a-line skirt", "连衣裙": "dress", "半裙": "midi skirt",
    "运动鞋": "sneakers", "帆布鞋": "canvas shoes",
    "乐福鞋": "loafers", "踝靴": "ankle boots",
    "徒步鞋": "hiking boots", "围巾": "scarf",
    "帽子": "hat cap", "墨镜": "sunglasses",
    "斜挎包": "crossbody bag", "双肩包": "backpack",
    "日系清新": "japanese minimalist fresh style",
    "法式松弛": "french effortless chic",
    "韩系简约": "korean minimal style",
    "smart casual": "smart casual outfit",
    "户外运动": "outdoor sporty activewear",
    "通勤": "commute office casual", "度假": "vacation resort style",
    "春季": "spring", "夏季": "summer", "秋季": "autumn fall", "冬季": "winter",
}


# ---------------------------------------------------------------------------
# Utility functions
# ---------------------------------------------------------------------------
def translate_keywords(chinese_terms: list[str]) -> str:
    en_parts = []
    for term in chinese_terms:
        term = term.strip()
        if not term:
            continue
        if term in KEYWORD_MAP:
            en_parts.append(KEYWORD_MAP[term])
        else:
            matched = False
            for cn, en in KEYWORD_MAP.items():
                if cn in term:
                    en_parts.append(en)
                    matched = True
                    break
            if not matched:
                en_parts.append(term)
    return " ".join(en_parts)


def fetch_pexels(query: str, api_key: str, per_page: int = 3) -> list[dict]:
    url = "https://api.pexels.com/v1/search?" + urllib.parse.urlencode(
        {"query": query, "per_page": per_page, "orientation": "landscape"}
    )
    req = urllib.request.Request(url, headers={
        "Authorization": api_key,
        "User-Agent": "CapsuleWardrobe/1.0",
    })
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read().decode())
        results = []
        for photo in data.get("photos", []):
            results.append({
                "url": photo["src"].get("large2x", photo["src"].get("large", photo["src"]["original"])),
                "url_medium": photo["src"].get("medium", photo["src"].get("small", "")),
                "alt": photo.get("alt", query),
                "photographer": photo.get("photographer", ""),
                "src_page": photo.get("url", ""),
            })
        return results
    except Exception as e:
        print(f"  [WARN] Pexels API failed for '{query}': {e}", file=sys.stderr)
        return []


def get_image(keywords_cn: list[str], api_key: str | None = None) -> dict:
    """获取图片。有 Pexels API Key 时调用 API，否则返回空（由 HTML 渲染 CSS 占位符）。"""
    en_query = translate_keywords(keywords_cn)
    if [REDACTED_SECRET] = fetch_pexels(en_query, api_key, per_page=1)
        if results:
            return {**results[0], "source": "pexels"}
    return {
        "url": "", "url_medium": "",
        "alt": " ".join(keywords_cn),
        "photographer": "", "src_page": "", "source": "placeholder",
    }


def xhs_url(keywords: str) -> str:
    return "https://www.xiaohongshu.com/search_result?" + urllib.parse.urlencode(
        {"keyword": keywords, "source": "web_search_result_notes"}
    )


def esc(s: str) -> str:
    return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")


def text_color(hex_color: str) -> str:
    h = hex_color.lstrip("#")
    if len(h) != 6:
        return "#fff"
    r, g, b = int(h[:2], 16), int(h[2:4], 16), int(h[4:6], 16)
    return "#1a1a1a" if (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.55 else "#fafafa"


def render_image(img: dict, css_class: str, keywords_cn: list[str] | None = None) -> str:
    """渲染图片或 CSS 占位符。"""
    img_url = img.get("url", "") or img.get("url_medium", "")
    alt = esc(img.get("alt", ""))
    source = img.get("source", "")

    if img_url and source != "placeholder":
        credit = ""
        photographer = img.get("photographer", "")
        if photographer and source == "pexels":
            credit = f'<div class="scene-credit">Photo by {esc(photographer)} / Pexels</div>'
        return f'<img class="{css_class}" src="{esc(img_url)}" alt="{alt}" loading="lazy">\n{credit}'
    else:
        label = alt or " ".join(keywords_cn or [])
        return f'''<div class="{css_class} placeholder" aria-label="{esc(label)}">
      <span class="placeholder__label">{esc(label)}</span>
      <span class="placeholder__hint">配置 Pexels API Key 可显示真实场景图</span>
    </div>'''


# ---------------------------------------------------------------------------
# HTML Generation — Editorial Style
# ---------------------------------------------------------------------------
def generate_html(data: dict, api_key: str | None = None) -> str:
    title = data.get("title", "旅行胶囊衣橱灵感板")
    summary = data.get("summary", {})
    colors = data.get("colors", {})
    items = data.get("items", [])
    daily = data.get("daily", [])
    scenes = data.get("scenes", [])

    scene_imgs = []
    for s in scenes:
        img = get_image(s.get("keywords_cn", []), api_key)
        scene_imgs.append({**s, "image": img})

    daily_imgs = []
    for d in daily:
        img = get_image(d.get("keywords_cn", []), api_key)
        daily_imgs.append({**d, "image": img})

    parts = [
        build_head(title), build_hero(title, summary), build_palette(colors),
        build_scenes(scene_imgs), build_items(items),
        build_daily(daily_imgs), build_foot()
    ]
    return "\n".join(parts)


# ---------------------------------------------------------------------------
# HTML Sections
# ---------------------------------------------------------------------------
def build_head(title: str) -> str:
    return f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{esc(title)}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Noto+Serif+SC:wght@400;700&family=Noto+Sans+SC:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{{margin:0;padding:0;box-sizing:border-box}}
:root{{
  --ink:#1a1a1a;--paper:#f8f6f3;--stone:#e8e4df;--mist:#c8c3bc;
  --warm:#a09080;--accent:#8b6f4e;--accent-light:#c4a87c;
  --serif:'Playfair Display','Noto Serif SC',Georgia,serif;
  --sans:'Noto Sans SC',-apple-system,BlinkMacSystemFont,sans-serif;
  --max:1080px;--gutter:clamp(20px,4vw,48px)
}}
html{{font-size:16px;-webkit-font-smoothing:antialiased;scroll-behavior:smooth}}
body{{font-family:var(--sans);background:var(--paper);color:var(--ink);line-height:1.75;font-weight:400}}
img{{display:block;max-width:100%;height:auto}}
a{{color:var(--accent);text-decoration:none}}
a:hover{{text-decoration:underline}}

/* ---- Hero ---- */
.hero{{
  max-width:var(--max);margin:0 auto;
  padding:clamp(60px,12vh,140px) var(--gutter) clamp(40px,8vh,80px);
  border-bottom:1px solid var(--stone)
}}
.hero__kicker{{
  font-family:var(--sans);font-size:11px;font-weight:600;
  letter-spacing:3px;text-transform:uppercase;color:var(--warm);
  margin-bottom:20px
}}
.hero__title{{
  font-family:var(--serif);font-size:clamp(32px,5vw,56px);
  font-weight:400;line-height:1.15;letter-spacing:-0.02em;
  max-width:16ch;margin-bottom:28px
}}
.hero__meta{{
  font-size:14px;color:var(--warm);line-height:2;
  border-left:2px solid var(--accent-light);padding-left:16px
}}

/* ---- Section ---- */
.section{{
  max-width:var(--max);margin:0 auto;
  padding:clamp(48px,8vh,96px) var(--gutter)
}}
.section+.section{{border-top:1px solid var(--stone)}}
.section__label{{
  font-family:var(--sans);font-size:11px;font-weight:600;
  letter-spacing:3px;text-transform:uppercase;color:var(--warm);
  margin-bottom:12px
}}
.section__heading{{
  font-family:var(--serif);font-size:clamp(24px,3.5vw,36px);
  font-weight:400;line-height:1.2;letter-spacing:-0.01em;
  margin-bottom:clamp(24px,4vh,48px)
}}

/* ---- Palette ---- */
.palette{{display:flex;gap:2px;border-radius:8px;overflow:hidden;margin-bottom:24px}}
.swatch{{
  flex:1;min-width:0;aspect-ratio:1/0.6;
  display:flex;flex-direction:column;align-items:center;justify-content:flex-end;
  padding:8px 6px;transition:flex .3s ease;max-height:80px
}}
.swatch:hover{{flex:1.3}}
.swatch:hover{{flex:1.6}}
.swatch__name{{font-size:11px;font-weight:500;margin-bottom:1px}}
.swatch__hex{{font-size:9px;opacity:.7;font-family:'SF Mono',Menlo,monospace}}
.formulas{{
  font-size:14px;line-height:2.2;color:var(--warm);
  padding-left:0;list-style:none
}}
.formulas li::before{{content:"—";margin-right:8px;color:var(--mist)}}
.xhs-link{{
  display:inline-flex;align-items:center;gap:6px;
  margin-top:16px;padding:8px 20px;
  border:1px solid var(--stone);border-radius:100px;
  font-size:13px;font-weight:500;color:var(--ink);
  transition:all .2s ease
}}
.xhs-link:hover{{background:var(--ink);color:var(--paper);border-color:var(--ink);text-decoration:none}}

/* ---- Scenes ---- */
.scene-spread{{margin-bottom:clamp(40px,6vh,72px)}}
.scene-spread__img{{
  width:100%;aspect-ratio:16/9;object-fit:cover;
  border-radius:4px;margin-bottom:20px
}}
.scene-spread__body{{
  display:grid;grid-template-columns:1fr 1fr;gap:24px;
  align-items:start
}}
.scene-spread__left h3{{
  font-family:var(--serif);font-size:clamp(20px,2.5vw,28px);
  font-weight:400;line-height:1.3;margin-bottom:8px
}}
.scene-spread__left p{{font-size:14px;color:var(--warm)}}
.scene-spread__right{{
  font-size:14px;line-height:1.8;
  border-left:2px solid var(--accent-light);padding-left:20px;
  color:var(--ink)
}}
.scene-spread__right .outfit{{font-weight:500;margin-bottom:8px}}
.scene-credit{{font-size:11px;color:var(--mist);margin-top:8px}}

/* ---- Items ---- */
.items-list{{list-style:none;padding:0}}
.items-list li{{
  display:grid;grid-template-columns:36px 1fr auto;gap:16px;
  align-items:baseline;
  padding:14px 0;border-bottom:1px solid var(--stone)
}}
.items-list li:last-child{{border-bottom:none}}
.item-num{{
  font-family:var(--serif);font-size:18px;font-weight:400;
  color:var(--accent);text-align:right
}}
.item-name{{font-weight:500;font-size:15px}}
.item-detail{{font-size:13px;color:var(--warm);margin-top:2px}}
.item-action{{
  font-size:12px;color:var(--warm);
  border:1px solid var(--stone);border-radius:100px;
  padding:4px 14px;white-space:nowrap;transition:all .2s
}}
.item-action:hover{{background:var(--ink);color:var(--paper);border-color:var(--ink);text-decoration:none}}

/* ---- Daily ---- */
.day-entry{{
  display:grid;grid-template-columns:320px 1fr;gap:clamp(20px,3vw,40px);
  margin-bottom:clamp(32px,5vh,56px);align-items:start
}}
.day-entry:nth-child(even){{direction:rtl}}
.day-entry:nth-child(even)>*{{direction:ltr}}
.day-entry__img{{
  width:100%;aspect-ratio:4/5;object-fit:cover;border-radius:4px
}}
.day-entry__body{{padding-top:8px}}
.day-entry__label{{
  font-family:var(--serif);font-size:clamp(18px,2.2vw,24px);
  font-weight:400;line-height:1.3;margin-bottom:4px
}}
.day-entry__scene{{font-size:13px;color:var(--warm);margin-bottom:16px}}
.day-entry__line{{font-size:14px;line-height:1.8;margin-bottom:4px}}
.day-entry__line strong{{font-weight:500}}
.day-entry__weather{{
  font-size:13px;color:var(--warm);
  margin-top:12px;padding:10px 14px;
  background:var(--stone);border-radius:6px
}}

/* ---- Placeholder (no-API-key fallback) ---- */
.placeholder{{
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  background:linear-gradient(135deg,var(--stone) 0%,#d5cfc7 50%,var(--stone) 100%);
  position:relative;overflow:hidden
}}
.placeholder::before{{
  content:"";position:absolute;inset:0;
  background:repeating-linear-gradient(
    -45deg,transparent,transparent 8px,
    rgba(255,255,255,.04) 8px,rgba(255,255,255,.04) 16px
  )
}}
.placeholder__label{{
  font-family:var(--serif);font-size:clamp(16px,2vw,22px);
  font-weight:400;color:var(--warm);text-align:center;
  padding:0 24px;position:relative;z-index:1
}}
.placeholder__hint{{
  font-size:11px;color:var(--mist);margin-top:8px;
  position:relative;z-index:1
}}

/* ---- Footer ---- */
.foot{{
  max-width:var(--max);margin:0 auto;
  padding:40px var(--gutter);
  border-top:1px solid var(--stone);
  font-size:12px;color:var(--mist);text-align:center
}}

/* ---- Responsive ---- */
@media(max-width:720px){{
  .scene-spread__body{{grid-template-columns:1fr}}
  .day-entry{{grid-template-columns:1fr}}
  .day-entry:nth-child(even){{direction:ltr}}
  .day-entry__img{{aspect-ratio:16/9}}
  .palette .swatch{{aspect-ratio:1/1}}
}}
</style>
</head>
<body>'''


def build_hero(title: str, summary: dict) -> str:
    trip = esc(summary.get("trip", ""))
    style = esc(summary.get("style", ""))
    traveler = esc(summary.get("traveler", ""))
    meta = ""
    if trip:
        meta += f"行程 {trip}<br>"
    if style:
        meta += f"风格 {style}<br>"
    if traveler:
        meta += f"出行人 {traveler}"
    return f'''
<header class="hero">
  <div class="hero__kicker">Capsule Wardrobe Lookbook</div>
  <h1 class="hero__title">{esc(title)}</h1>
  <div class="hero__meta">{meta}</div>
</header>'''


def build_palette(colors: dict) -> str:
    if not colors:
        return ""
    swatches = colors.get("swatches", [])
    formulas = colors.get("formulas", [])
    xhs_kw = colors.get("xhs_search", "")

    sw_html = ""
    for s in swatches:
        n, h = esc(s.get("name", "")), esc(s.get("hex", "#ccc"))
        tc = text_color(h)
        sw_html += f'<div class="swatch" style="background:{h};color:{tc}"><span class="swatch__name">{n}</span><span class="swatch__hex">{h}</span></div>\n'

    fm_html = ""
    if formulas:
        fm_html = '<ul class="formulas">' + "".join(f"<li>{esc(f)}</li>" for f in formulas) + "</ul>"

    xhs_html = ""
    if xhs_kw:
        xhs_html = f'<a class="xhs-link" href="{xhs_url(xhs_kw)}" target="_blank">在小红书搜索配色参考 →</a>'

    return f'''
<section class="section">
  <div class="section__label">01 — 色彩系统</div>
  <h2 class="section__heading">配色方案</h2>
  <div class="palette">{sw_html}</div>
  {fm_html}
  {xhs_html}
</section>'''


def build_scenes(scene_imgs: list[dict]) -> str:
    if not scene_imgs:
        return ""
    cards = ""
    for s in scene_imgs:
        img = s.get("image", {})
        name = esc(s.get("name", ""))
        desc = esc(s.get("description", ""))
        outfit = esc(s.get("outfit_hint", ""))
        xhs_kw = s.get("xhs_search", "")
        keywords_cn = s.get("keywords_cn", [])

        img_html = render_image(img, "scene-spread__img", keywords_cn)

        xhs_html = ""
        if xhs_kw:
            xhs_html = f'<a class="xhs-link" href="{xhs_url(xhs_kw)}" target="_blank" style="margin-top:12px">搜索穿搭参考 →</a>'

        cards += f'''
  <div class="scene-spread">
    {img_html}
    <div class="scene-spread__body">
      <div class="scene-spread__left">
        <h3>{name}</h3>
        <p>{desc}</p>
      </div>
      <div class="scene-spread__right">
        <div class="outfit">{outfit}</div>
        {xhs_html}
      </div>
    </div>
  </div>'''

    return f'''
<section class="section">
  <div class="section__label">02 — 场景氛围</div>
  <h2 class="section__heading">目的地与穿搭</h2>
  {cards}
</section>'''


def build_items(items: list[dict]) -> str:
    if not items:
        return ""
    li_html = ""
    for item in items:
        num = item.get("id", "")
        name = esc(item.get("name", ""))
        cm = esc(item.get("color_material", ""))
        sc = esc(item.get("scenes", ""))
        xhs_kw = item.get("xhs_search", "")
        action = ""
        if xhs_kw:
            action = f'<a class="item-action" href="{xhs_url(xhs_kw)}" target="_blank">搜索同款</a>'
        li_html += f'''<li>
      <span class="item-num">{num}</span>
      <div><div class="item-name">{name}</div><div class="item-detail">{cm} · {sc}</div></div>
      {action}
    </li>'''

    return f'''
<section class="section">
  <div class="section__label">03 — 胶囊单品</div>
  <h2 class="section__heading">精选单品清单</h2>
  <ul class="items-list">{li_html}</ul>
</section>'''


def build_daily(daily_imgs: list[dict]) -> str:
    if not daily_imgs:
        return ""
    entries = ""
    for d in daily_imgs:
        img = d.get("image", {})
        keywords_cn = d.get("keywords_cn", [])
        label = esc(d.get("day_label", ""))
        scene = esc(d.get("scene", ""))
        outfit = esc(d.get("outfit", ""))
        shoes = esc(d.get("shoes", ""))
        acc = esc(d.get("accessories", ""))
        weather = esc(d.get("weather_tip", ""))
        xhs_kw = d.get("xhs_search", "")

        img_html = render_image(img, "day-entry__img", keywords_cn)

        lines = ""
        if outfit:
            lines += f'<div class="day-entry__line"><strong>穿搭</strong> {outfit}</div>'
        if shoes:
            lines += f'<div class="day-entry__line"><strong>鞋履</strong> {shoes}</div>'
        if acc:
            lines += f'<div class="day-entry__line"><strong>配件</strong> {acc}</div>'
        w_html = f'<div class="day-entry__weather">{weather}</div>' if weather else ""
        xhs_html = ""
        if xhs_kw:
            xhs_html = f'<a class="xhs-link" href="{xhs_url(xhs_kw)}" target="_blank" style="margin-top:12px">搜索穿搭灵感 →</a>'

        entries += f'''
  <div class="day-entry">
    {img_html}
    <div class="day-entry__body">
      <div class="day-entry__label">{label}</div>
      <div class="day-entry__scene">{scene}</div>
      {lines}
      {w_html}
      {xhs_html}
    </div>
  </div>'''

    return f'''
<section class="section">
  <div class="section__label">04 — 每日穿搭</div>
  <h2 class="section__heading">逐日穿搭日历</h2>
  {entries}
</section>'''


def build_foot() -> str:
    return '''
<footer class="foot">
  Capsule Wardrobe Lookbook · Editorial Edition
</footer>
</body>
</html>'''


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main():
    parser = argparse.ArgumentParser(description="Generate wardrobe lookbook HTML (editorial style)")
    parser.add_argument("--input", required=True, help="Input JSON file path")
    parser.add_argument("--output", required=True, help="Output HTML file path")
    parser.add_argument("--pexels-key", default=None, help="Pexels API key (optional)")
    args = parser.parse_args()

    input_path = Path(args.input)
    if not input_path.exists():
        print(f"Error: Input file not found: {input_path}", file=sys.stderr)
        sys.exit(1)

    data = json.loads(input_path.read_text(encoding="utf-8"))
    html = generate_html(data, [REDACTED_SECRET])

    output_path = Path(args.output)
    output_path.write_text(html, encoding="utf-8")
    print(f"Generated: {output_path}")
    print(f"File size: {output_path.stat().st_size / 1024:.1f} KB")


if __name__ == "__main__":
    main()


path: references/lookbook-guide.md
content: # 视觉灵感板（Lookbook）详细参考

本文档是 `travel-outfit-planner` 技能的 Step 6 补充参考，包含 JSON 数据结构、灵感板模块说明和降级策略。

## JSON 数据结构

灵感板的输入是一个 JSON 文件，结构如下：

```json
{
  "title": "日本7天旅行胶囊衣橱灵感板",
  "summary": {
    "trip": "京都 3天 + 大阪 2天 + 富士山区域 2天",
    "style": "日系清新",
    "traveler": "女生"
  },
  "colors": {
    "swatches": [
      {"name": "奶白", "hex": "#F5F0EB"},
      {"name": "灰绿", "hex": "#8FA68B"}
    ],
    "formulas": [
      "奶白上衣 + 浅米下装 + 灰绿针织外搭"
    ],
    "xhs_search": "京都 春季 日系清新 奶白 灰绿 穿搭配色"
  },
  "items": [
    {
      "id": 1,
      "name": "棉麻衬衫",
      "color_material": "奶白 / 棉麻",
      "scenes": "京都神社、大阪街拍",
      "xhs_search": "奶白棉麻衬衫 女生 春季 京都拍照"
    }
  ],
  "scenes": [
    {
      "name": "Day 1-3 · 京都",
      "description": "神社巡礼 + 樱花拍照 + 和服体验",
      "outfit_hint": "#1 奶白棉麻衬衫 + #4 浅米A字裙 + #2 灰绿针织开衫",
      "keywords_cn": ["京都", "神社", "樱花", "春季"],
      "xhs_search": "京都 神社 樱花 日系清新 女生穿搭"
    }
  ],
  "daily": [
    {
      "day_label": "Day 1 — 4月7日（京都）",
      "scene": "伏见稻荷大社 + 清水寺",
      "outfit": "#1 奶白棉麻衬衫 + #4 浅米A字裙 + #2 灰绿针织开衫",
      "shoes": "#7 白色帆布鞋",
      "accessories": "编织草帽、帆布斜挎包",
      "weather_tip": "最高 18°C / 最低 9°C，早晚需要针织外搭",
      "keywords_cn": ["京都", "神社", "樱花", "春季"],
      "xhs_search": "京都 伏见稻荷 樱花 日系清新 女生穿搭"
    }
  ]
}
```

### 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `title` | string | 灵感板标题 |
| `summary.trip` | string | 行程概要 |
| `summary.style` | string | 穿搭风格 |
| `summary.traveler` | string | 旅行者类型（女生/男生/情侣等） |
| `colors.swatches` | array | 配色色块，每个包含 `name`（中文色名）和 `hex`（十六进制色值） |
| `colors.formulas` | array | 配色公式文字描述 |
| `colors.xhs_search` | string | 小红书配色搜索词 |
| `items` | array | 胶囊单品列表 |
| `items[].id` | number | 单品编号 |
| `items[].xhs_search` | string | 小红书单品搜索词 |
| `scenes` | array | 场景列表（按目的地分组） |
| `scenes[].keywords_cn` | array | 中文关键词（用于 Pexels 图片搜索） |
| `daily` | array | 每日穿搭日历 |
| `daily[].weather_tip` | string | 天气提示 |

## 灵感板包含的模块

| 模块 | 内容 | 图片来源 |
|------|------|----------|
| 色彩系统 | CSS 色块展示主色/辅助色/点缀色 + 配色公式 | 无需图片（纯 CSS） |
| 场景氛围 | 每个目的地/场景一张氛围图 + 穿搭建议 | Pexels API / CSS 占位符 |
| 胶囊单品 | 每件单品的卡片 + 小红书搜索按钮 | 小红书搜索链接 |
| 每日日历 | 场景图 + 穿搭公式 + 天气提示 + 搜索按钮 | Pexels API / CSS 占位符 |

## 脚本使用方式

```bash
# 无 API Key（降级模式，显示 CSS 占位符）
python3 {baseDir}/scripts/fetch_lookbook_images.py --input wardrobe_data.json --output wardrobe-lookbook.html

# 有 Pexels API Key（推荐，图片质量更高）
python3 {baseDir}/scripts/fetch_lookbook_images.py --input wardrobe_data.json --output wardrobe-lookbook.html --pexels-key YOUR_PEXELS_API_KEY
```

## 降级策略

| 环境 | 处理方式 |
|------|----------|
| 有 Pexels API Key | 调用 Pexels API 获取高质量场景图（200次/小时免费） |
| 无 API Key | 显示精心设计的 CSS 占位符（场景关键词 + 渐变背景）+ 小红书搜索引导按钮 |

> **注意**：Unsplash Source URL 已于 2024 年完全弃用，不再作为降级方案。推荐用户注册免费的 Pexels API Key 以获得最佳体验。


path: references/outfit-examples.md
content: # 详细示例与场景增强规则

本文档是 `travel-outfit-planner` 技能的补充参考，包含详细的输出示例、欧洲/大温差场景增强规则和图片检索词写法规范。SKILL.md 中会引用本文档，使用时按需读取。

---

## 色彩系统示例

### 配色参考图示意表

| 展示项 | 图片示意 |
|--------|----------|
| 整体配色参考 | 🔍 小红书搜索：京都 春季 日系清新 奶白 灰绿 裸粉 穿搭配色 |
| 户外分层配色 | 🔍 小红书搜索：富士山 4月 女生 户外穿搭 大地色系 分层搭配 |

### 单品表示例（5 列结构）

| 编号 | 单品 | 颜色/材质 | 适用场景 | 图片示意 |
|------|------|-----------|----------|----------|
| 1 | 棉麻衬衫 | 奶白/棉麻 | 京都神社、大阪街拍 | 🔍 小红书搜索：奶白棉麻衬衫 女生 春季 京都拍照 |
| 2 | 针织开衫 | 灰绿/羊毛混纺 | 早晚温差、咖啡厅 | 🔍 小红书搜索：灰绿针织开衫 女生 日系 春季搭配 |
| 3 | 轻薄风衣 | 卡其/防水面料 | 富士山、雨天 | 🔍 小红书搜索：卡其风衣 女生 春季 户外 日本旅行 |

---

## 每日穿搭日历示例

```
### Day 1 — 4月7日（京都）｜神社巡礼 + 樱花拍照

- **穿搭**：#1 奶白棉麻衬衫 + #4 米色A字裙 + #2 灰绿针织开衫
- **鞋履**：#6 白色帆布鞋
- **配件**：编织草帽、帆布斜挎包
- **天气提示**：最高 18°C / 最低 9°C，早晚需要针织外搭
- **图片示意**：🔍 小红书搜索：京都 神社 樱花 日系清新 针织开衫 A字裙 女生穿搭
```

---

## 欧洲/大温差场景增强规则

当满足任一条件时，必须额外增加"温差穿搭策略"小节：

- 行程在欧洲
- 行程跨多个气候带
- 最高温和最低温差值 > 10°C

### 温差穿搭策略表

不要只说"洋葱式穿搭"这种笼统建议。请把策略写成用户一眼能抄的表格，直接告诉用户在什么温度穿什么：

| 温度带 | 直观穿法 | 适用场景 | 图片示意 |
|--------|----------|----------|----------|
| 20°C 以上 | 单穿 T 恤/衬衫 + 裤/裙 | 意大利城市午后、南法海边 | 🔍 小红书搜索：意大利 夏季 女生 轻薄穿搭 城市漫步 |
| 10-18°C | 薄内搭 + 针织/西装/风衣 | 巴黎早晚、博物馆、通勤感街拍 | 🔍 小红书搜索：巴黎 春季 女生 风衣 针织 smart casual |
| 4-10°C | 保暖内搭 + 毛衣/抓绒 + 羽绒/冲锋衣 | 瑞士山区、清晨徒步 | 🔍 小红书搜索：瑞士 徒步 女生 三层穿搭 羽绒 冲锋衣 |

### 欧洲多人穿搭：男女分开写

如果是夫妻、情侣、男女同行，**必须把男女示例分开写**，不要揉在一段里。每条示例简洁到一行能看懂。

#### 👩 女士场景搭配公式

| 场景 | 穿搭公式 | 图片示意 |
|------|----------|----------|
| 巴黎城市漫步 | 条纹上衣 + 直筒牛仔裤 + 风衣 + 白色运动鞋 | 🔍 小红书搜索：巴黎 女生 条纹衫 风衣 街拍 |
| 正式晚餐 | 真丝衬衫/连衣裙 + 中裙/西裤 + 乐福鞋/踝靴 | 🔍 小红书搜索：欧洲 正式晚餐 女生 smart elegant 穿搭 |
| 瑞士徒步 | 保暖底层 + 速干裤 + 抓绒/羽绒 + 冲锋衣 + 徒步鞋 | 🔍 小红书搜索：瑞士 女生 徒步穿搭 户外 春季 |

#### 👨 男士场景搭配公式

| 场景 | 穿搭公式 | 图片示意 |
|------|----------|----------|
| 巴黎城市漫步 | 白T/POLO + 深色休闲裤 + 轻外套 + 运动鞋 | 🔍 小红书搜索：巴黎 男生 休闲穿搭 POLO 轻外套 |
| 正式晚餐 | 衬衫 + 美丽诺毛衣/西装外套 + 长裤 + 皮鞋 | 🔍 小红书搜索：欧洲 正式晚餐 男生 smart casual 西装 |
| 瑞士徒步 | 功能底层 + 徒步裤 + 羽绒背心/冲锋衣 + 徒步鞋 | 🔍 小红书搜索：瑞士 男生 徒步穿搭 户外 冲锋衣 |

### 欧洲旅行提醒清单

欧洲场景必须补充以下内容（这些是旅行硬需求，不是可选项）：

- **正式晚餐 dress code**：哪些餐厅需要 smart casual 或以上，男女分别需要什么
- **教堂/宗教场所**：遮肩、过膝要求，建议随身带一条大围巾
- **扒手和防盗**：巴黎地铁、罗马景点等高发区域，斜挎包/防盗腰包建议，手机和钱包分开放
- **石板路/步行**：欧洲老城石板路多，鞋底要防滑有支撑，高跟鞋基本不可行
- **瑞士山区**：风大、紫外线强、天气多变，防水防滑防晒三件套
- **购物留位与洗衣**：行李箱预留空间，标注哪些城市有自助洗衣

---

## 图片检索词写法规范

检索词要具体到能直接复制粘贴到小红书搜索框使用，包含以下要素：

- **地点**：具体城市或景点名
- **季节/月份**：春季、4月等
- **性别**：女生/男生
- **风格关键词**：日系清新、smart casual、户外等
- **核心单品**：风衣、针织、A字裙等
- **场景**：拍照、徒步、逛街、晚餐等

### 好的检索词示例

- `🔍 小红书搜索：京都 神社 樱花 日系清新 风衣 A字裙 女生穿搭`
- `🔍 小红书搜索：巴黎 春季 男生 smart casual 西装外套 博物馆`

### 差的检索词示例（太笼统，搜不到有用结果）

- `🔍 小红书搜索：日本穿搭`
- `🔍 小红书搜索：欧洲旅行衣服`

### 图片来源优先级

1. **小红书公开内容**：优先从小红书搜索穿搭参考图（中文用户最常用的穿搭参考平台）
2. **品牌 lookbook、公开穿搭图**
3. **Pinterest、公开图库、公开旅行街拍**
4. **以上都拿不到时**：输出高质量检索词（格式：`🔍 小红书搜索：[关键词]`），绝不编造链接

### 图片的作用

图片不是装饰，必须帮助用户更快理解：

- 配色关系（颜色之间怎么搭）
- 单品版型（什么样的衬衫、什么样的裤型）
- 场景气质（这套穿搭在那个景点是什么感觉）
- 景点和穿搭的匹配感（樱花 + 浅色系、雪山 + 户外风）

---

## 完整输出格式模板

```markdown
# [目的地/行程]旅行胶囊衣橱方案

> 行程：京都 3天 + 大阪 2天 + 富士山区域 2天
> 风格偏好：日系清新
> 出行人：女生

> ⚠️ 天气数据基于同期历史均温，仅供参考，出发前请查看实时天气预报。

## 一、天气概况

| 目的地 | 日期 | 温度/天气 | 天气特点 | 建议穿搭 |
|--------|------|-----------|----------|----------|
| 京都 | 4/7-4/9 | 10-19°C / 多云转晴 | 早晚凉，午后温暖 | 薄内搭 + 针织/风衣 |

## 二、温差穿搭策略
[欧洲/大温差时必须有，含温度带表格和图片示意；其他情况可按需省略]

## 三、胶囊衣橱（[N]件主力单品 × [M]+ 种组合）

### 色彩系统
- 主色：
- 辅助色：
- 点缀色：
- 配色公式：

| 展示项 | 图片示意 |
|--------|----------|
| 配色参考图 | 🔍 小红书搜索：[具体检索词] |

### 上装 / 下装 / 外套 / 鞋 / 配件

| 编号 | 单品 | 颜色/材质 | 适用场景 | 图片示意 |
|------|------|-----------|----------|----------|

## 四、场景搭配速看
[多人出行时用 👩/👨 分开写"女士场景搭配公式""男士场景搭配公式"]

## 五、每日穿搭日历

### Day 1 — [日期]（[目的地]）｜[场景]
- **穿搭**：#编号 单品名 + #编号 单品名 + ...
- **鞋履**：
- **配件**：
- **天气提示**：
- **图片示意**：🔍 小红书搜索：[景点 + 穿搭风格 + 关键单品]

[逐日重复]

## 六、打包清单
[去重后的衣物、鞋、配件、洗护、天气应对物品，可勾选格式]

## 七、行李打包策略
[分装建议、登机穿法、购物留位、收纳技巧、洗衣计划]

## 八、特别注意事项
[着装礼仪、防盗提示、天气应对、步行舒适、特殊场景、购物提醒等]
```


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)