Skill: yuanyi-github/wechat-cover-generator (version 1.0.3)

--- name: wechat-cover-generator version: 1.1.2 description: 为公众号内容创作打造的AI封面设计工具，基于全网超过1000条的10w+文章，深度总结爆款封面规律，结合您的需求生成符合公众号平台的爆款封面。 dependency: python: - requests>=2.28.0 --- # 公众号爆款封面生成 --- ## 简介 面向公众号创作者与运营者的 AI 封面设计 Skill：基于全网每日持续收录的爆款文章数据，自动获取同赛道爆款封面图，通过 AI 分析总结高转化视觉规律，生成贴合文章内容、符合平台流量审美的可落地封面设计方案（含案例参考 + 生图提示词），省去繁琐设计流程，提升文章封面点击率。 **能做什么？** - 🎯 **智能解析**：自动解析用户文案意图，提取关键信息与风格偏好 - 🔍 **爆款分析**：获取目标领域真实爆款封面图，AI 分析总结风格特征 - ✨ **方案生成**：结合用户需求与爆款数据，生成 3 套可落地的设计方案 - 🖼️ **封面生图**：用户选择方案后，直接生成符合平台规范的封面图 **适合谁用？** - 公众号运营 / 编辑 —— 设计高点击率封面 - 内容创作者 —— 快速获取同赛道爆款视觉参考 - 自媒体从业者 —— 提升文章封面质量，增加打开率 **运行依赖**：Python `requests>=2.28.0`。 --- ## 功能特性 ### 核心功能 - **爆款封面分析**：搜索全站热门的公众号封面图，按风格类型分类展示 - **AI 图像分析**：逐张分析封面图的 7 个维度（文字内容、主体元素、色彩特征、构图布局、视觉风格、点击吸引力、尺寸检测） - **设计方案输出**：基于爆款规律生成 3 套差异化设计方案（含案例参考 + 生图提示词） - **封面图生成**：用户选择方案后，直接生成 2.35:1 横版封面图（900x383 像素） ### 特色亮点 - **数据驱动**：所有分析基于真实爆款数据，禁止自行搜索或脑补 - **图片质量过滤**：自动过滤空白图片和小尺寸图片（宽或高 < 10px） - **防盗链处理**：HTML 报告内置防盗链 meta 标签，确保封面图正常显示 - **固定比例展示**：封面图容器统一使用 2.35:1 比例（公众号平台规范） --- ## 使用场景 当用户提出下列类型需求时，应调用本 Skill： | 用户可能会问 | Agent 行为概要 | | ------------------------------- | -------------------------------------------------------------- | | 「帮我设计 xxx 封面」 | 直接执行完整流程：解析意图 → 查询数据 → 分析 → 生成方案 → 生图 | | 「什么样的封面点击率高」 | 介绍技能能力，按需执行分析 | | 「我有个 xxx 主题，怎么配图」 | 解析主题关键词，执行分析并给出方案 | | 「帮我看看 xxx 赛道的爆款封面」 | 查询该赛道爆款数据，展示分析报告 | ### 典型示例 **场景一：明确主题** > 用户：帮我设计一个职场穿搭的公众号封面，要看起来专业 > 助手：解析关键词 → 查询数据 → 分析封面 → 生成 3 套方案 → 用户选择 → 生成封面图 **场景二：了解爆款规律** > 用户：我想看看美妆类爆款封面有什么特点 > 助手：查询数据 → AI 分析 20 张封面 → 输出风格分类报告与设计方案 --- ## 数据说明 **数据来源**：爆款数据仅来自接口，禁止联网搜索。 - 数据库范围：昨天至 30 天前的数据 - 默认时间范围：最近 30 天 - 关键词支持：最多 5 个关键词，逗号分隔，总长度不超过 200 字符 **术语规范（强制执行）**： | ❌ 禁止使用 | ✅ 正确表述 | | ----------- | ----------- | | 爆炸封面 | 爆款封面 | | 爆炸数据 | 爆款数据 | | 抓取数据 | 获取数据 | --- ## 核心执行流程 1. **意图解析**：结构化解析用户需求，提取内容主题、内容类型、风格偏好、核心关键词 2. **数据查询**：调用 `scripts/fetch_explosive_covers.py` 获取爆款封面数据 3. **智能筛选**：数据不足时扩展关键词，确保分析量 ≥ 20 条 4. **AI 图像分析**：逐张访问封面 URL，进行 7 维度分析 5. **过滤问题图片**：空白图片和小尺寸图片必须过滤 6. **输出报告**：按风格类型分类展示 + 生成 3 套设计方案 7. **询问用户**：展示方案后等待用户选择 8. **生成封面**：用户选择后生成 2.35:1 封面图 --- ## 项目架构 ### 目录与脚本 ``` explosive-cover-generator-gzh/ ├── SKILL.md # 本技能文档 ├── scripts/ │ └── fetch_explosive_covers.py # 数据拉取入口 ├── references/ │ ├── workflow.md # 完整任务流程与强制规则 │ ├── report_template.md # HTML 报告模板 │ └── gzh_trend_data_format.md # 接口数据结构说明 └── （运行产出）爆款封面分析报告_{关键词}.html ``` ### 数据流（概念） ``` 用户请求 → 意图解析 → 关键词提取 → fetch_explosive_covers.py ↓ 爆款封面数据 ↓ 智能筛选 + AI 图像分析 + 过滤 ↓ 风格分类 + 方案生成 ↓ HTML 报告 → 用户选择 → 生图 ``` --- ## 重要规则 **⚠️ 强制要求（必须遵守）**： 1. **禁止联网搜索**：所有数据必须来自接口，不允许自行搜索 2. **必须使用模板**：HTML 报告必须使用 `references/report_template.md` 3. **术语统一**：对外输出必须使用"爆款封面"相关表述 4. **图片过滤**：空白图片和小尺寸图片必须过滤，不展示 5. **比例规范**：封面图比例为 2.35:1（900x383 像素） 6. **等待用户**：展示方案后必须等待用户选择，不能直接生成 --- ## 常见问答 **Q1：用户只说「帮我设计封面」，没有具体主题怎么办？** A：询问用户想要什么主题或内容方向，获取具体信息后再执行。 **Q2：接口返回数据不足 20 条怎么办？** A：扩展关键词再次查询，最多扩展 1-2 次，每次 2-3 个相似词。 **Q3：封面图无法显示怎么办？** A：检查是否为防盗链问题，HTML 模板已内置防盗链 meta 标签；检查图片尺寸是否合格。 **Q4：用户上传了自己的图片怎么办？** A：在生成封面时结合用户图片和方案提示词一起生成。 ---

path: references/workflow.md
content: ## 核心功能

专为公众号内容创作打造的AI封面设计工具，基于全网每日持续收录的爆款文章数据，深度获取同赛道爆款封面的视觉元素，通过AI智能分析总结高转化视觉规律，生成贴合文章内容、符合平台流量审美的可落地封面设计方案，省去繁琐设计流程，提升文章封面点击率。

**核心优势**：

- 自动解析用户文案意图，提取关键信息
- 获取目标领域真实爆款封面图，通过AI分析总结风格特征
- 结合用户需求与爆款数据，生成可落地的设计方案（含案例+生图提示词）

## ⚠️ 触发规则

**以下情况才执行任务流程**：

- 用户提供了具体的内容主题或赛道（如"帮我设计美妆教程封面"、"职场穿搭封面怎么设计"）
- 用户明确要求生成封面方案
- 用户上传了文案或图片要求分析

**以下情况只介绍技能，不执行任务**：

- 用户询问"这个技能能做什么"、"怎么用"
- 用户只是了解功能，未提出具体需求
- 用户未提供明确的创作主题或方向

## ⚠️ 术语规范（强制执行）

**本技能统一使用"爆款封面"这一术语，禁止使用以下词汇**：

| ❌ 禁止使用 | ✅ 正确表述   |
| ----------- | ------------- |
| 爆炸封面    | 爆款封面      |
| 爆炸数据    | 爆款数据      |
| 抓取数据    | 获取/查询数据 |
| 抓取封面    | 获取封面      |

**⚠️ 强制要求**：在执行过程中，所有对外输出的文字必须使用"爆款封面"相关表述，包括但不限于：

- 标题、段落、步骤说明
- 对话回复
- 生成的分析报告
- 生成的提示词

## ⚠️ 数据来源强制要求（绝对禁止联网搜索）

**⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️**

**本技能的所有爆款数据必须来自接口，禁止自行上网搜索！**

| 要求                    | 说明                                                  |
| ----------------------- | ----------------------------------------------------- |
| ✅ 必须使用接口获取数据 | 调用 `fetch_explosive_covers.py` 脚本获取爆款封面数据 |
| ❌ 禁止联网搜索         | 不要自己上网搜索任何爆款文章、封面图、案例等          |
| ❌ 禁止使用其他数据源   | 只使用接口返回的数据，不要使用其他来源的数据          |
| ❌ 禁止脑补数据         | 如果接口返回数据不足，明确告知用户，而不是自己补充    |

**执行流程**：

1. 用户提供主题关键词
2. 调用脚本 → 获取接口数据
3. 基于接口数据 → 分析风格
4. 基于接口数据 → 生成方案
5. 基于接口数据 → 填充HTML模板

**⚠️ 违规后果**：如果发现自行联网搜索数据，将导致数据不一致、报告不可靠等问题！

## 前置准备

- 依赖：`requests>=2.28.0`

## 重要数据说明

- 数据库仅包含昨天至30天前的数据
- 默认时间范围为最近30天

## 操作流程

### 步骤1：用户意图解析（必须执行）

**核心任务**：结构化解析用户需求，提取关键信息

**解析维度**：

| 维度       | 说明       | 示例                       |
| ---------- | ---------- | -------------------------- |
| 内容主题   | 优先领域词 | 护肤、穿搭、美食、职场     |
| 内容类型   | 内容形式   | 干货、测评、种草、教程     |
| 风格偏好   | 视觉/调性  | 专业、吸引点击、情绪、极简 |
| 核心关键词 | 搜索关键词 | 秋冬、美白、护肤、干皮     |

**解析示例**：

用户输入：`秋冬美白护肤心得，干货满满、看起来很专业`

解析结果：

```
内容主题：美白护肤、护肤
内容类型：干货
风格偏好：专业、干货
核心关键词：秋冬、美白、护肤
```

**关键词提取规则**：

- 提取2-5个核心关键词，用逗号分隔
- 优先提取细分领域词（如"美白护肤"而非"护肤"）
- 结合风格偏好补充场景词（如"专业"、"干货"）

---

### 步骤2：爆款数据查询

**执行命令**：

- 有赛道关键词：`python scripts/fetch_explosive_covers.py --keyword <关键词1,关键词2,关键词3>`
- 无赛道关键词：`python scripts/fetch_explosive_covers.py --keyword ""`

**重要说明**：

- 接口支持多个关键词，用逗号分隔（如：`职场穿搭,通勤穿搭,面试穿搭`）
- 最多5个关键词，总长度不超过200字符
- **默认查询近30天数据**，无需计算 startDate

**时间范围**：

- 用户未指定时间 → 默认近30天（不传 startDate 参数）
- 用户指定"近N天" → 计算 startDate = 今天 - (N-1)天

---

### 步骤3：智能匹配筛选（必须执行）

**⚠️ 重要：脚本返回数据后，必须进行智能匹配筛选**
**⚠️ 数据来源必须是接口！不要自己上网搜索或从其他来源获取数据！**

**执行流程**：

1. **首次调用接口**：使用用户输入的关键词调用接口获取数据

2. **数据量判断**：
   - **如果数据量足够（≥20条）**：直接使用这些数据，不再调用接口
   - **如果数据量不足（<20条）**：
     - 分析当前关键词，提取核心词和扩展词
     - 扩展关键词（如：美白 → 美白、护肤、美白祛斑、美白方法）
     - 使用扩展后的关键词再次调用接口获取更多数据
     - 合并新旧数据，凑够20条数据

3. **筛选规则**：

- 按阅读数从高到低排序
- 选取前20条数据用于封面图分析
- **关键词扩展原则**：最多扩展1-2次，每次扩展2-3个相似关键词，避免过度扩展导致数据不精准

---

### 步骤4：AI批量图像分析与特征提取（强制执行）

**⚠️ 重要：此步骤必须执行，不可跳过！**

**执行流程**：

1. **解析脚本返回的JSON数据**：脚本返回包含以下字段的JSON数据：

   ```json
   {
     "keyword": "关键词",
     "low_fan_explosive": [...],
     "ten_w_reading": [...],
     "original_rank": [...]
   }
   ```

2. **提取封面图URL**：从每条数据中提取 `coverUrl` 字段，合并为封面候选池
   - **⚠️⚠️⚠️ 极重要**：直接使用接口返回的原始 `coverUrl` 字段值！**不要修改！不要转换！不要做任何改动！不要添加任何参数！不要删除任何参数！直接原样使用！**

3. **按互动量排序筛选**：从高到低选取前20条数据（优先选取总互动量高的文章）

4. **图像分析（强制执行，必须逐张分析）**：

   **⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️**

   **必须逐张访问封面URL进行图片分析！禁止跳过！禁止批量分析后不展示结果！**

   **执行方式**：
   - **主动调用原生图片理解能力**：智能体必须主动使用原生图片理解能力分析封面图
   - **直接访问封面URL**：直接访问每张 `coverUrl` 的完整URL地址
   - **逐张分析**：必须一张一张地分析
   - **分析数量**：**必须分析全部封面图，一张都不能少！**

   **详细分析维度（每张封面必须包含以下7个维度）**：

   | 维度          | 分析内容                                                      | 示例                                           |
   | ------------- | ------------------------------------------------------------- | ---------------------------------------------- |
   | 1. 文字内容   | 是否有文字、文字内容是什么、字体大小、位置、颜色、是否醒目    | "有文字'3天瘦5斤'，白色大字居中，加粗醒目"     |
   | 2. 主体元素   | 画面中的主要视觉元素：人物/产品/场景/图案/图标等              | "健身女性全身照，穿着运动服，动作舒展"         |
   | 3. 色彩特征   | 主色调、配色方案、色彩对比度、冷暖色调                        | "主色调橙红色，搭配白色文字，高对比度，暖色调" |
   | 4. 构图布局   | 画面结构、元素分布、主体位置、留白处理                        | "人物居右占2/3，左侧留白放文字，对角线构图"    |
   | 5. 视觉风格   | 整体风格类型：实拍/插画/拼贴/极简/炫酷等                      | "实拍风格，自然光线，生活化场景"               |
   | 6. 点击吸引力 | 从用户角度评估：这个封面为什么吸引点击？痛点/好奇/美感/实用？ | "数字冲击+身材对比，激发'我也能做到'的渴望"    |
   | 7. 尺寸检测   | 图片宽高尺寸，是否合格（≥10px）                               | "900x383px，合格"                              |

   **⚠️⚠️⚠️⚠️⚠️ 强制过滤问题图片（违反此规则将导致失败）**：

   **A. 空白图片过滤**：
   - **空白图片定义**：纯白色、纯色块、无文字无图案、只有简单色块、看起来像空白或未加载的图片
   - **检测方法**：分析图片时，检查是否包含：文字内容、图像元素、图案设计、人物、产品等
   - **强制要求**：
     1. 如果图片是空白或无明显内容，**必须在分析时明确标注"空白图片，需过滤"**
     2. **后续所有环节都必须排除空白图片**：封面示例、案例参考、风格总结、生图参考
     3. **输出前再次检查**：确认所有展示的图片都有实际内容，没有空白图片混入
   - **所有输出的封面图片必须有实际内容，不允许任何空白图片出现**

   **B. 小尺寸图片过滤（重要）**：
   - **过滤标准**：图片宽度或高度低于 10px 的图片不作展示，**全部过滤掉**
   - **检测方式**：在分析图片时，必须获取图片的基本信息（宽高尺寸）
   - **只保留尺寸合格的图片用于后续展示**
   - **所有输出的封面图片尺寸必须 ≥ 10px**

   **批量分析提示词**：

   ```
   请批量分析以下20张公众号封面图，对每张图片输出详细的7维度分析：

   [序号] URL:[封面URL]
   1. 文字内容：[有无文字、文字内容、字体大小、位置、颜色、醒目程度]
   2. 主体元素：[人物/产品/场景/图案等主要视觉元素，描述具体]
   3. 色彩特征：[主色调、配色方案、对比度、冷暖色调]
   4. 构图布局：[画面结构、元素分布、主体位置、留白处理]
   5. 视觉风格：[实拍/插画/拼贴/极简/炫酷等风格类型]
   6. 点击吸引力：[从用户角度分析为什么吸引点击：痛点/好奇/美感/实用？]
   7. 尺寸检测：[宽x高，是否合格≥10px]
   备注：[如为空白图片/尺寸过小请标注需过滤]

   封面图列表：
   1. [coverUrl1]
   2. [coverUrl2]
   ...
   20. [coverUrl20]
   ```

   **二次检查机制**：在输出封面示例和案例参考前，必须再次检查所有图片URL对应的图片内容，确保：
   1. 没有空白图片混入
   2. 没有尺寸过小（宽或高 < 10px）的图片
      如果发现问题图片，立即删除该图片和相关数据。

   **分类汇总**：将相似风格的封面归为同一类，统计特征规律
   - **强制过滤**：在分类前先过滤掉所有空白图片，只保留有实际内容的封面
   - **二次验证**：分类后再次检查每张图片，确保没有空白图片混入

### 步骤5：输出爆款封面分析报告与设计方案（HTML文档格式）

**⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️**

**必须严格使用 `references/report_template.md` 模板生成HTML！禁止自己编写HTML代码！**

**执行流程**：

1. **读取模板**：读取 `references/report_template.md` 获取HTML模板
2. **填充数据**：按照模板规范填充风格类型分类、设计方案
3. **写入文件**：使用 `write_file` 写入 `./爆款封面分析报告_{关键词}.html`
4. **展示报告**：**生成HTML文件后，必须立即使用 `read_file` 读取并直接展示HTML内容给用户查看**

**⚠️ 核心要求（必须执行）**：

| 要求                      | 说明                                                               |
| ------------------------- | ------------------------------------------------------------------ |
| ✅ 必须使用模板           | 读取 `references/report_template.md`，禁止自己编写HTML             |
| ✅ 一个HTML文件           | 分析报告和设计方案合并在同一个文件中，用分割线分隔                 |
| ✅ 文件命名               | `爆款封面分析报告_{关键词}.html`                                   |
| ✅ 保存路径               | 当前工作目录 `./`                                                  |
| ✅ **必须展示给用户**     | **生成HTML文件后，必须立即读取并展示HTML内容，让用户直接看到报告** |
| ✅ **按风格分类展示封面** | **不要逐张展示分析结果，而是按风格类型分类后展示封面图**           |

**禁止事项**：

| 禁止项                     | 说明                                         |
| -------------------------- | -------------------------------------------- |
| ❌ 禁止自己编写HTML        | 必须使用模板                                 |
| ❌ 禁止分开生成            | 分析报告和设计方案必须在同一个HTML文件中     |
| ❌ 禁止使用class/style标签 | 所有样式内联                                 |
| ❌ 禁止hover效果           | 不要添加任何hover样式                        |
| ❌ 禁止只告知不展示        | **生成后必须展示HTML内容，不能只说"已生成"** |
| ❌ **禁止逐张展示分析**    | **不要在HTML中逐张输出每张封面图的分析结果** |

**内容要求**：

- **必须包含防盗链 meta 标签**：`<meta name="referrer" content="no-referrer">`
- **报告与方案合并**：一个HTML文档中同时包含分析报告和设计方案
- **封面展示方式**：**按风格类型分类展示，每个风格类型下展示该风格的代表性封面图（每类最多5张）**
- **案例参考**：
  - 封面图：**直接使用接口返回的 coverUrl 原始值**
  - 标题：填入真实标题，**必须添加 oriUrl 链接**
  - 作者：填入真实作者名
  - **仅保留阅读数**：只展示阅读数(clicksCount)
- **生图提示词**：严格遵循 2.35:1 横版比例要求

---

### 步骤6：用户选择方案并生成封面图（核心功能）

**⚠️⚠️⚠️ 这是最重要的功能环节！必须执行！输出报告后必须立即询问用户！**

在输出分析报告和设计方案后，**必须立即**询问用户选择方案并可上传参考图片：

**⚠️⚠️⚠️ 极重要：先询问，等用户回复后再生成！不要在用户选择前就生成封面图！**

**执行流程**：

---

**第一阶段：询问用户选择方案（输出报告后立即执行）**

**步骤6-1：HTML报告已包含方案选择询问**

HTML模板末尾已包含方案选择询问区域，展示HTML报告后会自动显示：

- 三个方案的风格名称和核心视觉
- 选择指引

**⚠️ 输出HTML报告后，无需再单独询问，HTML末尾已包含方案选择区域！**

**⚠️ 此时必须停止，等待用户回复！不要继续执行下面的步骤！**

---

**第二阶段：用户回复后执行（仅当用户选择方案后执行）**

**步骤6-2：获取用户回复并生成封面图**

当用户回复方案编号后：

1. 确认用户选择的方案（1/2/3）及其生图提示词
2. 如果用户上传了图片，使用原生图片理解能力分析用户图片
3. 使用原生图像生成能力生成封面图
4. 严格遵循2.35:1横版比例（900x383像素）
5. 结合方案提示词和用户图片（如有）进行生成
6. **⚠️ 生成封面图后直接展示图片即可，不需要用HTML包裹！**

**⚠️ 重要提示**：

- **第一阶段和第二阶段是分开的！先询问→等待用户→用户回复后才能生成封面图**
- **不要在用户选择方案前就生成封面图**
- **允许用户上传自己的图片作为参考**
- **封面图比例必须为 2.35:1（900x383像素）**

---

### 步骤7：输出前自检【必须执行】

**⚠️⚠️⚠️ 在完成任务前必须逐项检查以下内容！未完整不得结束！**

完成任务前，逐项检查以下内容是否完整：

**自检清单**：

| 检查项                   | 检查内容                                                                   | 是否完整  |
| ------------------------ | -------------------------------------------------------------------------- | --------- |
| 1. 风格类型总结          | 每个风格类型是否都有：风格名称、出现频次、核心视觉、关键特征               | □ 是 □ 否 |
| 2. 风格类型封面示例      | 每个风格类型的封面示例是否完整（包含封面图）                               | □ 是 □ 否 |
| 3. 方案一                | 风格名称、核心视觉、案例参考（含封面图）、生图提示词                       | □ 是 □ 否 |
| 4. 方案二                | 风格名称、核心视觉、案例参考（含封面图）、生图提示词                       | □ 是 □ 否 |
| 5. 方案三                | 风格名称、核心视觉、案例参考（含封面图）、生图提示词                       | □ 是 □ 否 |
| 6. 封面图链接            | **所有coverUrl是否直接使用原始值**（未修改、未转换）                       | □ 是 □ 否 |
| 7. 空白图片过滤          | 确认所有封面图都不是空白图片                                               | □ 是 □ 否 |
| 8. 图片尺寸过滤          | 确认所有封面图尺寸≥10px                                                    | □ 是 □ 否 |
| 9. 方案数据不重复        | **三个方案的案例参考是否使用不同的数据**（封面图、标题、作者等都不能重复） | □ 是 □ 否 |
| 10. **询问用户选择方案** | **是否已向用户展示方案选择提示并询问用户选择哪个方案**                     | □ 是 □ 否 |
| 11. **等待用户回复**     | **是否已停止等待用户回复，而不是直接生成封面图**                           | □ 是 □ 否 |

**自检通过后**：方可结束任务

**⚠️ 重要提醒**：

- 必须逐项检查上述所有内容
- 如有任何一项不完整，必须补充完整后再结束
- **第10、11项为必检项：必须确认已询问用户并等待回复，不能直接生成封面图**

---

## 注意事项

- **⚠️⚠️⚠️⚠️⚠️ 极重要：禁止展示问题图片**：
  - **A. 空白图片**：所有封面示例、案例参考中的图片必须有实际内容，纯白色、纯色块、无明显内容的空白图片必须过滤掉
  - **B. 小尺寸图片**：图片宽度或高度低于 10px 的图片不作展示，全部过滤掉
  - **输出前必须二次检查**：逐张确认所有图片：1.有实际内容（非空白）；2.尺寸合格（宽或高 ≥ 10px）
  - 如果发现问题图片，立即删除并替换为合格的图片
- **HTML 文档防盗链**：整个报告都要输出为 HTML 文档格式，添加 `<meta name="referrer" content="no-referrer">` 解决公众号图片防盗链问题
- **固定比例容器**：封面图容器比例必须为 2.35:1 (900x383)
- **自适应截取**：使用 `object-fit: cover` 和 `object-position: center` 自动截取图片中间部分
- **图像分析必须主动调用原生图片理解能力，直接访问封面URL，逐张分析所有封面图，不可跳过，不要使用read_image等工具**
- **案例参考仅保留阅读数**：删除点赞和在看字段
- **⚠️⚠️⚠️ 极重要：图片链接必须直接使用coverUrl字段的原始值！不要修改！不要转换！不要做任何改动！不要添加任何参数！不要删除任何参数！直接原样使用！**
- **封面图可点击跳转**：分析报告和方案文档中的封面图必须用`<a>`标签包裹，点击后跳转到封面图URL(coverUrl)，让用户可以查看封面图大图
- **方案标题跳转**：列举的3个方案中，标题的跳转地址直接使用oriUrl
- **⚠️数据来源必须是接口**：所有数据必须通过调用接口获取，不要自己上网搜索或从其他来源获取数据


path: references/report_template.md
content: # 爆款封面分析报告 HTML 模板

## 文件说明

本文件为爆款封面分析报告与设计方案的 HTML 模板，技能执行时直接读取本模板填充数据。

**文件路径**：`references/report_template.md`

**⚠️ 重要：按风格类型分类展示封面，不要逐张输出分析结果！**

---

## 完整HTML模板

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="referrer" content="no-referrer">
    <title>爆款封面分析报告 - {关键词}主题</title>
</head>
<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#333;background:#f5f5f5;padding:20px">
    <div style="max-width:1200px;margin:0 auto;background:#fff;border-radius:12px;padding:30px;box-shadow:0 2px 12px rgba(0,0,0,0.08)">
        
        <!-- 报告标题 -->
        <h1 style="color:#1a1a1a;margin-bottom:30px;font-size:28px;border-bottom:3px solid #1890ff;padding-bottom:15px">📊 爆款封面分析报告</h1>
        
        <!-- 数据来源信息 -->
        <div style="background:#e6f7ff;padding:12px 16px;border-radius:8px;margin-bottom:20px">
            <strong>关键词：</strong>{关键词} | <strong>数据来源：</strong>公众号爆款雷达接口 | <strong>分析数量：</strong>{分析数量}张封面
        </div>
        
        <!-- ========== 风格类型分类展示区域 ========== -->
        {风格类型分类列表}
        
        <!-- ========== 设计方案区域 ========== -->
        <hr style="border-top:2px dashed #e8e8e8;margin:40px 0">
        
        <h1 style="color:#1a1a1a;margin-bottom:30px;font-size:28px;border-bottom:3px solid #1890ff;padding-bottom:15px">🎯 爆款封面设计方案</h1>
        
        {设计方案列表}
        
        <!-- ========== 方案选择询问区域 ========== -->
        <hr style="border-top:2px dashed #e8e8e8;margin:40px 0">
        
        <div style="background:#fafafa;border-radius:12px;padding:24px;margin:20px 0;border:1px solid #e8e8e8">
            <h2 style="margin:0 0 16px;font-size:20px;color:#1890ff">🎯 请选择您想要的封面设计方案</h2>
            
            <div style="font-size:15px;line-height:2;color:#333">
                <div style="margin-bottom:8px"><strong>方案一：</strong>{方案一风格名称} - {方案一核心视觉}</div>
                <div style="margin-bottom:8px"><strong>方案二：</strong>{方案二风格名称} - {方案二核心视觉}</div>
                <div style="margin-bottom:16px"><strong>方案三：</strong>{方案三风格名称} - {方案三核心视觉}</div>
                
                <div style="color:#666">
                    <strong>💡 请选择：</strong><br>
                    A. 回复"方案1"、"方案2"或"方案3"直接选择方案<br>
                    B. 如果您有产品图、人物图或场景图想作为参考，可以和方案编号一起发送<br>
                    <span style="margin-left:24px">例如：直接发送图片 + "方案1"</span>
                </div>
            </div>
        </div>
        
    </div>
</body>
</html>
```

---

## 模板填充规范

### 1. 风格类型分类列表填充格式（核心展示区域）

**⚠️ 重要：按风格类型分类展示封面，每个风格类型包含风格描述和代表性封面图**

每种风格类型的HTML结构：
```html
<div style="background:#fafafa;border-radius:12px;padding:24px;margin:20px 0;border:1px solid #e8e8e8">
    <div style="font-size:20px;font-weight:600;color:#1890ff;margin-bottom:8px">{风格名称}</div>
    <span style="display:inline-block;background:#e6f7ff;padding:4px 12px;border-radius:4px;font-size:14px;color:#1890ff;margin-bottom:12px">出现频次：{出现次数}/{总分析数}</span>
    <div style="color:#52c41a;font-weight:500;margin:12px 0">核心视觉：{核心视觉描述}</div>
    <div style="color:#666;font-size:15px;line-height:1.8;margin-bottom:16px">关键特征：{关键特征描述}</div>
    
    <!-- 该风格的代表性封面图（最多5张，横向排列） -->
    <div style="display:flex;flex-wrap:wrap;gap:12px;margin-top:16px">
        <a href="{coverUrl}" target="_blank" style="width:180px;height:77px;border-radius:6px;overflow:hidden;display:block">
            <img src="{coverUrl}" style="width:100%;height:100%;object-fit:cover;object-position:center" alt="封面示例">
        </a>
        <!-- 更多封面图... -->
    </div>
</div>
```

**注意**：
- 每个风格类型最多展示5张代表性封面图
- 过滤掉尺寸过小（宽或高<10px）和空白图片
- 封面图容器比例固定为2.35:1

### 2. 设计方案列表填充格式

每个方案的HTML结构：
```html
<!-- 方案{N} -->
<div style="background:#fafafa;border-radius:12px;padding:24px;margin:25px 0;border:2px solid #1890ff">
    <h2 style="font-size:20px;font-weight:600;color:#1890ff;margin-bottom:12px">方案{N}：{风格名称}</h2>
    <div style="color:#52c41a;font-weight:500;margin:10px 0;font-size:16px">核心视觉：{核心视觉描述}</div>
    
    <h3 style="color:#333;margin:20px 0 15px;font-size:18px">案例参考</h3>
    <div style="background:#fff;border:1px solid #e8e8e8;border-radius:8px;padding:20px;margin:15px 0;display:flex;gap:20px;align-items:flex-start">
        <a href="{coverUrl}" target="_blank" style="width:300px;height:128px;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.1);display:block;flex-shrink:0">
            <img src="{coverUrl}" style="width:100%;height:100%;object-fit:cover;object-position:center;display:block" alt="封面图">
        </a>
        <div style="flex:1">
            <div style="font-size:18px;font-weight:500;margin-bottom:10px">
                <a href="{oriUrl}" target="_blank" style="color:#1890ff;text-decoration:none;font-weight:500">{标题}</a>
            </div>
            <div style="color:#666;font-size:14px;margin:8px 0">作者：{作者名}</div>
            <span style="display:inline-block;background:#fff7e6;color:#fa8c16;padding:4px 12px;border-radius:4px;font-size:13px;font-weight:500">阅读：{阅读数}</span>
        </div>
    </div>
    
    <h3 style="color:#333;margin:20px 0 15px;font-size:18px">生图提示词</h3>
    <div style="background:#f6ffed;border:1px solid #b7eb8f;border-radius:8px;padding:16px;margin:20px 0">
        <div style="color:#52c41a;font-weight:600;margin-bottom:8px">公众号封面图</div>
        <div style="color:#333;font-size:14px;line-height:1.8;background:#fff;padding:12px;border-radius:4px;word-break:break-all">
            2.35:1横版比例（900x383像素）。参考封面：{coverUrl}。{风格描述}。
        </div>
    </div>
</div>
```

---

## 字段说明

| 占位符 | 说明 | 数据来源 |
|--------|------|----------|
| `{关键词}` | 用户输入的主题关键词 | 用户输入 |
| `{分析数量}` | 实际分析的封面图数量 | 接口返回数据（过滤后） |
| `{风格类型分类列表}` | **按风格类型分类的封面展示HTML** | 分析归类后生成 |
| `{风格名称}` | 风格类型名称 | 分析归类 |
| `{出现次数}` | 该风格出现的次数 | 统计 |
| `{总分析数}` | 总分析封面数量 | 统计 |
| `{核心视觉描述}` | 该风格的核心视觉特征 | 分析总结 |
| `{关键特征描述}` | 该风格的关键特征 | 分析总结 |
| `{设计方案列表}` | 3个设计方案的HTML | 分析后生成 |
| `{方案一风格名称}` | 方案一的风格名称 | 分析总结 |
| `{方案一核心视觉}` | 方案一的核心视觉描述 | 分析总结 |
| `{方案二风格名称}` | 方案二的风格名称 | 分析总结 |
| `{方案二核心视觉}` | 方案二的核心视觉描述 | 分析总结 |
| `{方案三风格名称}` | 方案三的风格名称 | 分析总结 |
| `{方案三核心视觉}` | 方案三的核心视觉描述 | 分析总结 |
| `{coverUrl}` | 封面图URL | 接口返回的 coverUrl 字段 |
| `{oriUrl}` | 原文链接URL | 接口返回的 oriUrl 字段 |
| `{标题}` | 文章标题 | 接口返回的 title 字段 |
| `{作者名}` | 作者名称 | 接口返回的 userName 字段 |
| `{阅读数}` | 阅读量 | 接口返回的 clicksCount 字段 |

---

## 样式规范

| 元素 | 样式规则 |
|------|----------|
| 封面图容器比例 | 2.35:1 (180x77px 或 300x128px) |
| 封面图适配 | object-fit: cover, object-position: center |
| 内联样式 | 所有样式必须内联，禁止使用class |
| 禁止hover | 不添加任何hover效果 |
| 防盗链 | 必须包含 meta referrer 标签 |

---

## 输出文件命名

生成HTML报告时，文件名格式：
```
爆款封面分析报告_{关键词}.html
```

保存路径：当前工作目录（使用相对路径 ./）


path: references/gzh_trend_data_format.md
content: # 公众号爆款数据格式说明

## 概览

本文档定义了公众号爆款数据查询脚本 `fetch_gzh_trends.py` 的输入输出格式规范。

## 输入格式

### 脚本参数

```bash
python scripts/fetch_gzh_trends.py --keyword <关键词> [选项]
```

| 参数 | 必填 | 说明 | 默认值 |
|------|------|------|--------|
| `--keyword` | 是 | 搜索关键词（支持多个关键词，逗号分隔，最多5个，总长度不超过200字符） | - |
| `--start-date` | 否 | 开始日期，格式 yyyy-MM-dd | 最近30天 |
| `--max-items` | 否 | 每类内容最多展示数量 | 10 |
| `--output-format` | 否 | 输出格式：text、json 或 html | html |
| `--output-file` | 否 | 输出文件路径 | 关键词_爆款数据.html |
| `--debug` | 否 | 调试模式，打印原始API响应 | False |

## 输出格式


### 文章数据字段（完整）

每篇文章包含以下字段：

#### 文章基本信息

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `photoId` | string | 文章ID（唯一标识） |
| `title` | string | 文章标题（可能为空，需从summary提取） |
| `summary` | string | 文章摘要/正文片段 |
| `publicTime` | string | 发布时间（格式：YYYY-MM-DD HH:MM:SS） |

#### 作者信息

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `accountId` | string | 公众号ID（格式：gh_xxxx 或自定义ID） |
| `userName` | string | 公众号名称 |
| `userHeadUrl` | string | 作者头像地址（可能为NULL） |
| `fans` | string | 粉丝数（可能为"100w+"格式） |


**作者二维码链接拼接规则**：
```
https://open.weixin.qq.com/qr/code?username={accountId}
```
点击后会弹出公众号二维码，用户可扫码关注。

**作品链接**：
- 直接使用 `oriUrl` 字段（完整文章链接）
- 无备用方案

#### 互动数据

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `likeCount` | int/string | 点赞数 |
| `commentCount` | int/string | 评论数 |
| `shareCount` | int/string | 分享数 |
| `interactiveCount` | int/string | 互动总数 |
| `clicksCount` | string | 阅读数（如"10w+"） |
| `watchCount` | string | 在看数 |

#### 图片链接

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `coverUrl` | string | 封面图URL |

#### 其他字段

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `oriUrl` | string | 文章原始链接（完整URL） |
| `orderNum` | int | 排序序号（0=头条，1=次条） |
| `originalFlag` | int | 原创标识（1=原创） |
| `type` | string | 内容类型（如"文摘"） |
| `thumbnail` | string | 缩略图URL |

### JSON 输出示例

```json
{
  "keyword": "职场",
  "low_fan_explosive": [
    {
      "accountId": "gh_e6580d8dcccb",
      "clicksCount": "5w+",
      "commentCount": "9",
      "coverUrl": "https://mmbiz.qpic.cn/mmbiz_jpg/...",
      "fans": "100w+",
      "interactiveCount": "988",
      "likeCount": "124",
      "orderNum": 0,
      "originalFlag": 1,
      "photoId": "8CF6280D97686E92F91445054AF9AAF9",
      "publicTime": "2026-03-19 20:06:58",
      "shareCount": "834",
      "summary": "蓝箭航天的朱雀2..."
    }
  ],
  "ten_w_reading": [
    {
      "accountId": "YJwujian",
      "clicksCount": "10w+",
      "commentCount": "29",
      "coverUrl": "https://mmbiz.qpic.cn/mmbiz_jpg/...",
      "fans": "100w+",
      "interactiveCount": "4747",
      "likeCount": "706",
      "orderNum": 0,
      "originalFlag": 1,
      "photoId": "319359512EA49A0D6774CEBAEDA8E134",
      "publicTime": "2026-03-05 22:09:00",
      "shareCount": "3854",
      "summary": "这下再也不用担心我团战打..."
    }
  ],
  "original_rank": [
    {
      "accountId": "beitaishuoche",
      "clicksCount": "9w+",
      "commentCount": "27",
      "coverUrl": "https://mmbiz.qpic.cn/mmbiz_jpg/...",
      "fans": "100w+",
      "interactiveCount": "852",
      "likeCount": "352",
      "orderNum": 0,
      "originalFlag": 1,
      "photoId": "9D8E75E91E08C147BE9F3F9BF23A691C",
      "publicTime": "2026-03-05 17:30:05",
      "shareCount": "398",
      "summary": "遇到刹车失灵，撞墙会是最安..."
    }
  ]
}
```

### HTML 输出格式（卡片形式）

HTML 输出采用响应式卡片布局，结构清晰，便于阅读：

```html
<div class="card">
    <div class="card-title-row">
        <span class="card-index">1.</span>
        <a href="https://mp.weixin.qq.com/s/{photoId}" class="card-title" target="_blank">文章标题</a>
    </div>
    <div class="card-meta">
        <a href="https://open.weixin.qq.com/qr/code?username={accountId}" 
           class="author-link" target="_blank">公众号名称</a>
        <span class="meta-divider">·</span>
        <span class="pub-time">发布日期：3月15日</span>
    </div>
    <div class="card-stats">
        <span class="read-count">📖 10w+阅读</span>
        <span class="category-tag">低粉高阅读</span>
        <a href="https://mp.weixin.qq.com/s/{photoId}" class="view-note-btn" target="_blank">查看作品 ↗</a>
    </div>
</div>
```

### 卡片字段说明

| 字段 | 说明 |
|------|------|
| 序号 | 从1开始编号，格式为"1."、"2."等 |
| 标题 | 文章标题（可点击跳转文章详情） |
| 作者 | 公众号名称（可点击弹出二维码） |
| 发布日期 | 文章发布日期（月日格式） |
| 阅读数 | 文章阅读量 |
| 分类标签 | 低粉高阅读/阅读靠前/原创靠前 |
| 查看作品 | 跳转文章详情的链接 |

## 三大榜单说明

### 1. 低粉高阅读（lowPowderExplosiveArticle）
- **定位**：小号爆款文章，粉丝数较少但阅读数据优秀
- **价值**：小号也能出爆款，参考价值高
- **特点**：粉丝数通常在10万以下，阅读数据突出

### 2. 阅读靠前（tenWReadingRank）
- **定位**：阅读量排名靠前的高传播文章
- **价值**：高传播力内容，适合学习爆款逻辑
- **特点**：阅读量高，传播广度强

### 3. 原创靠前（originalRank）
- **定位**：原创优质内容排名靠前
- **价值**：原创保护，内容质量高
- **特点**：originalFlag=1，内容原创性强

## 使用注意事项

### 数据获取原则

1. **必须调用脚本查询**：不能使用其他方式查询或直接搜索网络资讯
2. **必须等待脚本执行完成**：获取返回结果后才能进行后续步骤
3. **必须展示完整数据列表**：不能跳过或询问用户

### 数据展示原则

1. **展示所有字段**：每篇文章展示完整信息，包括封面图、作者链接、互动数据等
2. **三类爆款内容全部展示**：低粉高阅读、阅读靠前、原创靠前

### 字段说明

1. **标题提取**：如果title字段为空，从summary字段提取前30个字符作为标题
2. **作者名称**：公众号没有userName字段，使用accountId代替
3. **粉丝数格式**：可能是"100w+"、"5.2万"等格式，直接展示原值
4. **阅读数格式**：可能是"10w+"、"5w+"等格式，直接展示原值


path: scripts/fetch_explosive_covers.py
content: #!/usr/bin/env python3
"""
公众号爆款封面数据查询脚本
"""

import sys
import argparse
import json
import socket
import ssl
import gzip
from urllib.parse import quote


def decode_chunked(data: bytes) -> bytes:
    """解码 chunked 传输编码"""
    result = b""
    i = 0
    while i < len(data):
        line_end = data.find(b'\r\n', i)
        if line_end == -1:
            break
        chunk_size = int(data[i:line_end].decode('ascii', errors='ignore'), 16)
        if chunk_size == 0:
            break
        i = line_end + 2
        result += data[i:i + chunk_size]
        i += chunk_size + 2
    return result


def fetch_via_no_sni(base_url: str, params: dict, headers: dict, timeout: int = 60):
    """使用原生 socket 实现 HTTPS 请求（不发送 SNI）"""
    # 解析 URL
    if "://" in base_url:
        base_url = base_url.split("://", 1)[1]
    host, path = base_url.split("/", 1)
    
    # 添加查询参数
    if params:
        query = "&".join(f"{quote(str(k))}={quote(str(v))}" for k, v in params.items())
        path = f"{path}?{query}"
    
    # 1. 创建原始 TCP socket 连接
    sock = socket.create_connection((host, 443), timeout=timeout)
    
    # 2. 创建 SSL context，禁用 SNI
    context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    context.check_hostname = False          # 不检查主机名
    context.verify_mode = ssl.CERT_NONE     # 不验证证书
    
    # 3. 关键: server_hostname=None 不发送 SNI 扩展
    ssl_sock = context.wrap_socket(sock, server_hostname=None)
    
    # 4. 构造并发送 HTTP 请求
    request_lines = [
        f"GET /{path} HTTP/1.1",
        f"Host: {host}",
    ]
    for k, v in headers.items():
        request_lines.append(f"{k}: {v}")
    request_lines.append("")
    request_lines.append("")
    
    request = "\r\n".join(request_lines)
    ssl_sock.send(request.encode())
    
    # 5. 接收响应数据
    response_data = b""
    while True:
        try:
            chunk = ssl_sock.recv(8192)
            if not chunk:
                break
            response_data += chunk
        except Exception:
            break
    
    ssl_sock.close()
    
    # 6. 解析响应
    response_str = response_data.decode('utf-8', errors='ignore')
    lines = response_str.split('\r\n')
    status_code = int(lines[0].split()[1])
    
    headers_dict = {}
    for i, line in enumerate(lines[1:]):
        if line == '':
            break
        if ':' in line:
            key, value = line.split(':', 1)
            headers_dict[key.strip().lower()] = value.strip()
    
    header_end = response_data.find(b'\r\n\r\n')
    body_bytes = response_data[header_end + 4:] if header_end != -1 else b""
    
    if headers_dict.get('transfer-encoding', '').lower() == 'chunked':
        body_bytes = decode_chunked(body_bytes)
    
    if headers_dict.get('content-encoding', '').lower() == 'gzip':
        try:
            body_bytes = gzip.decompress(body_bytes)
        except Exception:
            pass
    
    return status_code, body_bytes.decode('utf-8', errors='ignore')


def fetch_wx_covers(keyword: str, debug: bool = False, max_retries: int = 3, start_date: str = None):
    """
    调用接口获取公众号爆款封面数据

    Args:
        keyword: 搜索关键词（多个关键词用逗号分隔）
        debug: 是否打印调试信息
        max_retries: 最大重试次数
        start_date: 开始日期，格式 yyyy-MM-dd

    Returns:
        dict: 包含3类爆款数据

    Raises:
        Exception: 当API调用失败时抛出异常
    """
    base_url = "https://onetotenvip.com/skill/cozeSkill/getWxCozeSkillDataCover"
    params = {
        "keyword": keyword,
        "source": "公众号爆款封面生成-ClawHub",
    }

    # 添加开始日期参数
    if start_date:
        params["startDate"] = start_date

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Accept": "application/json, text/plain, */*",
        "Accept-Language": "zh-CN,zh;q=0.9",
        "Accept-Encoding": "gzip, deflate, br",
        "Connection": "close",
    }

    last_error = None
    for attempt in range(max_retries):
        try:
            if debug:
                print(f"\n=== DEBUG: 第 {attempt + 1} 次尝试 ===", file=sys.stderr)

            status_code, body = fetch_via_no_sni(base_url, params, headers)

            if debug:
                print(f"状态码: {status_code}", file=sys.stderr)
                print(f"响应长度: {len(body)} 字节", file=sys.stderr)

            if status_code >= 400:
                raise Exception(f"HTTP请求失败: 状态码 {status_code}")

            data = json.loads(body)

            if "data" not in data:
                error_msg = data.get("msg", "未知错误")
                raise Exception(f"API 错误: {error_msg}")

            result_data = data.get("data", {})

            if debug:
                print("=== DEBUG: API 返回的 data 字段键 ===", file=sys.stderr)
                print(json.dumps(list(result_data.keys()), ensure_ascii=False, indent=2), file=sys.stderr)

            return {
                "keyword": keyword,
                "low_fan_explosive": result_data.get("lowPowderExplosiveArticle", []),
                "ten_w_reading": result_data.get("tenWReadingRank", []),
                "original_rank": result_data.get("originalRank", [])
            }

        except Exception as e:
            last_error = str(e)
            if debug:
                print(f"  错误: {type(e).__name__}: {str(e)[:100]}", file=sys.stderr)
            import time
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)
            continue

    raise Exception(f"{last_error}（已尝试 {max_retries} 次）")


def get_cover_urls(data, max_per_category=5):
    """提取所有封面图URL"""
    urls = []
    categories = [
        ('low_fan_explosive', '低粉爆文榜'),
        ('ten_w_reading', '10w+阅读榜'),
        ('original_rank', '原创榜')
    ]

    for key, name in categories:
        items = data.get(key, [])[:max_per_category]
        for item in items:
            cover_url = item.get('coverUrl', '')
            photo_id = item.get('photoId', '')
            title = item.get('title', '')[:20]
            if cover_url and photo_id:
                urls.append({
                    'category': name,
                    'title': title,
                    'photo_id': photo_id,
                    'cover_url': cover_url,
                    'link': f"https://mp.weixin.qq.com/s/{photo_id}"
                })
    return urls


def format_output(data: dict, max_items: int = None, start_date: str = None):
    """
    格式化输出爆款数据（表格形式）

    Args:
        data: 原始数据
        max_items: 每类爆款数据最多展示数量，None 表示展示所有数据
        start_date: 开始日期，格式 yyyy-MM-dd，用于计算统计时间范围
    """
    from datetime import datetime, timedelta

    # 计算统计时间范围
    def get_time_range(start_date):
        if start_date:
            try:
                start = datetime.strptime(start_date, '%Y-%m-%d')
                end = datetime.now()
                days = (end - start).days
                if days <= 1:
                    return "近1天"
                elif days <= 7:
                    return f"近{days}天"
                else:
                    return f"近{days}天"
            except:
                return "近30天"
        return "近30天"

    time_range = get_time_range(start_date)

    def process_title(item):
        """处理标题：转义特殊字符，空标题使用summary替代"""
        title = item.get('title', '')
        # 如果标题为空，尝试使用 summary 字段
        if not title or title.strip() == '':
            summary = item.get('summary', '')
            if summary:
                # 移除 summary 中的换行符并截取前30个字符
                title = summary.replace('\n', ' ').replace('\r', ' ').strip()[:30]
                if len(summary) > 30:
                    title = title + '...'

        if not title or title.strip() == '':
            title = '无标题'

        # 转义 Markdown 表格特殊字符（|）
        title = title.replace('|', '\\|')
        # 移除换行符
        title = title.replace('\n', ' ').replace('\r', ' ')
        # 移除多余空格
        title = ' '.join(title.split())

        # 截断过长标题
        if len(title) > 30:
            title = title[:30] + "..."

        return title

    def format_time(item):
        """格式化发布时间为 X月X日"""
        pub_time = item.get('publicTime', '')
        if pub_time:
            # publicTime 格式: "2026-03-06 13:03:56"
            try:
                month = int(pub_time[5:7])
                day = int(pub_time[8:10])
                return f"{month}月{day}日"
            except:
                pass
        return '--'

    def get_latest_date(data):
        """获取数据中最新的发布日期"""
        all_items = []
        for key in ['low_fan_explosive', 'ten_w_reading', 'original_rank']:
            all_items.extend(data.get(key, []))

        latest_date = None
        for item in all_items:
            pub_time = item.get('publicTime', '')
            if pub_time:
                try:
                    date_str = pub_time[:10]  # 取 "YYYY-MM-DD" 部分
                    if latest_date is None or date_str > latest_date:
                        latest_date = date_str
                except:
                    pass
        return latest_date

    output = []

    # 检查数据日期
    latest_date = get_latest_date(data)

    # 按 photoId 去重（API 返回数据可能有重复）
    def dedup_items(items):
        seen = set()
        result = []
        for item in items:
            photo_id = item.get('photoId', '')
            if photo_id and photo_id not in seen:
                seen.add(photo_id)
                result.append(item)
        return result

    # 检查是否有任何数据
    low_fan_items = dedup_items(data.get("low_fan_explosive", []))
    ten_w_items = dedup_items(data.get("ten_w_reading", []))
    original_items = dedup_items(data.get("original_rank", []))

    total_count = len(low_fan_items) + len(ten_w_items) + len(original_items)

    # 如果所有类型都没有数据，输出友好提示
    if total_count == 0:
        keyword = data.get("keyword", "")
        output.append(f"# 公众号爆款数据分析报告\n\n**关键词**：{keyword}\n\n")
        output.append("---\n\n")
        output.append("## 暂无相关爆款数据\n\n")
        output.append(f"很抱歉，当前关键词 **「{keyword}」** 尚未有足够的爆款文章数据。\n\n")
        output.append("### 可能原因\n\n")
        output.append("- 该关键词相对小众或新兴，爆款内容积累较少\n")
        output.append("- 近期该赛道热度较低，暂无突出爆款文章\n")
        output.append("- 关键词表述方式可以更加具体或热门\n\n")
        output.append("### 建议操作\n\n")
        output.append("- 更换为更热门的关键词，如：**\"职场成长\"**、**\"美食\"**、**\"情感故事\"** 等\n")
        output.append("- 尝试更细分的长尾关键词\n")
        output.append("- 输入其他感兴趣的领域或赛道进行追踪\n\n")
        output.append("---\n\n")
        output.append("*数据来源：公众号爆款雷达，每日更新最新热门内容*\n")
        return "\n".join(output)

    # 1. 低粉爆文榜
    items = low_fan_items
    if max_items is not None:
        items = items[:max_items]

    output.append(f"\n### - **低粉爆文榜**（粉丝量较少的账号中爆款文章）")
    output.append("\n")

    if not items:
        output.append("(无数据)\n")
    else:
        output.append("| 封面 | 序号 | 发布时间 | 标题 | 作者 | 阅读数 | 点赞数 | 在看数 |")
        output.append("|------|------|----------|------|------|--------|--------|--------|")

        for idx, item in enumerate(items, 1):
            user_name = item.get('userName', '未知')
            fans = item.get('fans', '未知')

            # 封面缩略图
            cover_url = item.get('coverUrl', '')
            if cover_url:
                cover_str = f"![]({cover_url})"
            else:
                cover_str = "--"

            # 作者信息
            author_str = f"{user_name}（粉丝：{fans}）"

            # 标题添加链接
            title = process_title(item)
            ori_url = item.get('oriUrl', '')
            if ori_url:
                title_with_link = f"[{title}]({ori_url})"
            else:
                title_with_link = title

            pub_time = format_time(item)

            # 获取互动数据（字符串类型）
            clicks = item.get('clicksCount', '--')
            likes = item.get('likeCount', '--')
            watches = item.get('watchCount', '--')

            output.append(f"| {cover_str} | {idx} | {pub_time} | {title_with_link} | {author_str} | {clicks} | {likes} | {watches} |")

    # 2. 10w+阅读榜
    items = ten_w_items
    if max_items is not None:
        items = items[:max_items]

    output.append(f"\n### - **10w+阅读榜**（阅读量超过10万的爆款文章）")
    output.append("\n")

    if not items:
        output.append("(无数据)\n")
    else:
        output.append("| 封面 | 序号 | 发布时间 | 标题 | 作者 | 阅读数 | 点赞数 | 在看数 |")
        output.append("|------|------|----------|------|------|--------|--------|--------|")

        for idx, item in enumerate(items, 1):
            user_name = item.get('userName', '未知')
            fans = item.get('fans', '未知')

            # 封面缩略图
            cover_url = item.get('coverUrl', '')
            if cover_url:
                cover_str = f"![]({cover_url})"
            else:
                cover_str = "--"

            # 作者信息
            author_str = f"{user_name}（粉丝：{fans}）"

            # 标题添加链接
            title = process_title(item)
            ori_url = item.get('oriUrl', '')
            if ori_url:
                title_with_link = f"[{title}]({ori_url})"
            else:
                title_with_link = title

            pub_time = format_time(item)

            # 获取互动数据（字符串类型）
            clicks = item.get('clicksCount', '--')
            likes = item.get('likeCount', '--')
            watches = item.get('watchCount', '--')

            output.append(f"| {cover_str} | {idx} | {pub_time} | {title_with_link} | {author_str} | {clicks} | {likes} | {watches} |")

    # 3. 原创榜
    items = original_items
    if max_items is not None:
        items = items[:max_items]

    output.append(f"\n### - **原创榜**（优质原创爆款文章）")
    output.append("\n")

    if not items:
        output.append("(无数据)\n")
    else:
        output.append("| 封面 | 序号 | 发布时间 | 标题 | 作者 | 阅读数 | 点赞数 | 在看数 |")
        output.append("|------|------|----------|------|------|--------|--------|--------|")

        for idx, item in enumerate(items, 1):
            user_name = item.get('userName', '未知')
            fans = item.get('fans', '未知')

            # 封面缩略图
            cover_url = item.get('coverUrl', '')
            if cover_url:
                cover_str = f"![]({cover_url})"
            else:
                cover_str = "--"

            # 作者信息
            author_str = f"{user_name}（粉丝：{fans}）"

            # 标题添加链接
            title = process_title(item)
            ori_url = item.get('oriUrl', '')
            if ori_url:
                title_with_link = f"[{title}]({ori_url})"
            else:
                title_with_link = title

            pub_time = format_time(item)

            # 获取互动数据（字符串类型）
            clicks = item.get('clicksCount', '--')
            likes = item.get('likeCount', '--')
            watches = item.get('watchCount', '--')

            output.append(f"| {cover_str} | {idx} | {pub_time} | {title_with_link} | {author_str} | {clicks} | {likes} | {watches} |")

    return "\n".join(output)


def main():
    """主函数"""
    parser = argparse.ArgumentParser(description='公众号爆款封面数据查询工具')
    parser.add_argument('--keyword', required=True, help='搜索关键词')
    parser.add_argument('--max-items', type=int, default=10,
                       help='每类爆款内容最多展示数量（默认10条）')
    parser.add_argument('--output-format', choices=['text', 'json', 'markdown'],
                       default='json', help='输出格式：text（文本表格）、json（JSON格式，默认）或 markdown（Markdown格式）')
    parser.add_argument('--output-file', type=str, default=None,
                       help='输出文件路径')
    parser.add_argument('--start-date', type=str, default=None,
                       help='开始日期，格式 yyyy-MM-dd（默认最近30天）')
    parser.add_argument('--debug', action='store_true', help='启用调试模式')
    parser.add_argument('--max-retries', type=int, default=3,
                       help='最大重试次数（默认3次）')

    args = parser.parse_args()

    try:
        data = fetch_wx_covers(args.keyword, debug=args.debug, max_retries=args.max_retries, start_date=args.start_date)

        # 生成输出内容
        if args.output_format == 'json':
            output_content = json.dumps(data, ensure_ascii=False, indent=2)
        elif args.output_format == 'markdown':
            # Markdown 格式添加标题
            markdown_header = f"# 公众号爆款数据分析报告\n\n**关键词**：{args.keyword}\n\n"
            output_content = markdown_header + format_output(data, max_items=args.max_items, start_date=args.start_date)
        else:
            output_content = format_output(data, max_items=args.max_items, start_date=args.start_date)

        # 确定输出文件路径（默认不输出文件，只输出到控制台）
        output_file = args.output_file

        # 输出到文件或控制台
        if output_file:
            with open(output_file, 'w', encoding='utf-8') as f:
                f.write(output_content)
            print(f"✓ 结果已保存到: {output_file}", file=sys.stderr)
            print(f"✓ 关键词: {args.keyword}", file=sys.stderr)
            # 统计数据
            total_items = (
                len(data.get('low_fan_explosive', [])) +
                len(data.get('ten_w_reading', [])) +
                len(data.get('original_rank', []))
            )
            print(f"✓ 总计: {total_items} 条数据", file=sys.stderr)
            # 显示每类数据量
            print(f"  - 低粉爆文榜: {len(data.get('low_fan_explosive', []))} 条", file=sys.stderr)
            print(f"  - 10w+阅读榜: {len(data.get('ten_w_reading', []))} 条", file=sys.stderr)
            print(f"  - 原创榜: {len(data.get('original_rank', []))} 条", file=sys.stderr)
            # 输出封面图URL供后续分析
            cover_urls = get_cover_urls(data, max_per_category=3)
            if cover_urls:
                print(f"\n=== 封面图URL（用于风格分析）===", file=sys.stderr)
                for i, item in enumerate(cover_urls, 1):
                    print(f"{i}. [{item['category']}] {item['title']}: {item['cover_url']}", file=sys.stderr)
        else:
            print(output_content)
            # 统计数据输出到 stderr
            print(f"\n✓ 关键词: {args.keyword}", file=sys.stderr)

    except Exception as e:
        print(f"❌ 错误: {str(e)}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()


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