Claude Skill

playlet-xhs-feed

短剧-小红书信息源 — 每日扫描小红书短剧爆款内容,按互动量筛选热门笔记,智能聚类题材方向后生成包含封面、互动数据与创作洞察的HTML日报。支持按题材(穿越/霸总/重生等)、达人、时间范围定向查询。⚠️数据每日15:00更新前一天数据,目标日期无数据时必须先告知用户并等待确认后才能调用接口,禁止自动获取。⚠️查询前先校验分类/关键词是否符合短剧题材词库,不满足时不请求接口直接提示并推荐相关词。当用户需要短剧小红书日报、小红书短剧爆款、短剧热点、短剧创作趋势或自定义题材查询时使用。

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download redfox-data-redfox-community-skills_playlet-xiaohongshu-feed-5e7b435.zip · 48 KB
Part of redfox-data/redfox-community — 66 skills

Install

skills CLI npx skills add https://github.com/redfox-data/redfox-community/tree/main/skills/playlet-xiaohongshu-feed
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install redfox-data-redfox-community@llmmart
Git git clone https://github.com/redfox-data/redfox-community.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole redfox-data/redfox-community collection as a plugin from our marketplace. Git is the plain clone.

README

短剧-小红书信息源 / playlet-xhs-feed


简介

每日自动扫描小红书短剧爆款内容,按互动量筛选热门笔记,智能聚类题材方向后生成可视化日报,同步提供创作趋势分析,帮助短剧创作者精准把握流量风口。

核心价值

  • 📊 每日自动扫描小红书短剧内容,按互动量筛选爆款作品
  • 🏷️ 智能聚类题材方向(穿越/霸总/重生/悬疑等),每天分类由内容动态决定
  • 📈 生成包含封面图、互动数据与创作洞察的可视化日报
  • 🔍 支持按题材、达人、时间范围定向查询
  • 🔔 一键订阅,日报自动保存到本地文件夹

适用对象

  • 🎬 短剧编剧/制作人 — 精准把握流量风口,提升选题命中率
  • 📊 短剧运营/MCN — 追踪竞品动态,优化自身运营策略
  • 🔍 内容策划/数据分析师 — 产出结构化趋势报告,支撑内容战略决策

功能特性

核心功能

  • 📊 爆款发现 — 从小红书短剧中按互动量筛选热门内容,精准定位高热度短剧笔记
  • 🏷️ 题材聚类 — 自动识别题材方向(穿越/霸总/重生/悬疑等),每天分类由内容动态决定
  • 🔍 智能查询 — 默认查询全部短剧,数据不足时自动扩展题材批量查询,节省接口额度
  • 🎯 自定义查询 — 可指定任意题材、达人、关键词定向查询,灵活覆盖短剧细分方向
  • 📈 创作洞察 — 分析爆款标题特征、题材趋势、达人表现,深度挖掘创作规律
  • 🎨 可视化日报 — 深色主题页面,封面图 + 互动数据 + 笔记直链,直观展示每日短剧热点
  • 🔔 一键订阅 — 开启每日自动产出,日报自动保存至本地文件夹

特色亮点

  • 🧭 前置输入校验 — 查询前先校验分类/关键词是否符合短剧题材词库、日期是否在有效查询范围;全部不满足时不请求接口,直接提示并推荐相关词
  • ⚡ 智能日期判断 — 内置每日 15:00 更新规则,自动检测目标日期数据可用性,无需手动推算
  • 📱 小红书原生适配 — 作品链接自动拼接 xiaohongshu.com/explore/,互动数据为零的字段自动隐藏
  • 🔒 安全可靠 — API Key 仅通过环境变量获取,禁止硬编码,保障数据安全

密钥获取与安全说明

  • 本技能需要使用环境变量:REDFOX_API_KEY。
  • REDFOX_API_KEY 由 红狐 hub (https://redfox.hk)提供。
  • 请前往 红狐 hub 注册账号,获取 REDFOX_API_KEY。
  • 配置设备环境变量 REDFOX_API_KEY 后使用本技能。
  • 在提供密钥前,请先确认密钥来源、可用范围、有效期及是否支持重置/撤销。
  • 禁止在代码、提示词、日志或输出文件中硬编码/明文暴露密钥。

使用指南

直接用自然语言描述需求,无需记忆命令。

常用说法速查

意图 示例话术 效果
获取最新日报 "帮我生成最新的短剧小红书日报" / "查询短剧热点" 自动获取最新可用日期的爆款日报,含题材聚类与趋势分析
查询历史日期 "查询 6 月 10 日的短剧小红书日报" 生成指定日期的爆款内容日报
按题材定向查询 "看看穿越题材的短剧趋势" / "查询霸总和甜宠题材" 定向查询指定题材,结果自动去重聚类
创作趋势分析 "分析近期短剧创作趋势" / "短剧爆款有哪些" 输出题材趋势、爆款标题特征、达人排行与跨题材建议
开启每日订阅 "帮我开启每日短剧日报订阅" 日报每日自动生成并保存到本地文件夹

输出示例

日报生成后,你将获得:

  • 📊 题材概览表 — 各题材数量、占比及头部作品亮点
  • 🔥 新兴起量信号 — 数量少但互动高的潜力题材
  • 📝 爆款标题特征 — 高频标题模式、出现次数与平均互动
  • 👤 核心达人榜 — 达人作品数、总互动与代表作
  • 📈 题材趋势报告 — Top 题材的详细分析与创作建议
  • 🌐 跨题材对比建议 — 题材融合趋势观察

同时生成一个可视化日报页面,包含封面图、互动数据和笔记直链,自动在浏览器中打开。


使用场景

场景 角色 示例问法 收益
短剧选题参考 短剧编剧/制作人 "帮我生成最新的短剧小红书日报" 了解热门题材和爆款趋势,指导选题决策
竞品动态监控 短剧运营/MCN "看看霸总题材最近表现怎么样" 追踪竞品账号表现,优化运营策略
内容趋势分析 内容策划/分析师 "分析近一周短剧创作趋势" 产出结构化趋势报告,支撑内容战略决策

重要数据说明

  • 数据更新时间:每日 15:00 更新前一天的数据
  • 15:00 前,最新可用日期为前天(T-2);15:00 后,最新可用日期为昨天(T-1)
  • 目标日期数据尚未更新时,会先提示用户确认,不会自动调用接口(保留确认逻辑,禁止自动回退)
  • 关键词/分类不符合短剧题材词库时,不请求任何接口,直接提示并推荐相关词
  • 数据来源:红狐 Hub API,基于小红书平台(platform=3)
  • 数据追踪:API 请求通过 source 字段标识为 短剧小红书信息源-GitHub,用于数据来源统计

Skill manifest

短剧-小红书信息源

简介

短剧-小红书信息源是一款专为短剧创作者设计的小红书爆款内容追踪工具,参考AI-B站信息源的功能和样式设计。

通过红狐Hub API,你可以:

  • 📊 每日自动扫描小红书短剧内容,按互动量筛选爆款作品
  • 🏷️ 智能聚类题材方向(穿越/霸总/重生/悬疑等)
  • 📈 生成包含封面图、互动数据与创作洞察的可视化HTML日报
  • 🔍 支持按题材、达人、时间范围定向查询

适用于短剧编剧、制作人、运营人员等需要把握小红书短剧流量风口的场景。

⚠️ 重要-确认逻辑:数据每日15:00更新前一天数据,目标日期无数据时禁止自动调用接口/禁止自动回退,必须先告知用户并等待确认后才能执行。

🧭 重要-前置校验:查询前先校验分类/关键词是否符合短剧题材词库(穿越/霸总/重生/甜宠/悬疑/逆袭等)、日期是否在有效查询范围;关键词/分类全部不满足时不请求任何接口,直接提示"关键词不满足查询条件"并推荐相关分类和关键词。

⛔ 输出规范:日报生成后,对话回复必须严格按照输出模板输出 md 内容,禁止任何自由发挥、省略或口语化文字。详见下方「📊 输出格式」。

功能特性

🎯 核心功能

功能模块 能力描述 核心价值
📊 爆款发现 从小红书短剧中按互动量筛选热门内容 精准定位高热度短剧笔记
🏷️ 题材聚类 自动识别题材方向(穿越/霸总/重生/悬疑等) 每天题材分类由内容动态决定
🔍 智能查询 默认查询全部短剧,数据不足时自动扩展题材批量查询 节省接口额度,高效获取数据
🎯 自定义查询 用户可指定任意题材/达人/关键词定向查询 灵活覆盖任意短剧细分方向
📈 创作洞察 分析爆款标题特征、题材趋势、达人表现 深度挖掘创作规律
🎨 可视化日报 深色主题HTML,封面图+互动数据+笔记直链 直观展示每日短剧热点
🔔 一键订阅 --subscribe 开启每日自动产出 日报自动攒在本地文件夹

✨ 特色亮点

  • 🧭 前置输入校验(v2.3):查询前先判断分类/关键词是否符合短剧题材词库、日期是否在有效查询范围;全部不满足时不请求任何接口,直接提醒"关键词不满足查询条件"并推荐相关分类和关键词;混合词保留有效关键词查询
  • ⚡ 智能日期判断:脚本内置 DATA_UPDATE_HOUR = 15 常量,调用接口前自动检测目标日期是否在无数据区间,无数据时提示用户等待确认
  • 🧠 结构化空因:空结果时明确输出原因分类(数据源无数据/关键词无匹配/接口异常)并给出下一步建议
  • 🔄 自动扩展题材:全量查询数据不足(<100条)时,自动追加穿越→霸总→重生→悬疑→甜宠→逆袭定向查询补充
  • 🔁 空结果重试:单题材查询为空/异常时,间隔4秒自动重试1次
  • 📱 小红书适配:作品链接自动拼接 xiaohongshu.com/explore/{photoId},互动数据为0的字段自动隐藏,HEIF封面自动转JPG,加载失败自动fallback默认封面
  • 🔒 安全可靠:API Key通过环境变量 REDFOX_API_KEY 获取,禁止硬编码

一键安装

前置条件

  • Python 3 运行环境
  • 已注册红狐Hub账号并获取 API Key

安装步骤

1. 获取 API Key

前往 红狐Hub 官网 注册,登录后在个人中心获取,格式为 ak_xxxxxxxx。新注册用户获赠免费积分。

2. 配置环境变量

macOS/Linux:

echo 'export REDFOX_API_KEY=ak_xxxxxxxx' >> ~/.zshrc
source ~/.zshrc

Windows(PowerShell):

[Environment]::SetEnvironmentVariable("REDFOX_API_KEY", "ak_xxxxxxxx", "User")

配置后需重启终端使环境变量生效。

3. 验证配置

# macOS/Linux
echo $REDFOX_API_KEY

# Windows
echo %REDFOX_API_KEY%

环境变量配置

变量名 必填 说明
REDFOX_API_KEY 是 红狐Hub API访问密钥,通过 X-API-KEY 请求头鉴权

工作流程

第零步:日期有效性预检(必须执行,先于任何接口调用)

⛔ 核心规则:未经用户确认,禁止调用任何数据接口,禁止自动执行 --latest

绝对不能在用户未确认的情况下自动执行脚本获取数据(与B站版本不同,B站为自动兜底,本技能保留确认逻辑)。

数据更新规则:每日15:00更新前一天的数据

  • 15:00前:最新可用日期 = T-2(前天)
  • 15:00后:最新可用日期 = T-1(昨天)

执行流程(每次查询前强制执行):

  1. 前置校验-日期(v2.3):检查日期格式(YYYY-MM-DD)且不晚于今天;超出有效查询范围时提醒并推荐最近可用日期
  2. 获取当前系统日期 T 和当前时间,按15:00规则计算最新可用日期
  3. 判断用户请求的目标日期是否在无数据区间(即 > 最新可用日期)
  4. 若目标日期有数据(≤ 最新可用日期):直接执行查询,无需额外确认
  5. 若目标日期无数据(> 最新可用日期),向用户输出以下提示,并等待用户明确确认后才能执行(带 --latest 参数);若用户拒绝,则不执行任何接口调用:
**⚠️{查询日期}数据尚未更新**
数据更新规则:每日15:00更新前一天的数据
当前可查询的最新日期:{最新可查询到数据的日期}

是否需要查询{最新可查询到数据的日期}的数据?

示例对话:

用户:查询今天的短剧小红书日报
Agent:⚠️2026-06-16数据尚未更新
      数据更新规则:每日15:00更新前一天的数据
      当前可查询的最新日期:2026-06-14

      是否需要查询2026-06-14的数据?
用户:好的
Agent:(执行 python3 daily_report.py --latest)

前置校验:分类/关键词(v2.3,先于日期处理)

查询前先判断用户输入的关键词/分类是否符合短剧题材词库(穿越/霸总/重生/甜宠/悬疑/逆袭/年代/战神/古装/都市/科幻等)。

  • 全部不符合:不请求任何接口(含日期探活),直接提示"关键词不满足短剧查询条件"并推荐相关分类和关键词后停止
  • 部分不符合(混合词):保留有效关键词继续查询,无效关键词自动忽略并提示
  • 全部符合:正常查询

推荐逻辑:优先从无效词中提取包含的题材词(如"穿越重生"→推荐穿越、重生),再补热门题材词。

第一步:生成爆款日报

# 生成最新一期日报(用户确认后执行)
python3 "$SKILL_PATH/assets/daily_report.py" --latest

# 生成指定日期日报(历史日期已有数据,无需确认)
python3 "$SKILL_PATH/assets/daily_report.py" --date 2026-06-10

# 自定义题材查询(用户指定方向,自动校验关键词)
python3 "$SKILL_PATH/assets/daily_report.py" --topics "穿越,霸总,重生,悬疑" --latest

# 订阅 / 取消订阅
python3 "$SKILL_PATH/assets/daily_report.py" --subscribe
python3 "$SKILL_PATH/assets/daily_report.py" --unsubscribe

查询策略:默认查询全部短剧内容(pageSize=200),数据不足(<100条)时自动追加热门题材(穿越→霸总→重生→悬疑→甜宠→逆袭),所有题材通过批量接口一次性查询。用户自定义题材时仅使用用户提供的有效题材词,同样批量查询。

日期智能判断:脚本内置 DATA_UPDATE_HOUR = 15 常量(每日15:00更新前一天数据),调用接口前自动检测目标日期是否在无数据区间。作为双保险,Agent 在第零步已提前拦截,避免脚本层的交互提示被忽略。

第二步:执行创作趋势分析

日报生成后,必须基于聚类结果自动执行创作趋势分析:

  1. 读取题材聚类结果,选取TOP 5热门题材
  2. 分析每个题材的爆款数量、平均互动数据、头部作品特征
  3. 识别新兴起量题材(数量少但互动高)
  4. 输出结构化创作趋势报告

生成的HTML日报保存在 ~/Downloads/QoderReports/,自动浏览器打开。终端同步输出题材分类表格 + 创作趋势分析报告。

📊 输出格式(强制执行)

⛔ 严格执行规则:

  • 以下模板是唯一合法输出格式,禁止任何自由发挥、省略、简化或重新组织
  • 禁止输出模板中未定义的额外内容(如"我来帮你…""以下是…"等口语化文字)
  • 禁止合并、跳过任何板块,即使某板块数据为"暂无"也必须保留该板块标题
  • 每次获取日报后,对话回复必须严格按此模板输出 md 内容,不得包含模板以外的任何文字

每次运行日报后,对话输出必须严格按以下模板原样输出(仅替换 {...} 占位符):

## 短剧-小红书信息源 · {日期} 日报

**扫描 {N} 部热门短剧,聚类 {M} 个题材方向**

---

### 题材概览

| 题材 | 数量 | 占比 | 爆款亮点 |
|------|------|------|---------|
| #{题材名} | {N}部 | {X}% | 头部作品亮点描述 |
| ... | ... | ... | ... |

---

### 创作趋势分析

**一、新兴起量信号**

- 🔥 **#{题材}** — 仅{N}部但均互动{X}+,描述
(若无新兴题材,输出:暂无新兴起量信号)

**二、爆款标题特征**

| 特征模式 | 出现次数 | 典型案例 | 平均互动 |
|---------|---------|---------|---------|
| {特征1} | {N}次 | 《{标题}》 | {X}w |
| ... | ... | ... | ... |
(若无标题数据,输出:暂无爆款标题数据)

**三、核心达人榜**

| 达人 | 作品数 | 总互动 | 代表作 |
|------|--------|--------|--------|
| @{达人} | {N}部 | {X}w | 《{作品}》 |
| ... | ... | ... | ... |
(若无达人数据,输出:暂无核心达人数据)

**四、题材趋势报告**

**题材**:#{题材1}
**作品数**:{N}部
**平均互动**:{X}w
**头部作品**:《{标题}》-{互动}w

**题材特征**:{描述该题材的共性特征}
**创作建议**:{针对该题材的创作建议}

**五、#{题材2}**

(同上格式)

**六、#{题材3}**

(同上格式)

**七、跨题材对比建议**

- **{题材}** — 建议同步关注{相关题材}的联动创作,观察题材融合趋势
(若无建议,输出:暂无跨题材对比建议)

---

**日报地址**:{HTML文件绝对路径}

> 数据说明:每日15:00更新昨天的数据

以上格式为强制规范,所有字段不可省略,板块标题(一、二、三、四、五、六、七)必须保留。若某模块无数据则在该板块内标注"暂无",不得删除板块本身。

参数说明

参数 说明 默认值
--topics "关键词,..." 自定义题材查询,逗号分隔;全部不符合短剧题材词库时不请求接口直接提示+推荐;混合词保留有效词 全量查询
--count N 扫描作品数量,满足即停 200
--date YYYY-MM-DD 指定日期(超出有效范围/未更新时提示并询问,等待确认;历史日期已有数据无需确认) 今天
--start-time 自定义开始时间 YYYY-MM-DD HH:MM:SS(覆盖 --date 推算) —
--end-time 自定义结束时间 YYYY-MM-DD HH:MM:SS(覆盖 --date 推算) —
--latest 自动使用最新有数据的日期(用户确认后执行,不扣积分) —
--output-dir 输出目录 ~/Downloads/QoderReports
--api-key 指定 API Key —
--subscribe 开启每日订阅 —
--unsubscribe 关闭每日订阅 —
--from-cache 使用缓存数据,不扣积分 —

💬 自定义题材查询场景

除默认短剧日报外,用户可指定任意题材组合进行定向查询与分析:

# 查询穿越题材热门短剧
python3 "$SKILL_PATH/assets/daily_report.py" --topics "穿越,时空,重生"

# 查询霸总/甜宠题材
python3 "$SKILL_PATH/assets/daily_report.py" --topics "霸总,甜宠,总裁,虐恋"

# 查询悬疑/反转题材
python3 "$SKILL_PATH/assets/daily_report.py" --topics "悬疑,推理,反转,惊悚"

自定义查询逻辑:

  • 查询前先校验关键词/分类是否符合短剧题材词库(穿越/霸总/重生/甜宠/悬疑/逆袭/年代/战神/古装等)
  • 全部不满足时不请求任何接口,提醒"关键词不满足短剧查询条件"并推荐相关分类和关键词,停止
  • 混合词场景保留有效关键词继续查询,无效关键词自动忽略并提示
  • 查询结果自动去重,题材聚类、趋势分析均基于查询结果生成,与用户关注方向强关联

使用场景

场景一:短剧创作者选题参考

角色:短剧编剧/制作人

需求:了解小红书短剧市场的热门题材和爆款趋势,指导选题决策

使用方式:

  1. 每日确认后执行 --latest 获取最新日报
  2. 分析题材聚类结果,关注新兴起量信号
  3. 参考爆款标题特征和题材趋势报告

预期收益:精准把握流量风口,提升选题命中率


场景二:运营团队竞品监控

角色:短剧运营/MCN机构

需求:追踪竞品账号在小红书的表现,分析爆款内容特征

使用方式:

  1. 使用 --topics 定向查询竞品所在题材
  2. 关注核心达人榜,识别头部竞品账号
  3. 开启 --subscribe 每日自动攒日报

预期收益:及时掌握竞品动态,优化自身运营策略


场景三:内容趋势分析

角色:内容策划/数据分析师

需求:分析小红书短剧的内容趋势,为内容规划提供数据支撑

使用方式:

  1. 结合 --start-time 和 --end-time 批量分析时间范围数据
  2. 利用创作趋势分析的5个维度深度挖掘规律
  3. 关注跨题材对比建议,发现题材融合机会

预期收益:产出结构化趋势报告,支撑内容战略决策

项目架构

目录结构

短剧-小红书信息源/
├── SKILL.md                      # Skill主文档(本文件)
├── README.md                     # 项目说明文档
├── scripts/                      # 脚本源码
│   └── playlet_xhs_daily.py      # 日报生成脚本(开发版,与assets同步)
├── assets/                       # Skill运行时资源
│   ├── daily_report.py           # 日报生成脚本(运行时使用,v2.3增强版)
│   └── default_cover.png         # 默认封面图(加载失败时的fallback)
└── references/                   # 参考文档
    ├── core_workflow.md          # 核心执行流程、格式模板、日期判断逻辑
    └── examples.md               # 使用示例与常见用法组合

技术栈

组件 技术 说明
运行环境 Python 3 脚本语言
数据源 红狐Hub API X-API-KEY 请求头鉴权,source 字段追踪来源
API端点 https://redfox.hk/story/api/parseWork/queryPlayletMsgs POST请求
平台标识 platform=3 小红书(1=抖音,2=视频号,6=B站)
来源标识 source "短剧小红书信息源-GitHub"
数据存储 JSON缓存 ~/.workbuddy/cache/playlet_xhs_data.json
输出格式 HTML + 终端Markdown 深色主题,小红书红(#FF2442)
输出目录 ~/Downloads/QoderReports 可自定义

数据流转

用户请求 → 前置输入校验(分类/关键词/日期) → 日期预检(15:00规则)
  ↓(关键词/分类全部不满足)        ↓(日期未更新/超范围)
不请求接口,直接提示+推荐停止   提示用户并等待确认(禁止自动回退)
  ↓(用户确认后)
API调用(批量查询+重试+去重+排序,数据不足自动扩展题材)
  ↓
题材聚类(关键词匹配)
  ↓
HTML日报生成 + 终端摘要输出
  ↓
创作趋势分析(5个维度)

常见问答

安装相关问题

Q1: 安装时提示 "未找到 REDFOX_API_KEY 环境变量" 怎么办?

A: 请按以下步骤检查:

  1. 确认 API Key 已正确配置(Windows: [Environment]::SetEnvironmentVariable("REDFOX_API_KEY", "ak_xxx", "User"))
  2. 配置后需重启终端使环境变量生效
  3. 验证: echo %REDFOX_API_KEY% 应输出你的Key值

Q2: API Key 如何获取?

A: 前往 红狐Hub 官网 注册账号,登录后在个人中心获取,格式为 ak_xxxxxxxx。新注册用户获赠免费积分。


使用相关问题

Q3: 数据多久更新一次?

A: 每日15:00更新前一天的数据。

  • 15:00前:最新可用日期 = 前天(T-2)
  • 15:00后:最新可用日期 = 昨天(T-1)

Q4: 查询指定日期提示"数据尚未更新"怎么办?

A: 目标日期尚未更新时,脚本不会自动回退,而是提示当前可查询的最新日期并询问"是否需要查询最新日期的数据?",等待用户确认后才执行。这是本技能保留的确认逻辑(与B站版本的自动兜底不同)。

Q5: 关键词/分类不满足短剧题材词库时怎么办?

A: 查询前脚本会先校验关键词/分类:全部不符合短剧题材词库(如"智能辅助驾驶""火星人"等非题材词)时,不请求任何接口,直接提示"关键词不满足短剧查询条件"并推荐相关分类和关键词(穿越/霸总/重生/甜宠/悬疑/逆袭等);混合词场景保留有效关键词继续查询。

Q6: 查询不到数据怎么办?

A: 可能原因:

  1. 目标日期尚无数据(未到15:00更新时间)
  2. 关键词在该日期无匹配作品(建议改用题材词或去掉 --topics 查询全部,需确认后执行)
  3. API Key权限不足或积分耗尽
  4. 网络连接问题

建议先使用 --from-cache 检查缓存,或换一个日期尝试。


故障排除

Q7: Windows PowerShell 执行报 UnicodeEncodeError?

A: 脚本输出包含emoji,需设置UTF-8编码:

$env:PYTHONIOENCODING='utf-8'
python assets/daily_report.py --latest

Q8: HTML日报中图片加载失败?

A: 脚本已内置fallback机制,加载失败时自动显示默认封面图(assets/default_cover.png)。部分封面图URL使用HEIF格式,脚本会自动转换为JPG格式提高兼容性。


安全与许可

Q9: 数据安全如何保障?

A:

  • API Key仅通过环境变量获取,禁止硬编码
  • 数据来源唯一:仅使用红狐Hub API,禁止自主采集
  • 缓存文件存储在本地 ~/.workbuddy/cache/

📚 参考文档

Files (redfox-community)
  • assets
    • daily_report.py 28.4 KB
      #!/usr/bin/env python3
      # -*- coding: utf-8 -*-
      """
      短剧-小红书信息源日报生成脚本 (增强版 v2.3)
      ===========================================
      每日扫描小红书短剧爆款内容,智能聚类题材后生成HTML日报
      
      v2.3 同步说明(从 B站信息源同步增强,保留"需用户确认"逻辑):
      - 【前置校验】查询前先判断用户的分类/关键词是否符合短剧题材词库、日期是否在有效
        查询范围;关键词/分类全部不满足时【不请求任何接口】,直接提示"关键词不满足短剧
        查询条件"并推荐相关分类和关键词后停止;混合词保留有效关键词查询。
      - 【防御式解析】兼容 {"code":2000,"data":{"list":[...]}} 与直出 list 两种响应格式,
        防止服务端调整响应结构时脚本静默失效。
      - 【自动扩展题材】全量查询数据不足(小于 AUTO_EXPAND_THRESHOLD)时,自动追加
        穿越→霸总→重生→悬疑→甜宠→逆袭 定向查询补充数据。
      - 【空结果重试】单题材查询为空/异常时,间隔 RETRY_INTERVAL 秒重试 1 次。
      - 【结构化空因】空结果时明确输出原因分类:无数据 / 关键词无匹配 / API异常。
      - ⛔【保留确认逻辑】目标日期无数据时,禁止自动回退/自动降级查询,必须先提示用户
        并等待确认后才能执行(与 B站版本不同,B站 v2.1 起为自动兜底直接出日报)。
      
      用法(与原版完全兼容):
          python3 daily_report.py --latest          # 用户确认后执行
          python3 daily_report.py --date 2026-06-10
          python3 daily_report.py --topics "穿越,霸总" --latest
      """
      
      import argparse
      import json
      import os
      import sys
      import time
      import webbrowser
      from datetime import datetime, timedelta
      from urllib import request, error
      
      
      # ============ 配置 ============
      API_BASE_URL = "https://redfox.hk/story/api/parseWork/queryPlayletMsgs"
      CACHE_DIR = os.path.expanduser("~/.workbuddy/cache")
      CACHE_FILE = os.path.join(CACHE_DIR, "playlet_xhs_data.json")
      OUTPUT_DIR = os.path.expanduser("~/Downloads/QoderReports")
      DATA_UPDATE_HOUR = 15       # 数据源声称的更新时刻(15:00更新前一天数据)
      RETRY_TIMES = 1             # 空结果/异常重试次数
      RETRY_INTERVAL = 4          # 重试间隔(秒)
      REQUEST_TIMEOUT = 30        # 单次请求超时(秒)
      AUTO_EXPAND_THRESHOLD = 100 # 全量结果少于该值时自动扩展题材
      AUTO_EXPAND_TOPICS = ["穿越", "霸总", "重生", "悬疑", "甜宠", "逆袭"]  # 扩展顺序
      
      # 短剧题材词库:用于 --topics 输入校验提示与推荐
      TOPIC_THESAURUS = {
          "穿越", "霸总", "重生", "悬疑", "甜宠", "逆袭", "年代", "战神",
          "古装", "总裁", "豪门", "复仇", "惊悚", "推理", "反转", "爽文",
          "科幻", "玄幻", "修仙", "都市", "职场", "萌宝", "萌娃", "亲子",
          "离婚", "闪婚", "替身", "虐恋", "先婚后爱", "双重生",
      }
      
      
      # ============ 工具函数 ============
      def get_api_key():
          """从环境变量获取 API Key"""
          api_key = os.environ.get("REDFOX_API_KEY")
          if not api_key:
              print("❌ 错误:未找到 REDFOX_API_KEY 环境变量")
              print("请先配置:export REDFOX_API_KEY=<你的apikey>")
              sys.exit(1)
          return api_key
      
      
      def calculate_latest_date():
          """按15:00规则估算最新可用日期"""
          now = datetime.now()
          if now.hour < DATA_UPDATE_HOUR:
              # 15:00前,最新可用日期是前天
              return (now - timedelta(days=2)).strftime("%Y-%m-%d")
          else:
              # 15:00后,最新可用日期是昨天
              return (now - timedelta(days=1)).strftime("%Y-%m-%d")
      
      
      def validate_date(date_str):
          """基于本地15:00规则判断目标日期是否有数据(确认逻辑核心)"""
          latest_date = calculate_latest_date()
          target_date = datetime.strptime(date_str, "%Y-%m-%d")
          latest = datetime.strptime(latest_date, "%Y-%m-%d")
          return target_date <= latest, latest_date
      
      
      def check_topics(topics):
          """
          v2.3 前置校验:判断用户输入的分类/关键词是否符合短剧题材词库。
          返回 (有效词列表, 无效词列表, 推荐词列表)
          推荐逻辑:优先从无效词中提取包含的题材词,再补充热门题材词。
          """
          hot_topics = ["穿越", "霸总", "重生", "甜宠", "悬疑", "逆袭",
                        "年代", "战神", "古装", "都市", "科幻"]
          valid, invalid = [], []
          for t in topics:
              if t in TOPIC_THESAURUS or t == "短剧":
                  valid.append(t)
              else:
                  invalid.append(t)
          recommends = []
          for t in invalid:
              # 无效词若包含题材词(如"穿越重生"含"穿越""重生"),优先推荐
              contained = [w for w in TOPIC_THESAURUS if w in t or t in w]
              for c in contained:
                  if c not in recommends:
                      recommends.append(c)
          for h in hot_topics:
              if h not in recommends:
                  recommends.append(h)
          return valid, invalid, recommends
      
      
      def check_date(date_str):
          """
          v2.3 前置校验:判断日期是否在有效查询范围(格式正确、不晚于今天)。
          返回 (是否有效, 提示信息, 推荐日期或None)
          """
          try:
              d = datetime.strptime(date_str, "%Y-%m-%d")
          except ValueError:
              return False, f"日期格式无效:{date_str}(应为 YYYY-MM-DD)", None
          today = datetime.now().date()
          if d.date() > today:
              latest = calculate_latest_date()
              return False, f"日期 {date_str} 超出有效查询范围(晚于今天,数据每日15:00更新前一天)", latest
          return True, "", None
      
      
      def parse_response(result):
          """
          防御式响应解析(加固):兼容两种格式
          格式A(API实际完整响应): {"code":2000,"data":{"list":[...],"total":M},"msg":"..."}
          格式B(兜底/直出):      {"list":[...], "pageNum":1, "pages":N, "total":M}
          返回: (items列表, error_msg或None)
          """
          if not isinstance(result, dict):
              return [], "响应非JSON对象"
          # 格式A:code/data 包装(当前 API 实际格式)
          if result.get("code") == 2000:
              data = result.get("data") or {}
              return data.get("list") or [], None
          # 显式业务错误
          code = result.get("code")
          if code is not None:
              msg = result.get("msg")
              return [], f"API业务错误 code={code} msg={msg}"
          # 格式B:直出 list(兜底)
          if "list" in result:
              return result.get("list") or [], None
          return [], None
      
      
      def http_post(payload, api_key):
          """执行一次 POST 请求,返回原始响应 dict(网络/HTTP 层异常向上抛)"""
          data = json.dumps(payload, ensure_ascii=False).encode('utf-8')
          req = request.Request(
              API_BASE_URL,
              data=data,
              headers={
                  "Content-Type": "application/json",
                  "X-API-KEY": api_key
              },
              method="POST"
          )
          with request.urlopen(req, timeout=REQUEST_TIMEOUT) as response:
              return json.loads(response.read().decode('utf-8'))
      
      
      def build_payload(start_time, end_time, keyword=None, page_size=200):
          """构建请求体;keyword 为 None 时表示全量查询(不带 keyword 字段)"""
          payload = {
              "msgType": "短剧",
              "platform": 3,  # 3=小红书 (1=抖音,2=视频号,6=B站)
              "source": "短剧小红书信息源-GitHub",
              "pageNum": 1,
              "pageSize": page_size,
              "startTime": start_time,
              "endTime": end_time,
          }
          if keyword:
              payload["keyword"] = keyword
          return payload
      
      
      def _fetch_topic_once(api_key, payload, topic):
          """单题材单次查询(含重试),返回 (items, api_error: bool)"""
          for attempt in range(RETRY_TIMES + 1):
              try:
                  result = http_post(payload, api_key)
                  items, err = parse_response(result)
                  if err:
                      if attempt < RETRY_TIMES:
                          time.sleep(RETRY_INTERVAL)
                      continue
                  return items, False  # 解析成功(可能为空 list,但非异常)
              except Exception as e:
                  if attempt < RETRY_TIMES:
                      print(f"  ⚠️ 题材 {topic} 第{attempt+1}次请求失败({e}),{RETRY_INTERVAL}s后重试...")
                      time.sleep(RETRY_INTERVAL)
                  else:
                      print(f"❌ 查询题材 {topic} 失败:{str(e)}")
          return [], True
      
      
      # ============ 数据获取 ============
      def fetch_playlet_data(
          topics=None,
          start_time=None,
          end_time=None,
          count=200,
          use_cache=False,
      ):
          """
          调用 API 查询小红书短剧数据(增强版)
      
          Args:
              topics: 题材列表(逗号分隔),None/空 → 全量查询,数据不足时自动扩展题材
              start_time / end_time: 查询时间窗
              count: 扫描作品数量
              use_cache: 是否使用缓存
      
          Returns:
              (items, meta) 其中 meta 含 reason 字段用于结构化空因:
                  reason in {"ok", "no_data", "keyword_no_match", "api_error"}
          """
          if use_cache:
              cached_data = load_cache()
              if cached_data:
                  print("📦 使用缓存数据")
                  return cached_data, {"reason": "ok", "note": "cache"}
      
          if not start_time or not end_time:
              latest_date = calculate_latest_date()
              start_time = f"{latest_date} 00:00:00"
              end_time = f"{latest_date} 23:59:59"
      
          api_key = get_api_key()
          meta = {"reason": "ok"}
      
          # 确定查询题材序列
          # 用户指定题材 → 仅用用户列表(自定义时不用扩展列表)
          # 未指定 → 全量查询;数据不足时自动按 AUTO_EXPAND_TOPICS 扩展
          if topics:
              query_topics = list(topics)
              auto_expand = False
          else:
              query_topics = [None]
              auto_expand = True
      
          all_items = []
          api_errors = 0
          expanded = False
      
          for topic in query_topics:
              keyword = None if topic is None or topic == "短剧" else topic
              payload = build_payload(start_time, end_time, keyword=keyword,
                                      page_size=min(count, 200))
              items, had_error = _fetch_topic_once(api_key, payload, topic or "全量")
              if had_error:
                  api_errors += 1
              if items:
                  all_items.extend(items)
      
              # 自动扩展题材:去重数不足且未满 count 时继续
              if auto_expand:
                  unique_ids = {it.get("photoId") for it in all_items if it.get("photoId")}
                  if len(unique_ids) < min(count, AUTO_EXPAND_THRESHOLD) and not expanded:
                      expanded = True
                      print(f"  ➕ 全量数据不足({len(unique_ids)}条 < {AUTO_EXPAND_THRESHOLD}),"
                            f"自动扩展题材: {'→'.join(AUTO_EXPAND_TOPICS)}")
                      for extra in AUTO_EXPAND_TOPICS:
                          ep = build_payload(start_time, end_time, keyword=extra,
                                             page_size=min(count, 200))
                          e_items, e_err = _fetch_topic_once(api_key, ep, extra)
                          if e_err:
                              api_errors += 1
                          if e_items:
                              all_items.extend(e_items)
                          unique_ids = {it.get("photoId") for it in all_items if it.get("photoId")}
                          if len(unique_ids) >= count:
                              break
                      break  # 扩展后结束循环
      
          # 去重(基于photoId)
          seen = set()
          unique_items = []
          for item in all_items:
              item_id = item.get("photoId")
              if item_id and item_id not in seen:
                  seen.add(item_id)
                  unique_items.append(item)
      
          # 按互动量排序(使用likeCount)
          unique_items.sort(key=lambda x: x.get("likeCount", 0) or 0, reverse=True)
      
          if not unique_items:
              # 查询结果为空 → 区分关键词无匹配 / 数据源无数据
              if topics and topics != [None]:
                  meta["reason"] = "keyword_no_match"
              else:
                  meta["reason"] = "no_data"
              return [], meta
      
          # 保存缓存
          save_cache(unique_items)
          meta["reason"] = "ok"
          return unique_items[:count], meta
      
      
      # ============ 题材聚类 ============
      def cluster_by_topic(items):
          """按题材聚类作品(9大题材关键词匹配+自动归类)"""
          topic_keywords = {
              "穿越": ["穿越", "时空", "古代", "现代", "回到", "大宋", "北宋", "南宋", "唐朝", "明朝", "清朝"],
              "霸总": ["霸总", "总裁", "豪门", "冷酷", "宠妻", "娇妻", "替身"],
              "重生": ["重生", "逆袭", "回到", "翻盘", "重来", "再生"],
              "悬疑": ["悬疑", "推理", "反转", "惊悚", "谜案", "秘密", "真相"],
              "甜宠": ["甜宠", "恋爱", "撒糖", "甜蜜", "宠溺", "甜甜", "撒糖"],
              "逆袭": ["逆袭", "翻身", "打脸", "崛起", "反击", "报复"],
              "年代": ["年代", "八零", "九零", "七零", "六零"],
              "战神": ["战神", "龙王", "兵王", "高手"],
              "古装": ["古装", "宫廷", "皇后", "贵妃", "王爷", "世子"]
          }
          clusters = {}
          for item in items:
              title = item.get("title", "") or item.get("desc", "")
              matched_topics = []
              for topic, keywords in topic_keywords.items():
                  if any(kw in title for kw in keywords):
                      matched_topics.append(topic)
              matched_topic = matched_topics[0] if matched_topics else "其他"
              clusters.setdefault(matched_topic, []).append(item)
          return clusters
      
      
      # ============ HTML 日报 ============
      def format_number(num):
          """格式化数字(万→w),兼容字符串数值(如"12w+")"""
          if num is None:
              return "0"
          if isinstance(num, str):
              num = num.replace("+", "").replace("w", "0000").replace("亿", "00000000")
              try:
                  num = float(num)
              except Exception:
                  return "0"
          if num >= 10000:
              return f"{num/10000:.1f}w"
          return str(int(num))
      
      
      def generate_html_report(items, clusters, date_str):
          """生成HTML日报(小红书红 #FF2442,HEIF自动转JPG,封面fallback)"""
          os.makedirs(OUTPUT_DIR, exist_ok=True)
      
          html_file = os.path.join(OUTPUT_DIR, f"短剧小红书日报_{date_str}.html")
      
          try:
              dt_obj = datetime.strptime(date_str, "%Y-%m-%d")
              weekdays = ["一", "二", "三", "四", "五", "六", "日"]
              date_cn = f"{dt_obj.year}年{dt_obj.month}月{dt_obj.day}日 星期{weekdays[dt_obj.weekday()]}"
          except ValueError:
              date_cn = date_str
      
          total_count = len(items)
          topic_count = len(clusters)
          total_likes = sum(item.get("likeCount", 0) or 0 for item in items)
          avg_likes = total_likes / total_count if total_count > 0 else 0
      
          # 默认封面图路径(用于加载失败时的fallback)
          default_cover_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "assets", "default_cover.png"))
      
          category_cards = ""
          for i, (topic, topic_items) in enumerate(
                  sorted(clusters.items(), key=lambda x: len(x[1]), reverse=True), 1):
              articles_html = ""
              for item in topic_items[:5]:  # 每个题材展示前5个
                  title = item.get("title", "无标题")
                  author = item.get("userName", "")
                  cover = item.get("coverUrl") or ""
                  # 将HEIF格式转换为JPG格式,提高浏览器兼容性
                  if cover and "format/heif" in cover:
                      cover = cover.replace("format/heif", "format/jpg")
                  photo_id = item.get("photoId") or ""  # 作品ID
                  likes_raw = item.get("likeCount", 0) or 0
                  comments_raw = item.get("commentCount", 0) or 0
                  shares_raw = item.get("shareCount", 0) or 0
                  likes = format_number(likes_raw)
                  comments = format_number(comments_raw)
                  shares = format_number(shares_raw)
      
                  cover_html = ""
                  if cover:
                      cover_html = (f'<img class="article-cover" src="{cover}" alt="" loading="lazy" '
                                    f'referrerpolicy="no-referrer" '
                                    f'onerror="this.style.display=&apos;none&apos;;this.nextElementSibling.style.display=&apos;block&apos;">'
                                    f'<img class="article-cover" src="file://{default_cover_path}" alt="" loading="lazy" style="display:none">')
      
                  # 生成作品链接:使用photoId拼接小红书链接
                  if photo_id:
                      title_html = f'<a href="https://www.xiaohongshu.com/explore/{photo_id}" target="_blank" class="article-title">{title}</a>'
                  else:
                      title_html = f'<span class="article-title">{title}</span>'
      
                  # 动态构建指标项:数据为0时不展示该字段
                  metrics_parts = []
                  if shares_raw > 0:
                      metrics_parts.append(f'<span class="metric">🔗 {shares}</span>')
                  if likes_raw > 0:
                      metrics_parts.append(f'<span class="metric">👍 {likes}</span>')
                  if comments_raw > 0:
                      metrics_parts.append(f'<span class="metric">💬 {comments}</span>')
                  metrics_html = '\n                                '.join(metrics_parts)
      
                  articles_html += f'''
                      <div class="article-item">
                          {cover_html}
                          <div class="article-info">
                              {title_html}
                              <div class="article-meta">
                                  <span class="author">{author}</span>
                                  <span class="metrics">
                                      {metrics_html}
                                  </span>
                              </div>
                          </div>
                      </div>'''
      
              category_cards += f'''
              <div class="category-card reveal">
                  <div class="card-header">
                      <span class="card-number">{i:02d}</span>
                      <h3 class="card-category">#{topic}</h3>
                      <span class="card-count">{len(topic_items)} 部</span>
                  </div>
                  <div class="card-body">{articles_html}
                  </div>
              </div>'''
      
          timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
          html_content = f'''<!DOCTYPE html>
      <html lang="zh-CN">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>短剧-小红书信息源 - {date_str}</title>
      <style>
      * {{ margin: 0; padding: 0; box-sizing: border-box; }}
      body {{ font-family: -apple-system, sans-serif; background: #1a1a1a; color: #e8e4df; padding: 2rem; }}
      .header {{ text-align: center; padding: 2rem 0; }}
      .header h1 {{ font-size: 2rem; color: #FF2442; }}
      .header p {{ color: #9a9590; margin-top: 0.5rem; }}
      .stats {{ display: flex; justify-content: center; gap: 2rem; padding: 1rem; margin: 1rem 0; }}
      .stat-item {{ text-align: center; }}
      .stat-value {{ font-size: 1.5rem; font-weight: bold; color: #FF2442; }}
      .stat-label {{ font-size: 0.8rem; color: #9a9590; }}
      .cards {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 1.5rem; max-width: 1200px; margin: 2rem auto; }}
      .category-card {{ background: #2d2d2d; border-radius: 12px; padding: 1.5rem; }}
      .card-header {{ display: flex; align-items: center; gap: 0.8rem; margin-bottom: 1rem; padding-bottom: 0.8rem; border-bottom: 1px solid #3d3d3d; }}
      .card-number {{ font-size: 1.5rem; font-weight: bold; color: #FF2442; }}
      .card-category {{ flex: 1; font-size: 1.1rem; }}
      .card-count {{ color: #9a9590; font-size: 0.9rem; }}
      .article-item {{ padding: 0.6rem 0; border-bottom: 1px solid #3d3d3d; display: flex; gap: 0.8rem; }}
      .article-item:last-child {{ border-bottom: none; }}
      .article-cover {{ width: 60px; height: 60px; border-radius: 6px; object-fit: cover; flex-shrink: 0; }}
      .article-info {{ flex: 1; min-width: 0; }}
      .article-title {{ color: #e8e4df; font-size: 0.9rem; line-height: 1.4; display: block; }}
      .article-title:hover {{ color: #FF2442; text-decoration: underline; cursor: pointer; }}
      a.article-title {{ text-decoration: none; }}
      a.article-title:hover {{ color: #FF2442; text-decoration: underline; }}
      .article-meta {{ display: flex; justify-content: space-between; margin-top: 0.3rem; font-size: 0.75rem; color: #9a9590; }}
      .metrics {{ display: flex; gap: 0.8rem; }}
      .footer {{ text-align: center; padding: 2rem; color: #666; font-size: 0.8rem; }}
      .reveal {{ animation: fadeIn 0.5s ease-in; }}
      @keyframes fadeIn {{ from {{ opacity: 0; transform: translateY(20px); }} to {{ opacity: 1; transform: translateY(0); }} }}
      </style>
      </head>
      <body>
      <div class="header">
          <h1>📕 短剧-小红书信息源</h1>
          <p>{date_cn} | 共 {total_count} 部热门短剧</p>
      </div>
      <div class="stats">
          <div class="stat-item"><div class="stat-value">{topic_count}</div><div class="stat-label">题材</div></div>
          <div class="stat-item"><div class="stat-value">{total_count}</div><div class="stat-label">短剧</div></div>
          <div class="stat-item"><div class="stat-value">{format_number(int(avg_likes))}</div><div class="stat-label">平均互动</div></div>
          <div class="stat-item"><div class="stat-value">{format_number(total_likes)}</div><div class="stat-label">总互动</div></div>
      </div>
      <div class="cards">{category_cards}</div>
      <div class="footer">Generated at {timestamp} by 短剧-小红书信息源 Skill<br>数据说明:每日15:00更新前一天的数据 | 数据来源:红狐Hub</div>
      </body>
      </html>'''
      
          with open(html_file, 'w', encoding='utf-8') as f:
              f.write(html_content)
          return html_file
      
      
      # ============ 缓存 ============
      def load_cache():
          """加载缓存数据(1小时有效期)"""
          if not os.path.exists(CACHE_FILE):
              return None
          try:
              with open(CACHE_FILE, 'r', encoding='utf-8') as f:
                  cache_data = json.load(f)
                  if time.time() - cache_data.get("timestamp", 0) < 3600:
                      return cache_data.get("items")
          except Exception:
              pass
          return None
      
      
      def save_cache(items):
          """保存缓存数据"""
          os.makedirs(CACHE_DIR, exist_ok=True)
          cache_data = {"timestamp": time.time(), "items": items}
          try:
              with open(CACHE_FILE, 'w', encoding='utf-8') as f:
                  json.dump(cache_data, f, ensure_ascii=False, indent=2)
          except Exception:
              pass
      
      
      # ============ 主流程 ============
      def main():
          global OUTPUT_DIR
          parser = argparse.ArgumentParser(description="短剧-小红书信息源日报生成工具 (v2.3增强版)")
          parser.add_argument("--topics", type=str, help="题材关键词,逗号分隔;全部不符合短剧题材词时不请求接口直接提示")
          parser.add_argument("--count", type=int, default=200, help="扫描作品数量")
          parser.add_argument("--date", type=str, help="指定日期 YYYY-MM-DD;超出有效范围或未更新时提示,等待用户确认")
          parser.add_argument("--start-time", type=str, help="开始时间 YYYY-MM-DD HH:MM:SS")
          parser.add_argument("--end-time", type=str, help="结束时间 YYYY-MM-DD HH:MM:SS")
          parser.add_argument("--latest", action="store_true", help="使用最新有数据的日期(用户确认后执行)")
          parser.add_argument("--output-dir", type=str, default=OUTPUT_DIR, help="输出目录")
          parser.add_argument("--api-key", type=str, help="指定 API Key")
          parser.add_argument("--subscribe", action="store_true", help="开启每日订阅")
          parser.add_argument("--unsubscribe", action="store_true", help="关闭每日订阅")
          parser.add_argument("--from-cache", action="store_true", help="使用缓存数据")
          args = parser.parse_args()
      
          if args.subscribe:
              print("✅ 已开启每日订阅,日报将自动保存至:", OUTPUT_DIR)
              return
          if args.unsubscribe:
              print("✅ 已关闭每日订阅")
              return
      
          if args.output_dir:
              OUTPUT_DIR = args.output_dir
      
          api_key = args.api_key or get_api_key()
      
          # ---- v2.3 前置校验①:分类/关键词是否符合短剧题材词库(不满足时不请求任何接口)----
          topics = None
          if args.topics:
              raw_topics = [t.strip() for t in args.topics.split(",") if t.strip()]
              valid_topics, invalid_topics, recommends = check_topics(raw_topics)
              if invalid_topics:
                  print(f"⚠️ 关键词 {invalid_topics} 不满足短剧查询条件(短剧按题材/剧情词匹配标题,非短剧题材词无法查询)")
                  print(f"💡 推荐相关分类和关键词:{'、'.join(recommends[:10])}")
                  if valid_topics:
                      print(f"✅ 已保留有效关键词 {valid_topics} 继续查询,无效关键词已自动忽略")
                      topics = valid_topics
                  else:
                      print("🔇 所有关键词/分类均不满足短剧查询条件,本次未调用任何接口;请使用上述推荐词重新查询")
                      return
              else:
                  topics = valid_topics
      
          # ---- 确定查询日期(保留确认逻辑:无数据不自动回退,先提示等待用户确认)----
          if args.start_time:
              start_time = args.start_time
              date_str = args.start_time[:10]
              end_time = args.end_time or f"{date_str} 23:59:59"
          elif args.latest:
              # 用户确认后执行:按15:00规则定位最新可用日期(不做自动回退探测)
              date_str = calculate_latest_date()
              start_time = f"{date_str} 00:00:00"
              end_time = f"{date_str} 23:59:59"
              print(f"✅ 已定位最新可用日期: {date_str}")
          elif args.date:
              date_str = args.date
              # v2.3 前置校验:日期格式与有效查询范围
              date_ok, date_msg, date_suggest = check_date(date_str)
              if not date_ok:
                  print(f"⚠️ {date_msg}")
                  if date_suggest:
                      print(f"💡 推荐查询时间范围:{date_suggest}(数据每日15:00更新前一天)")
              # ⛔ 确认逻辑:目标日期无数据时,提示用户并等待确认,禁止自动执行
              has_data, latest_date = validate_date(date_str)
              if not has_data:
                  print(f"⚠️ {date_str}数据尚未更新")
                  print(f"数据更新规则:每日15:00更新前一天的数据")
                  print(f"当前可查询的最新日期:{latest_date}")
                  print(f"\n是否需要查询{latest_date}的数据?")
                  return  # 等待用户确认后带 --latest 重跑
              start_time = f"{date_str} 00:00:00"
              end_time = f"{date_str} 23:59:59"
          else:
              date_str = calculate_latest_date()
              start_time = f"{date_str} 00:00:00"
              end_time = f"{date_str} 23:59:59"
      
          # 使用自定义时间(如果提供)
          if args.start_time:
              start_time = args.start_time
              date_str = args.start_time[:10]
          if args.end_time:
              end_time = args.end_time
      
          print(f"🔍 正在查询 {date_str} 的小红书短剧数据...")
      
          items, meta = fetch_playlet_data(
              topics=topics,
              start_time=start_time,
              end_time=end_time,
              count=args.count,
              use_cache=args.from_cache,
          )
      
          if not items:
              reason = meta.get("reason", "unknown")
              hint = {
                  "no_data": "数据源当日无数据(未更新或缺失)",
                  "keyword_no_match": "查询条件(题材词)在该日期无匹配作品",
                  "api_error": "接口调用异常",
              }.get(reason, "未知原因")
              print(f"📭 未查询到相关数据 [原因: {hint}]")
              if reason == "keyword_no_match":
                  print("💡 建议: 改用常见题材词(穿越/霸总/重生/悬疑/甜宠等),或去掉 --topics 查询全部(需确认后执行)")
              elif reason == "no_data":
                  print("💡 建议: 数据源当日无数据,可等15:00更新后重试;或告知后使用 --latest 查询最近有数据的日期")
              return
      
          print(f"✅ 共获取 {len(items)} 部短剧作品")
      
          # 题材聚类
          clusters = cluster_by_topic(items)
          print(f"📊 聚类为 {len(clusters)} 个题材方向")
      
          # 生成HTML日报
          html_file = generate_html_report(items, clusters, date_str)
          print(f"📄 日报已生成:{html_file}")
      
          # 自动打开
          webbrowser.open(f"file://{html_file}")
      
          # 输出终端摘要
          print(f"\n## 短剧-小红书信息源 · {date_str} 日报\n")
          print(f"**扫描 {len(items)} 部热门短剧,聚类 {len(clusters)} 个题材方向**\n")
      
          print("### 题材概览\n")
          print("| 题材 | 数量 | 占比 | 爆款亮点 |")
          print("|------|------|------|---------|")
          for topic, topic_items in sorted(clusters.items(), key=lambda x: len(x[1]), reverse=True):
              top_item = topic_items[0] if topic_items else {}
              title = top_item.get("title", "无标题")
              likes = top_item.get("likeCount", 0)
              print(f"| #{topic} | {len(topic_items)}部 | {len(topic_items)/len(items)*100:.1f}% | 《{title[:20]}》{format_number(likes)}赞 |")
      
      
      if __name__ == "__main__":
          main()
      
    • default_cover.png 2.4 KB · in bundle
  • references
    • core_workflow.md 18.6 KB
      # 短剧-小红书信息源 - 核心工作流程
      
      ## 📋 执行流程概览
      
      ```
      前置输入校验(v2.3:分类/关键词/日期)
        ↓
      第零步:日期有效性预检(必须,保留确认逻辑)
        ↓
      第一步:生成爆款日报(调用API)
        ↓
      第二步:执行创作趋势分析(自动)
        ↓
      输出:HTML日报 + 终端摘要
      ```
      
      ## ⛔ 第零步:日期预检规则(核心)
      
      ### 数据更新机制
      
      - **更新时间**:每日15:00更新前一天的数据
      - **15:00前**:最新可用日期 = T-2(前天)
      - **15:00后**:最新可用日期 = T-1(昨天)
      
      ### 前置校验-日期(v2.3)
      
      查询前先校验日期是否在有效查询范围:
      - 格式必须为 `YYYY-MM-DD`,格式无效时提示"日期格式无效"
      - 不能晚于今天;超出范围时提示"日期超出有效查询范围"并推荐最近可用日期
      
      ### 强制拦截逻辑
      
      ```python
      DATA_UPDATE_HOUR = 15  # 每日15:00更新前一天数据
      
      def calculate_latest_date():
          now = datetime.now()
          if now.hour < DATA_UPDATE_HOUR:
              return (now - timedelta(days=2)).strftime("%Y-%m-%d")
          else:
              return (now - timedelta(days=1)).strftime("%Y-%m-%d")
      ```
      
      ### Agent行为约束
      
      1. **未经用户确认,禁止调用任何数据接口**
      2. **禁止自动执行 `--latest` 参数**
      3. **禁止自动回退**(与B站v2.1+不同,B站自动兜底,本技能保留确认逻辑)
      4. 目标日期无数据时,必须输出提示并等待用户明确确认
      
      ### 标准提示模板
      
      ```
      **⚠️{查询日期}数据尚未更新**
      数据更新规则:每日15:00更新前一天的数据
      当前可查询的最新日期:{最新可查询到数据的日期}
      
      是否需要查询{最新可查询到数据的日期}的数据?
      ```
      
      ### 示例对话
      
      ```
      用户:查询今天的短剧小红书日报
      Agent:⚠️2026-06-16数据尚未更新
            数据更新规则:每日15:00更新前一天的数据
            当前可查询的最新日期:2026-06-14
      
            是否需要查询2026-06-14的数据?
      用户:好的
      Agent:(执行 python3 daily_report.py --latest)
      ```
      
      ## 🧭 前置校验:分类/关键词(v2.3,先于日期处理)
      
      > 查询前先判断用户输入的关键词/分类是否符合**短剧题材词库**,校验在日期处理**之前**执行,确保无效条件时零接口请求。
      
      ### 短剧题材词库
      
      ```
      穿越/霸总/重生/悬疑/甜宠/逆袭/年代/战神/古装/总裁/豪门/复仇/惊悚/推理/反转/爽文/
      科幻/玄幻/修仙/都市/职场/萌宝/萌娃/亲子/离婚/闪婚/替身/虐恋/先婚后爱/双重生
      ```
      
      ### 三种处理分支
      
      | 输入情况 | 行为 |
      |---------|------|
      | 全部不符合(如"智能辅助驾驶""火星人") | **不请求任何接口**(含日期探活),提示"关键词不满足短剧查询条件"并推荐相关分类和关键词,停止 |
      | 部分不符合(混合词,如"穿越,智能驾驶") | 保留有效关键词继续查询,无效关键词自动忽略并提示 |
      | 全部符合 | 正常查询 |
      
      ### 推荐逻辑
      
      优先从无效词中提取包含的题材词(如"穿越重生"→推荐穿越、重生),再补热门题材词(穿越/霸总/重生/甜宠/悬疑/逆袭/年代/战神/古装/都市/科幻)。
      
      ### 标准提示模板
      
      ```
      ⚠️ 关键词 {无效词} 不满足短剧查询条件(短剧按题材/剧情词匹配标题,非短剧题材词无法查询)
      💡 推荐相关分类和关键词:{推荐词列表}
      🔇 所有关键词/分类均不满足短剧查询条件,本次未调用任何接口;请使用上述推荐词重新查询
      ```
      
      ## 📊 第一步:爆款日报生成
      
      ### 默认查询策略
      
      ```bash
      # 生成最新一期日报(用户确认后,自动跳过无数据日期,不扣积分)
      python3 assets/daily_report.py --latest
      
      # 生成指定日期日报(历史日期已有数据,无需确认)
      python3 assets/daily_report.py --date 2026-06-10
      
      # 自定义题材查询(用户指定方向,数据不足时按顺序逐个扩展)
      python3 assets/daily_report.py --topics "穿越,霸总,重生,悬疑" --latest
      
      # 订阅 / 取消订阅
      python3 assets/daily_report.py --subscribe
      python3 assets/daily_report.py --unsubscribe
      ```
      
      ### 智能扩展逻辑
      
      1. 默认查询题材=`全量查询`(不带keyword)
      2. 全量数据不足 **100条** 时,自动追加热门题材:
         - 穿越 → 霸总 → 重生 → 悬疑 → 甜宠 → 逆袭
      3. 所有题材通过批量接口一次性查询,无需逐个调用
      4. 单题材查询为空/异常时,间隔4秒**自动重试1次**
      
      ### 增强机制(v2.3同步)
      
      - **防御式响应解析**:兼容 `{"code":2000,"data":{"list":[]}}` 与直出 `{"list":[]}` 两种格式,防止服务端调整响应结构时脚本静默失效
      - **结构化空因**:空结果时明确输出原因分类(数据源无数据 / 关键词无匹配 / 接口异常)并给出下一步建议
      - **确认逻辑保留**:关键词无匹配时提示建议(改用题材词/去掉 --topics),**不自动降级全量**,等待用户确认;数据源当日无数据时提示等15:00更新或告知后使用 --latest
      
      ### API调用参数
      
      ```json
      {
        "msgType": "短剧",
        "platform": 3,
        "source": "短剧小红书信息源-GitHub",
        "pageNum": 1,
        "pageSize": 200,
        "startTime": "2026-06-15 00:00:00",
        "endTime": "2026-06-15 23:59:59",
        "keyword": "穿越"
      }
      ```
      
      > **注意**: `platform=3` 表示小红书(`platform=1`为抖音,`platform=2`为视频号)。`keyword` 仅在题材!="短剧"时添加。`source` 字段固定为 `"短剧小红书信息源-GitHub"`,用于数据来源追踪。
      
      ### 小红书字段映射
      
      **重要发现**:小红书和抖音的短剧API返回**完全相同的字段结构**。
      
      | 字段 | 说明 | 示例 |
      |------|------|------|
      | photoId | 作品唯一标识 | `"3887749249586745158"` |
      | likeCount | 点赞数 | `5741` |
      | commentCount | 评论数 | `948` |
      | shareCount | 分享数 | `13999` |
      | readCount | 阅读/播放数 | `8885` |
      | url | 作品链接(小红书为null) | `null` |
      | userName | 作者昵称 | `"柒宝追剧"` |
      | userHeadUrl | 作者头像URL | `"https://wx.qlogo.cn/..."` |
      | coverUrl | 封面图 | `"https://wxapp.tc.qq.com/..."` |
      | title | 标题(含话题标签) | `"人间清醒的豪门太太..."` |
      | authorId | 作者ID | `"d879e5f15cea558305c82149ebb3800d"` |
      | gmtCreate | 作品创建时间 | `"2026-06-16 22:00:17"` |
      | gmtModified | 最后更新时间 | `"2026-06-17 12:04:06"` |
      | topic | 主要话题标签 | `"#好剧推荐"` |
      | platform | 平台标识(3=小红书) | `3` |
      | msgType | 内容类型 | `"短剧"` |
      | type | 内容来源类型 | `"xxxbiu"` |
      
      **链接生成规则**:小红书`url`字段为null,需通过`photoId`拼接:
      ```
      https://www.xiaohongshu.com/explore/{photoId}
      ```
      
      **字段使用逻辑**:
      - **去重**:基于 `photoId`
      - **排序**:基于 `likeCount`(降序)
      - **数据为0时**:HTML日报中该指标不展示
      
      ## 📈 第二步:创作趋势分析
      
      日报生成后,**必须**基于聚类结果自动执行创作趋势分析:
      
      ### 分析维度
      
      1. **新兴起量信号**
         - 数量少但互动高的题材
         - 阈值:作品数≤5且平均互动较高
      
      2. **爆款标题特征**
         - 提取标题高频词
         - 统计特征模式出现次数、典型案例、平均互动
      
      3. **核心达人榜**
         - 按作品数+总互动排序
         - 展示达人名称、作品数、总互动、代表作
      
      4. **题材趋势报告**
         - TOP 5题材详细分析
         - 每个题材:作品数、平均互动、头部作品、题材特征、创作建议
      
      5. **跨题材对比建议**
         - 题材融合趋势观察
         - 联动创作建议
      
      ## 🎨 HTML日报格式规范
      
      ### 主题配色
      
      - **背景色**:`#1a1a1a`(深色)
      - **卡片背景**:`#2d2d2d`
      - **主色调**:`#FF2442`(小红书红)
      - **文字色**:`#e8e4df`(浅色)
      
      ### 布局结构
      
      ```
      Header(标题📕+日期)
        ↓
      Stats(4个统计指标:题材数、短剧数、平均互动、总互动)
        ↓
      Cards(题材卡片网格布局)
        ↓
      Footer(生成时间+数据说明)
      ```
      
      ### 卡片内容
      
      每个题材卡片包含:
      - 题材编号+名称+作品数量
      - 前5部热门作品
      - 作品信息:封面+标题(可点击跳转)+作者+互动数据
      - **互动数据为0的字段不展示**
      
      ## 📝 终端输出格式(强制执行)
      
      > ⛔ **严格执行规则**:
      > - 以下模板是**唯一合法输出格式**,禁止任何自由发挥、省略、简化或重新组织
      > - 禁止输出模板中未定义的额外内容(如"我来帮你…""以下是…"等口语化文字)
      > - 禁止合并、跳过任何板块,即使某板块数据为"暂无"也必须保留该板块标题
      > - **每次获取日报后,对话回复必须严格按此模板输出 md 内容,不得包含模板以外的任何文字**
      
      每次运行日报后,对话输出**必须严格**按以下模板原样输出(仅替换 `{...}` 占位符):
      
      ```
      ## 短剧-小红书信息源 · {日期} 日报
      
      **扫描 {N} 部热门短剧,聚类 {M} 个题材方向**
      
      ---
      
      ### 题材概览
      
      | 题材 | 数量 | 占比 | 爆款亮点 |
      |------|------|------|---------|
      | #{题材名} | {N}部 | {X}% | 头部作品亮点描述 |
      | ... | ... | ... | ... |
      
      ---
      
      ### 创作趋势分析
      
      **一、新兴起量信号**
      
      - 🔥 **#{题材}** — 仅{N}部但均互动{X}+,描述
      (若无新兴题材,输出:暂无新兴起量信号)
      
      **二、爆款标题特征**
      
      | 特征模式 | 出现次数 | 典型案例 | 平均互动 |
      |---------|---------|---------|---------|
      | {特征1} | {N}次 | 《{标题}》 | {X}w |
      | ... | ... | ... | ... |
      (若无标题数据,输出:暂无爆款标题数据)
      
      **三、核心达人榜**
      
      | 达人 | 作品数 | 总互动 | 代表作 |
      |------|--------|--------|--------|
      | @{达人} | {N}部 | {X}w | 《{作品}》 |
      | ... | ... | ... | ... |
      (若无达人数据,输出:暂无核心达人数据)
      
      **四、题材趋势报告**
      
      **题材**:#{题材1}
      **作品数**:{N}部
      **平均互动**:{X}w
      **头部作品**:《{标题}》-{互动}w
      
      **题材特征**:{描述该题材的共性特征}
      **创作建议**:{针对该题材的创作建议}
      
      **五、#{题材2}**
      
      (同上格式)
      
      **六、#{题材3}**
      
      (同上格式)
      
      **七、跨题材对比建议**
      
      - **{题材}** — 建议同步关注{相关题材}的联动创作,观察题材融合趋势
      (若无建议,输出:暂无跨题材对比建议)
      
      ---
      
      **日报地址**:{HTML文件绝对路径}
      
      > 数据说明:每日15:00更新昨天的数据
      ```
      
      > 以上格式为**强制规范**,所有字段不可省略,板块标题(一、二、三、四、五、六、七)必须保留。若某模块无数据则在该板块内标注"暂无",不得删除板块本身。
      
      ## 💾 缓存机制
      
      ### 缓存路径
      
      ```
      ~/.workbuddy/cache/playlet_xhs_data.json
      ```
      
      ### 缓存策略
      
      - **有效期**:1小时(3600秒)
      - **去重字段**:`photoId`
      - **排序字段**:`likeCount`(降序)
      
      ## ⚙️ 参数说明
      
      | 参数 | 说明 | 默认值 |
      |------|------|--------|
      | `--topics "词1,词2"` | 自定义题材关键词,逗号分隔。全部不符合短剧题材词库时不请求接口直接提示+推荐;混合词保留有效词 | 全量查询 |
      | `--count` | 扫描作品数量,满足即停 | `200` |
      | `--date` | 指定日期 YYYY-MM-DD(超出有效范围/未更新时提示并询问,等待确认) | 今天 |
      | `--start-time` | 自定义开始时间 YYYY-MM-DD HH:MM:SS(覆盖 --date 推算) | — |
      | `--end-time` | 自定义结束时间 YYYY-MM-DD HH:MM:SS(覆盖 --date 推算) | — |
      | `--latest` | 自动使用最新有数据的日期(用户确认后执行,不扣积分) | — |
      | `--output-dir` | 输出目录 | `~/Downloads/QoderReports` |
      | `--api-key` | 指定 API Key | — |
      | `--subscribe` | 开启每日订阅 | — |
      | `--unsubscribe` | 关闭每日订阅 | — |
      | `--from-cache` | 使用缓存数据 | — |
      
      ## 🔒 安全约束
      
      1. **API Key**从环境变量`REDFOX_API_KEY`获取
      2. **禁止硬编码**密钥
      3. **数据来源唯一性**:仅使用红狐API
      4. **未收录账号**:需先提交收录再查询
      
      ## 📋 平台适配说明(抖音→小红书)
      
      本 Skill 基于"短剧-抖音信息源"改造,通过**最小化变更**原则,仅调整平台相关字段和展示文案。
      
      ### 核心变更对照
      
      | 维度 | 抖音版本 | 小红书版本 |
      |------|---------|-----------|
      | platform参数 | `1` | `3` |
      | source字段 | `短剧抖音信息源-GitHub` | `短剧小红书信息源-GitHub` |
      | 主色调 | `#FB7299`(抖音粉) | `#FF2442`(小红书红) |
      | 图标 | 🎬 | 📕 |
      | 作品链接 | `https://www.douyin.com/video/{photoId}` | `https://www.xiaohongshu.com/explore/{photoId}` |
      | 缓存文件 | `playlet_douyin_data.json` | `playlet_xhs_data.json` |
      | HTML文件名 | `短剧抖音日报_YYYY-MM-DD.html` | `短剧小红书日报_YYYY-MM-DD.html` |
      | 脚本文件名 | `playlet_douyin_daily.py` | `playlet_xhs_daily.py` |
      | 终端文案 | "赞" | "互动" |
      
      ### HTML样式适配
      
      ```css
      /* 主题色统一替换 */
      .header h1 { color: #FF2442; }       /* 原 #FB7299 */
      .stat-value { color: #FF2442; }
      .card-number { color: #FF2442; }
      .article-title:hover { color: #FF2442; }
      ```
      
      ```html
      <!-- 标题图标 -->
      <h1>📕 短剧-小红书信息源</h1>  <!-- 原 🎬 -->
      ```
      
      ### 作品链接适配
      
      ```python
      # 抖音:优先使用url字段,fallback到photoId拼接
      url = item.get("url") or ""
      if url:
          title_html = f'<a href="{url}" target="_blank">{title}</a>'
      elif photo_id:
          title_html = f'<a href="https://www.douyin.com/video/{photo_id}" target="_blank">{title}</a>'
      
      # 小红书:url字段为null,直接用photoId拼接
      photo_id = item.get("photoId") or ""
      if photo_id:
          title_html = f'<a href="https://www.xiaohongshu.com/explore/{photo_id}" target="_blank">{title}</a>'
      ```
      
      ### 改造影响评估
      
      | 模块 | 影响程度 | 说明 |
      |------|---------|------|
      | API调用 | 🟢 低 | 仅platform参数变更(1→3) |
      | 数据处理 | 🟢 低 | 字段结构完全一致,无需额外适配 |
      | HTML展示 | 🟢 低 | 主题色+文案+链接域名调整 |
      | 终端输出 | 🟢 低 | "赞"→"互动"文案调整 |
      | 工作流程 | ⚪ 无 | 完全保持一致 |
      
      **保持不变的部分**:
      - 工作流程(第零步日期预检 → 第一步生成日报 → 第二步趋势分析)
      - 日期规则(每日15:00更新前一天数据)
      - 题材聚类逻辑(9大题材关键词匹配规则)
      - 查询策略(默认查询全部,数据不足时自动扩展题材)
      - 缓存机制(1小时有效期,基于photoId去重)
      - 输出格式(HTML日报结构 + 终端摘要模板)
      - 参数体系、订阅功能、API鉴权方式
      - 创作趋势分析(5个分析维度)
      
      ## 📝 字段映射修正记录
      
      > 修正日期:2026-06-18
      
      ### 问题背景
      
      在初版测试中,脚本使用了与抖音版本不同的小红书字段名(如 `noteId`、`useLikeCount`、`collectedCount` 等),但 API 实际返回的字段名与抖音**完全一致**,导致所有互动数据显示为 0。
      
      ### 错误字段对照(已修正)
      
      | 修正前(错误) | 修正后(正确) | 问题说明 |
      |-------------|-------------|---------|
      | `noteId` | `photoId` | 小红书不存在 noteId 字段 |
      | `useLikeCount` | `likeCount` | 字段名错误 |
      | `useCommentCount` | `commentCount` | 字段名错误 |
      | `collectedCount` | `shareCount` | 字段名错误,且语义不同(收藏→分享) |
      | `photoJumpUrl` | 不存在(url为null) | 完全错误的字段名 |
      | `interactiveCount` | 不存在 | 完全错误的字段名 |
      | `desc` | 不存在 | 小红书短剧无 desc 字段 |
      
      ### 核心修正代码
      
      **去重逻辑**:
      ```python
      # 修正前
      item_id = item.get("noteId") or item.get("photoId")
      # 修正后
      item_id = item.get("photoId")
      ```
      
      **排序逻辑**:
      ```python
      # 修正前
      unique_items.sort(key=lambda x: x.get("useLikeCount", 0) or x.get("interactiveCount", 0), reverse=True)
      # 修正后
      unique_items.sort(key=lambda x: x.get("likeCount", 0), reverse=True)
      ```
      
      **互动数据提取**:
      ```python
      # 修正前
      likes = format_number(item.get("useLikeCount", 0) or item.get("interactiveCount", 0))
      comments = format_number(item.get("useCommentCount", 0))
      collected = format_number(item.get("collectedCount", 0))
      # 修正后
      likes = format_number(item.get("likeCount", 0))
      comments = format_number(item.get("commentCount", 0))
      shares = format_number(item.get("shareCount", 0))
      ```
      
      **链接生成**:
      ```python
      # 修正前(复杂且错误)
      url = item.get("photoJumpUrl") or item.get("url") or ""
      note_id = item.get("noteId") or item.get("photoId") or ""
      # 修正后(简洁正确)
      photo_id = item.get("photoId") or ""
      if photo_id:
          title_html = f'<a href="https://www.xiaohongshu.com/explore/{photo_id}" target="_blank">{title}</a>'
      ```
      
      ### 关键结论
      
      **小红书和抖音的短剧 API 返回完全相同的字段结构**:
      - 字段名完全一致(photoId、likeCount、commentCount、shareCount 等)
      - 数据类型完全一致(number、string)
      - 唯一差异:
        1. `platform` 参数不同(抖音=1, 小红书=3)
        2. `url` 字段在小红书中返回 null
        3. 链接拼接域名不同(douyin.com vs xiaohongshu.com)
      
      ## ✅ 改造验证清单
      
      - [x] API接口地址正确(`https://redfox.hk/story/api/parseWork/queryPlayletMsgs`)
      - [x] platform参数改为3(小红书)
      - [x] 字段映射确认(与抖音完全一致:photoId、likeCount等)
      - [x] 去重逻辑使用photoId
      - [x] 排序逻辑使用likeCount(降序)
      - [x] 链接生成适配小红书URL格式(`xiaohongshu.com/explore/`)
      - [x] HTML主题色改为 `#FF2442`
      - [x] 图标改为 📕
      - [x] 文件命名统一改为xhs
      - [x] 数字格式化逻辑(与抖音一致,万→w)
      - [x] 终端输出文案"赞"→"互动"
      - [x] 字段映射修正(noteId→photoId、useLikeCount→likeCount等)
      - [x] 互动数据为0时隐藏该字段
      - [x] 封面图HEIF→JPG格式兼容转换
      - [x] SKILL.md文档更新
      - [x] 参考文档同步更新
      - [x] API请求新增 `source` 字段(`"短剧小红书信息源-GitHub"`)
      
      ## 🧭 v2.3 增强同步记录(从B站信息源同步,保留确认逻辑)
      
      > 同步日期:2026-08-18
      
      ### 同步内容(与B站 v2.3 对齐)
      
      - [x] 前置输入校验:分类/关键词是否符合短剧题材词库(check_topics)
      - [x] 前置输入校验:日期格式与有效查询范围(check_date)
      - [x] 无效条件零请求:关键词/分类全部不满足时不请求任何接口,直接提示+推荐
      - [x] 混合词处理:保留有效关键词查询,无效词自动忽略
      - [x] 题材词库 TOPIC_THESAURUS(30词)+ 推荐逻辑
      - [x] 防御式响应解析(parse_response,兼容两种格式)
      - [x] 自动扩展题材阈值统一为100条(AUTO_EXPAND_THRESHOLD)
      - [x] 空结果自动重试1次(间隔4秒)
      - [x] 结构化空因(no_data/keyword_no_match/api_error)
      
      ### 有意保留的差异(确认逻辑)
      
      > ⛔ 与B站版本不同,本技能**保留"不主动查询数据,需用户确认后再执行"**的逻辑:
      
      | 行为 | B站 v2.1+ | 小红书 v2.3 |
      |------|----------|------------|
      | 日期未更新/超范围 | 自动回退最近有数据日期并直接出日报 | **提示+询问,等待用户确认后执行** |
      | 关键词查询后无匹配 | 自动降级全量查询 | **提示建议,等待用户确认** |
      | --latest 自动回退探测 | 自动向前探测最多7天 | 按15:00规则定位,不做自动探测 |
      | 前置校验无效条件 | 不请求接口直接提示+推荐 | 不请求接口直接提示+推荐(一致) |
      
    • examples.md 10.2 KB
      # 短剧-小红书信息源 - 使用示例
      
      ## 📖 基础用法
      
      ### 示例1:生成最新日报
      
      ```bash
      python3 assets/daily_report.py --latest
      ```
      
      **执行流程**:
      1. 自动计算最新可用日期(15:00规则)
      2. 查询全部短剧内容
      3. 题材聚类+生成HTML日报
      4. 自动浏览器打开
      
      **终端输出**:
      ```
      🔍 正在查询 2026-06-15 的小红书短剧数据...
      ✅ 共获取 156 部短剧作品
      📊 聚类为 8 个题材方向
      📄 日报已生成:/Users/xxx/Downloads/QoderReports/短剧小红书日报_2026-06-15.html
      
      ## 短剧-小红书信息源 · 2026-06-15 日报
      
      **扫描 156 部热门短剧,聚类 8 个题材方向**
      
      ### 题材概览
      
      | 题材 | 数量 | 占比 | 爆款亮点 |
      |------|------|------|---------|
      | #穿越 | 45部 | 28.8% | 《重生之大宋风云》12.5w互动 |
      | #霸总 | 32部 | 20.5% | 《豪门宠妻日常》8.3w互动 |
      | ... | ... | ... | ... |
      ```
      
      ### 示例2:查询历史日期
      
      ```bash
      python3 assets/daily_report.py --date 2026-06-10
      ```
      
      **说明**:历史日期已有数据,无需确认,直接查询
      
      ### 示例3:自定义题材查询
      
      ```bash
      # 查询穿越题材
      python3 assets/daily_report.py --topics "穿越,时空,重生"
      
      # 查询霸总/甜宠题材
      python3 assets/daily_report.py --topics "霸总,甜宠,总裁,虐恋"
      
      # 查询悬疑题材
      python3 assets/daily_report.py --topics "悬疑,推理,反转,惊悚"
      ```
      
      **查询逻辑**:
      - 用户提供的题材通过批量接口一次性查询
      - 结果自动去重
      - 题材聚类基于查询结果生成
      
      ### 示例3.1:无效关键词(v2.3前置校验)
      
      ```bash
      # 全部不符合短剧题材词库 → 不请求任何接口,直接提示+推荐
      python3 assets/daily_report.py --topics "智能辅助驾驶,火星人"
      
      # 输出:
      # ⚠️ 关键词 ['智能辅助驾驶', '火星人'] 不满足短剧查询条件(短剧按题材/剧情词匹配标题,非短剧题材词无法查询)
      # 💡 推荐相关分类和关键词:穿越、霸总、重生、甜宠、悬疑、逆袭、年代、战神、古装、都市
      # 🔇 所有关键词/分类均不满足短剧查询条件,本次未调用任何接口;请使用上述推荐词重新查询
      
      # 混合词 → 保留有效关键词继续查询,无效词忽略
      python3 assets/daily_report.py --topics "穿越,智能驾驶"
      
      # 输出:
      # ⚠️ 关键词 ['智能驾驶'] 不满足短剧查询条件(...)
      # 💡 推荐相关分类和关键词:穿越、霸总、重生、...
      # ✅ 已保留有效关键词 ['穿越'] 继续查询,无效关键词已自动忽略
      ```
      
      **说明**:
      - 短剧题材词库:穿越/霸总/重生/悬疑/甜宠/逆袭/年代/战神/古装/总裁/豪门/复仇/惊悚/推理/反转/爽文/科幻/玄幻/修仙/都市/职场/萌宝/萌娃/亲子/离婚/闪婚/替身/虐恋/先婚后爱/双重生
      - 全部不满足时**零接口请求**(含日期探活),直接提示并推荐相关词
      - 混合词保留有效关键词查询,无效词自动忽略
      
      ## 🎯 高级用法
      
      ### 示例4:自定义时间范围
      
      ```bash
      python3 assets/daily_report.py \
        --start-time "2026-06-10 08:00:00" \
        --end-time "2026-06-15 20:00:00"
      ```
      
      **说明**:覆盖`--date`推算,精确控制查询时间窗口
      
      ### 示例5:使用缓存数据
      
      ```bash
      python3 assets/daily_report.py --from-cache
      ```
      
      **说明**:
      - 从`~/.workbuddy/cache/playlet_xhs_data.json`加载
      - 缓存有效期1小时
      - 不扣积分,快速预览
      
      ### 示例6:开启每日订阅
      
      ```bash
      python3 assets/daily_report.py --subscribe
      ```
      
      **效果**:
      - 日报自动保存至`~/Downloads/QoderReports/`
      - 文件命名:`短剧小红书日报_YYYY-MM-DD.html`
      
      ### 示例7:指定输出目录
      
      ```bash
      python3 assets/daily_report.py --latest --output-dir "/Users/xxx/Desktop/Reports"
      ```
      
      ## 📊 完整输出示例
      
      ### HTML日报预览
      
      ```
      📕 短剧-小红书信息源
      2026年6月15日 星期一 | 共 156 部热门短剧
      
      ┌────────┬────────┬────────┬────────┐
      │  题材   │  短剧   │ 平均互动 │  总互动  │
      ├────────┼────────┼────────┼────────┤
      │   8    │  156   │  6.2w  │ 967.2w │
      └────────┴────────┴────────┴────────┘
      
      ┌─────────────────────────────────────────┐
      │ 01 #穿越                    45 部       │
      ├─────────────────────────────────────────┤
      │ [封面] 《重生之大宋风云》               │
      │        @历史剧作家                        │
      │        ⭐ 3.2w  👍 12.5w  💬 8562      │
      │                                         │
      │ [封面] 《回到唐朝当王爷》               │
      │        @古装短剧                          │
      │        ⭐ 2.8w  👍 10.3w  💬 7234      │
      │ ... (展示前5部)                          │
      └─────────────────────────────────────────┘
      
      Generated at 2026-06-16 10:30:45 by 短剧-小红书信息源 Skill
      数据说明:每日15:00更新前一天的数据 | 数据来源:红狐Hub
      ```
      
      ### 终端摘要输出
      
      ```markdown
      ## 短剧-小红书信息源 · 2026-06-15 日报
      
      **扫描 156 部热门短剧,聚类 8 个题材方向**
      
      ---
      
      ### 题材概览
      
      | 题材 | 数量 | 占比 | 爆款亮点 |
      |------|------|------|---------|
      | #穿越 | 45部 | 28.8% | 《重生之大宋风云》12.5w互动 |
      | #霸总 | 32部 | 20.5% | 《豪门宠妻日常》8.3w互动 |
      | #重生 | 28部 | 17.9% | 《逆袭之路》7.6w互动 |
      | #悬疑 | 21部 | 13.5% | 《谜案追踪》6.8w互动 |
      | #甜宠 | 15部 | 9.6% | 《甜蜜暴击》5.2w互动 |
      | #逆袭 | 8部 | 5.1% | 《翻身做主》4.5w互动 |
      | #年代 | 5部 | 3.2% | 《八零往事》3.8w互动 |
      | #其他 | 2部 | 1.3% | 《都市情缘》2.1w互动 |
      
      ---
      
      ### 创作趋势分析
      
      **一、新兴起量信号**
      
      - 🔥 **#年代** — 仅5部但均互动3.8w+,八零九零年代题材 resurgence,建议关注怀旧风创作
      
      **二、爆款标题特征**
      
      | 特征模式 | 出现次数 | 典型案例 | 平均互动 |
      |---------|---------|---------|---------|
      | "重生/回到" | 73次 | 《重生之大宋风云》 | 8.5w |
      | "豪门/总裁" | 45次 | 《豪门宠妻日常》 | 6.2w |
      | "逆袭/翻身" | 32次 | 《逆袭之路》 | 5.8w |
      
      **三、核心达人榜**
      
      | 达人 | 作品数 | 总互动 | 代表作 |
      |------|--------|--------|--------|
      | @历史剧作家 | 12部 | 45.6w | 《重生之大宋风云》 |
      | @古装短剧 | 9部 | 38.2w | 《回到唐朝当王爷》 |
      | @甜宠工厂 | 8部 | 32.5w | 《甜蜜暴击》 |
      
      **四、题材趋势报告**
      
      **题材**:#穿越
      **作品数**:45部
      **平均互动**:6.8w
      **头部作品**:《重生之大宋风云》-12.5w互动
      
      **题材特征**:以古代穿越为主,大宋/唐朝背景占比60%,强调历史细节还原
      **创作建议**:结合真实历史事件+虚构人物,增强代入感;封面使用古风场景
      
      **五、#霸总**
      
      **作品数**:32部
      **平均互动**:5.2w
      **头部作品**:《豪门宠妻日常》-8.3w互动
      
      **题材特征**:豪门恩怨+宠妻元素,强调身份反差和情感张力
      **创作建议**:突出男主"霸道"与"温柔"的反差,设置悬念钩子引导追更
      
      **六、#重生**
      
      **作品数**:28部
      **平均互动**:4.9w
      **头部作品**:《逆袭之路》-7.6w互动
      
      **题材特征**:重生逆袭+打脸爽文,节奏快、反转多
      **创作建议**:前3秒设置冲突点,每集结尾留悬念,提升完播率
      
      **七、跨题材对比建议**
      
      - **穿越+重生** — 建议同步关注"重生穿越"的联动创作,观察题材融合趋势
      - **霸总+甜宠** — 可尝试"霸总甜宠"细分方向,满足女性用户情感需求
      
      ---
      
      **日报地址**:/Users/xxx/Downloads/QoderReports/短剧小红书日报_2026-06-15.html
      
      > 数据说明:每日15:00更新昨天的数据
      ```
      
      ## ⚠️ 日期预检示例
      
      ### 场景1:15:00前查询今天
      
      ```
      用户:查询今天的短剧小红书日报
      
      Agent:⚠️2026-06-16数据尚未更新
            数据更新规则:每日15:00更新前一天的数据
            当前可查询的最新日期:2026-06-14
      
            是否需要查询2026-06-14的数据?
      
      用户:好的
      
      Agent:(执行 python3 daily_report.py --latest)
      ```
      
      ### 场景2:15:00后查询今天
      
      ```
      用户:查询今天的短剧小红书日报
      
      Agent:(直接执行 python3 daily_report.py --latest,查询2026-06-15数据)
      ```
      
      ### 场景3:查询历史日期
      
      ```
      用户:查询2026-06-10的短剧小红书日报
      
      Agent:(直接执行 python3 daily_report.py --date 2026-06-10,无需确认)
      ```
      
      ## 🔧 故障排查
      
      ### 问题1:API Key未配置
      
      ```
      ❌ 错误:未找到 REDFOX_API_KEY 环境变量
      请先配置:export REDFOX_API_KEY=<你的apikey>
      ```
      
      **解决**:
      ```bash
      # macOS/Linux
      echo 'export REDFOX_API_KEY=ak_xxxxxxxx' >> ~/.zshrc
      source ~/.zshrc
      
      # 验证
      echo $REDFOX_API_KEY
      ```
      
      ### 问题2:目标日期无数据
      
      ```
      ⚠️ 2026-06-16数据尚未更新
      数据更新规则:每日15:00更新前一天的数据
      当前可查询的最新日期:2026-06-14
      
      是否需要查询2026-06-14的数据?
      ```
      
      **解决**:等待用户确认后,使用`--latest`参数查询最新可用日期
      
      ### 问题3:未查询到数据
      
      ```
      🔍 正在查询 2026-06-15 的小红书短剧数据...
      📭 未查询到相关数据
      ```
      
      **可能原因**:
      1. 该日期确实无短剧内容
      2. API Key权限不足
      3. 网络问题
      
      **解决**:
      - 确认API Key有效性
      - 尝试查询其他日期
      - 检查网络连接
      
      ### 问题4:缓存过期
      
      ```
      🔍 正在查询 2026-06-15 的小红书短剧数据...
      (未使用缓存,重新查询)
      ```
      
      **说明**:缓存有效期1小时,超时后自动失效,重新调用API
      
      ## 💡 最佳实践
      
      1. **首次使用**:配置API Key → 执行`--latest`测试
      2. **日常使用**:直接`--latest`,自动处理日期逻辑
      3. **自定义查询**:使用`--topics`指定题材方向
      4. **快速预览**:使用`--from-cache`避免重复调用API
      5. **批量分析**:结合`--start-time`和`--end-time`查询时间范围
      6. **订阅模式**:开启`--subscribe`自动攒日报
      
      ## 📚 相关文档
      
      - [SKILL.md](../SKILL.md) — 功能说明、鉴权、工作流程
      - [core_workflow.md](core_workflow.md) — 核心执行流程、格式模板、日期判断逻辑
      
  • scripts
    • playlet_xhs_daily.py 28.4 KB
      #!/usr/bin/env python3
      # -*- coding: utf-8 -*-
      """
      短剧-小红书信息源日报生成脚本 (增强版 v2.3)
      ===========================================
      每日扫描小红书短剧爆款内容,智能聚类题材后生成HTML日报
      
      v2.3 同步说明(从 B站信息源同步增强,保留"需用户确认"逻辑):
      - 【前置校验】查询前先判断用户的分类/关键词是否符合短剧题材词库、日期是否在有效
        查询范围;关键词/分类全部不满足时【不请求任何接口】,直接提示"关键词不满足短剧
        查询条件"并推荐相关分类和关键词后停止;混合词保留有效关键词查询。
      - 【防御式解析】兼容 {"code":2000,"data":{"list":[...]}} 与直出 list 两种响应格式,
        防止服务端调整响应结构时脚本静默失效。
      - 【自动扩展题材】全量查询数据不足(小于 AUTO_EXPAND_THRESHOLD)时,自动追加
        穿越→霸总→重生→悬疑→甜宠→逆袭 定向查询补充数据。
      - 【空结果重试】单题材查询为空/异常时,间隔 RETRY_INTERVAL 秒重试 1 次。
      - 【结构化空因】空结果时明确输出原因分类:无数据 / 关键词无匹配 / API异常。
      - ⛔【保留确认逻辑】目标日期无数据时,禁止自动回退/自动降级查询,必须先提示用户
        并等待确认后才能执行(与 B站版本不同,B站 v2.1 起为自动兜底直接出日报)。
      
      用法(与原版完全兼容):
          python3 daily_report.py --latest          # 用户确认后执行
          python3 daily_report.py --date 2026-06-10
          python3 daily_report.py --topics "穿越,霸总" --latest
      """
      
      import argparse
      import json
      import os
      import sys
      import time
      import webbrowser
      from datetime import datetime, timedelta
      from urllib import request, error
      
      
      # ============ 配置 ============
      API_BASE_URL = "https://redfox.hk/story/api/parseWork/queryPlayletMsgs"
      CACHE_DIR = os.path.expanduser("~/.workbuddy/cache")
      CACHE_FILE = os.path.join(CACHE_DIR, "playlet_xhs_data.json")
      OUTPUT_DIR = os.path.expanduser("~/Downloads/QoderReports")
      DATA_UPDATE_HOUR = 15       # 数据源声称的更新时刻(15:00更新前一天数据)
      RETRY_TIMES = 1             # 空结果/异常重试次数
      RETRY_INTERVAL = 4          # 重试间隔(秒)
      REQUEST_TIMEOUT = 30        # 单次请求超时(秒)
      AUTO_EXPAND_THRESHOLD = 100 # 全量结果少于该值时自动扩展题材
      AUTO_EXPAND_TOPICS = ["穿越", "霸总", "重生", "悬疑", "甜宠", "逆袭"]  # 扩展顺序
      
      # 短剧题材词库:用于 --topics 输入校验提示与推荐
      TOPIC_THESAURUS = {
          "穿越", "霸总", "重生", "悬疑", "甜宠", "逆袭", "年代", "战神",
          "古装", "总裁", "豪门", "复仇", "惊悚", "推理", "反转", "爽文",
          "科幻", "玄幻", "修仙", "都市", "职场", "萌宝", "萌娃", "亲子",
          "离婚", "闪婚", "替身", "虐恋", "先婚后爱", "双重生",
      }
      
      
      # ============ 工具函数 ============
      def get_api_key():
          """从环境变量获取 API Key"""
          api_key = os.environ.get("REDFOX_API_KEY")
          if not api_key:
              print("❌ 错误:未找到 REDFOX_API_KEY 环境变量")
              print("请先配置:export REDFOX_API_KEY=<你的apikey>")
              sys.exit(1)
          return api_key
      
      
      def calculate_latest_date():
          """按15:00规则估算最新可用日期"""
          now = datetime.now()
          if now.hour < DATA_UPDATE_HOUR:
              # 15:00前,最新可用日期是前天
              return (now - timedelta(days=2)).strftime("%Y-%m-%d")
          else:
              # 15:00后,最新可用日期是昨天
              return (now - timedelta(days=1)).strftime("%Y-%m-%d")
      
      
      def validate_date(date_str):
          """基于本地15:00规则判断目标日期是否有数据(确认逻辑核心)"""
          latest_date = calculate_latest_date()
          target_date = datetime.strptime(date_str, "%Y-%m-%d")
          latest = datetime.strptime(latest_date, "%Y-%m-%d")
          return target_date <= latest, latest_date
      
      
      def check_topics(topics):
          """
          v2.3 前置校验:判断用户输入的分类/关键词是否符合短剧题材词库。
          返回 (有效词列表, 无效词列表, 推荐词列表)
          推荐逻辑:优先从无效词中提取包含的题材词,再补充热门题材词。
          """
          hot_topics = ["穿越", "霸总", "重生", "甜宠", "悬疑", "逆袭",
                        "年代", "战神", "古装", "都市", "科幻"]
          valid, invalid = [], []
          for t in topics:
              if t in TOPIC_THESAURUS or t == "短剧":
                  valid.append(t)
              else:
                  invalid.append(t)
          recommends = []
          for t in invalid:
              # 无效词若包含题材词(如"穿越重生"含"穿越""重生"),优先推荐
              contained = [w for w in TOPIC_THESAURUS if w in t or t in w]
              for c in contained:
                  if c not in recommends:
                      recommends.append(c)
          for h in hot_topics:
              if h not in recommends:
                  recommends.append(h)
          return valid, invalid, recommends
      
      
      def check_date(date_str):
          """
          v2.3 前置校验:判断日期是否在有效查询范围(格式正确、不晚于今天)。
          返回 (是否有效, 提示信息, 推荐日期或None)
          """
          try:
              d = datetime.strptime(date_str, "%Y-%m-%d")
          except ValueError:
              return False, f"日期格式无效:{date_str}(应为 YYYY-MM-DD)", None
          today = datetime.now().date()
          if d.date() > today:
              latest = calculate_latest_date()
              return False, f"日期 {date_str} 超出有效查询范围(晚于今天,数据每日15:00更新前一天)", latest
          return True, "", None
      
      
      def parse_response(result):
          """
          防御式响应解析(加固):兼容两种格式
          格式A(API实际完整响应): {"code":2000,"data":{"list":[...],"total":M},"msg":"..."}
          格式B(兜底/直出):      {"list":[...], "pageNum":1, "pages":N, "total":M}
          返回: (items列表, error_msg或None)
          """
          if not isinstance(result, dict):
              return [], "响应非JSON对象"
          # 格式A:code/data 包装(当前 API 实际格式)
          if result.get("code") == 2000:
              data = result.get("data") or {}
              return data.get("list") or [], None
          # 显式业务错误
          code = result.get("code")
          if code is not None:
              msg = result.get("msg")
              return [], f"API业务错误 code={code} msg={msg}"
          # 格式B:直出 list(兜底)
          if "list" in result:
              return result.get("list") or [], None
          return [], None
      
      
      def http_post(payload, api_key):
          """执行一次 POST 请求,返回原始响应 dict(网络/HTTP 层异常向上抛)"""
          data = json.dumps(payload, ensure_ascii=False).encode('utf-8')
          req = request.Request(
              API_BASE_URL,
              data=data,
              headers={
                  "Content-Type": "application/json",
                  "X-API-KEY": api_key
              },
              method="POST"
          )
          with request.urlopen(req, timeout=REQUEST_TIMEOUT) as response:
              return json.loads(response.read().decode('utf-8'))
      
      
      def build_payload(start_time, end_time, keyword=None, page_size=200):
          """构建请求体;keyword 为 None 时表示全量查询(不带 keyword 字段)"""
          payload = {
              "msgType": "短剧",
              "platform": 3,  # 3=小红书 (1=抖音,2=视频号,6=B站)
              "source": "短剧小红书信息源-GitHub",
              "pageNum": 1,
              "pageSize": page_size,
              "startTime": start_time,
              "endTime": end_time,
          }
          if keyword:
              payload["keyword"] = keyword
          return payload
      
      
      def _fetch_topic_once(api_key, payload, topic):
          """单题材单次查询(含重试),返回 (items, api_error: bool)"""
          for attempt in range(RETRY_TIMES + 1):
              try:
                  result = http_post(payload, api_key)
                  items, err = parse_response(result)
                  if err:
                      if attempt < RETRY_TIMES:
                          time.sleep(RETRY_INTERVAL)
                      continue
                  return items, False  # 解析成功(可能为空 list,但非异常)
              except Exception as e:
                  if attempt < RETRY_TIMES:
                      print(f"  ⚠️ 题材 {topic} 第{attempt+1}次请求失败({e}),{RETRY_INTERVAL}s后重试...")
                      time.sleep(RETRY_INTERVAL)
                  else:
                      print(f"❌ 查询题材 {topic} 失败:{str(e)}")
          return [], True
      
      
      # ============ 数据获取 ============
      def fetch_playlet_data(
          topics=None,
          start_time=None,
          end_time=None,
          count=200,
          use_cache=False,
      ):
          """
          调用 API 查询小红书短剧数据(增强版)
      
          Args:
              topics: 题材列表(逗号分隔),None/空 → 全量查询,数据不足时自动扩展题材
              start_time / end_time: 查询时间窗
              count: 扫描作品数量
              use_cache: 是否使用缓存
      
          Returns:
              (items, meta) 其中 meta 含 reason 字段用于结构化空因:
                  reason in {"ok", "no_data", "keyword_no_match", "api_error"}
          """
          if use_cache:
              cached_data = load_cache()
              if cached_data:
                  print("📦 使用缓存数据")
                  return cached_data, {"reason": "ok", "note": "cache"}
      
          if not start_time or not end_time:
              latest_date = calculate_latest_date()
              start_time = f"{latest_date} 00:00:00"
              end_time = f"{latest_date} 23:59:59"
      
          api_key = get_api_key()
          meta = {"reason": "ok"}
      
          # 确定查询题材序列
          # 用户指定题材 → 仅用用户列表(自定义时不用扩展列表)
          # 未指定 → 全量查询;数据不足时自动按 AUTO_EXPAND_TOPICS 扩展
          if topics:
              query_topics = list(topics)
              auto_expand = False
          else:
              query_topics = [None]
              auto_expand = True
      
          all_items = []
          api_errors = 0
          expanded = False
      
          for topic in query_topics:
              keyword = None if topic is None or topic == "短剧" else topic
              payload = build_payload(start_time, end_time, keyword=keyword,
                                      page_size=min(count, 200))
              items, had_error = _fetch_topic_once(api_key, payload, topic or "全量")
              if had_error:
                  api_errors += 1
              if items:
                  all_items.extend(items)
      
              # 自动扩展题材:去重数不足且未满 count 时继续
              if auto_expand:
                  unique_ids = {it.get("photoId") for it in all_items if it.get("photoId")}
                  if len(unique_ids) < min(count, AUTO_EXPAND_THRESHOLD) and not expanded:
                      expanded = True
                      print(f"  ➕ 全量数据不足({len(unique_ids)}条 < {AUTO_EXPAND_THRESHOLD}),"
                            f"自动扩展题材: {'→'.join(AUTO_EXPAND_TOPICS)}")
                      for extra in AUTO_EXPAND_TOPICS:
                          ep = build_payload(start_time, end_time, keyword=extra,
                                             page_size=min(count, 200))
                          e_items, e_err = _fetch_topic_once(api_key, ep, extra)
                          if e_err:
                              api_errors += 1
                          if e_items:
                              all_items.extend(e_items)
                          unique_ids = {it.get("photoId") for it in all_items if it.get("photoId")}
                          if len(unique_ids) >= count:
                              break
                      break  # 扩展后结束循环
      
          # 去重(基于photoId)
          seen = set()
          unique_items = []
          for item in all_items:
              item_id = item.get("photoId")
              if item_id and item_id not in seen:
                  seen.add(item_id)
                  unique_items.append(item)
      
          # 按互动量排序(使用likeCount)
          unique_items.sort(key=lambda x: x.get("likeCount", 0) or 0, reverse=True)
      
          if not unique_items:
              # 查询结果为空 → 区分关键词无匹配 / 数据源无数据
              if topics and topics != [None]:
                  meta["reason"] = "keyword_no_match"
              else:
                  meta["reason"] = "no_data"
              return [], meta
      
          # 保存缓存
          save_cache(unique_items)
          meta["reason"] = "ok"
          return unique_items[:count], meta
      
      
      # ============ 题材聚类 ============
      def cluster_by_topic(items):
          """按题材聚类作品(9大题材关键词匹配+自动归类)"""
          topic_keywords = {
              "穿越": ["穿越", "时空", "古代", "现代", "回到", "大宋", "北宋", "南宋", "唐朝", "明朝", "清朝"],
              "霸总": ["霸总", "总裁", "豪门", "冷酷", "宠妻", "娇妻", "替身"],
              "重生": ["重生", "逆袭", "回到", "翻盘", "重来", "再生"],
              "悬疑": ["悬疑", "推理", "反转", "惊悚", "谜案", "秘密", "真相"],
              "甜宠": ["甜宠", "恋爱", "撒糖", "甜蜜", "宠溺", "甜甜", "撒糖"],
              "逆袭": ["逆袭", "翻身", "打脸", "崛起", "反击", "报复"],
              "年代": ["年代", "八零", "九零", "七零", "六零"],
              "战神": ["战神", "龙王", "兵王", "高手"],
              "古装": ["古装", "宫廷", "皇后", "贵妃", "王爷", "世子"]
          }
          clusters = {}
          for item in items:
              title = item.get("title", "") or item.get("desc", "")
              matched_topics = []
              for topic, keywords in topic_keywords.items():
                  if any(kw in title for kw in keywords):
                      matched_topics.append(topic)
              matched_topic = matched_topics[0] if matched_topics else "其他"
              clusters.setdefault(matched_topic, []).append(item)
          return clusters
      
      
      # ============ HTML 日报 ============
      def format_number(num):
          """格式化数字(万→w),兼容字符串数值(如"12w+")"""
          if num is None:
              return "0"
          if isinstance(num, str):
              num = num.replace("+", "").replace("w", "0000").replace("亿", "00000000")
              try:
                  num = float(num)
              except Exception:
                  return "0"
          if num >= 10000:
              return f"{num/10000:.1f}w"
          return str(int(num))
      
      
      def generate_html_report(items, clusters, date_str):
          """生成HTML日报(小红书红 #FF2442,HEIF自动转JPG,封面fallback)"""
          os.makedirs(OUTPUT_DIR, exist_ok=True)
      
          html_file = os.path.join(OUTPUT_DIR, f"短剧小红书日报_{date_str}.html")
      
          try:
              dt_obj = datetime.strptime(date_str, "%Y-%m-%d")
              weekdays = ["一", "二", "三", "四", "五", "六", "日"]
              date_cn = f"{dt_obj.year}年{dt_obj.month}月{dt_obj.day}日 星期{weekdays[dt_obj.weekday()]}"
          except ValueError:
              date_cn = date_str
      
          total_count = len(items)
          topic_count = len(clusters)
          total_likes = sum(item.get("likeCount", 0) or 0 for item in items)
          avg_likes = total_likes / total_count if total_count > 0 else 0
      
          # 默认封面图路径(用于加载失败时的fallback)
          default_cover_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "assets", "default_cover.png"))
      
          category_cards = ""
          for i, (topic, topic_items) in enumerate(
                  sorted(clusters.items(), key=lambda x: len(x[1]), reverse=True), 1):
              articles_html = ""
              for item in topic_items[:5]:  # 每个题材展示前5个
                  title = item.get("title", "无标题")
                  author = item.get("userName", "")
                  cover = item.get("coverUrl") or ""
                  # 将HEIF格式转换为JPG格式,提高浏览器兼容性
                  if cover and "format/heif" in cover:
                      cover = cover.replace("format/heif", "format/jpg")
                  photo_id = item.get("photoId") or ""  # 作品ID
                  likes_raw = item.get("likeCount", 0) or 0
                  comments_raw = item.get("commentCount", 0) or 0
                  shares_raw = item.get("shareCount", 0) or 0
                  likes = format_number(likes_raw)
                  comments = format_number(comments_raw)
                  shares = format_number(shares_raw)
      
                  cover_html = ""
                  if cover:
                      cover_html = (f'<img class="article-cover" src="{cover}" alt="" loading="lazy" '
                                    f'referrerpolicy="no-referrer" '
                                    f'onerror="this.style.display=&apos;none&apos;;this.nextElementSibling.style.display=&apos;block&apos;">'
                                    f'<img class="article-cover" src="file://{default_cover_path}" alt="" loading="lazy" style="display:none">')
      
                  # 生成作品链接:使用photoId拼接小红书链接
                  if photo_id:
                      title_html = f'<a href="https://www.xiaohongshu.com/explore/{photo_id}" target="_blank" class="article-title">{title}</a>'
                  else:
                      title_html = f'<span class="article-title">{title}</span>'
      
                  # 动态构建指标项:数据为0时不展示该字段
                  metrics_parts = []
                  if shares_raw > 0:
                      metrics_parts.append(f'<span class="metric">🔗 {shares}</span>')
                  if likes_raw > 0:
                      metrics_parts.append(f'<span class="metric">👍 {likes}</span>')
                  if comments_raw > 0:
                      metrics_parts.append(f'<span class="metric">💬 {comments}</span>')
                  metrics_html = '\n                                '.join(metrics_parts)
      
                  articles_html += f'''
                      <div class="article-item">
                          {cover_html}
                          <div class="article-info">
                              {title_html}
                              <div class="article-meta">
                                  <span class="author">{author}</span>
                                  <span class="metrics">
                                      {metrics_html}
                                  </span>
                              </div>
                          </div>
                      </div>'''
      
              category_cards += f'''
              <div class="category-card reveal">
                  <div class="card-header">
                      <span class="card-number">{i:02d}</span>
                      <h3 class="card-category">#{topic}</h3>
                      <span class="card-count">{len(topic_items)} 部</span>
                  </div>
                  <div class="card-body">{articles_html}
                  </div>
              </div>'''
      
          timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
          html_content = f'''<!DOCTYPE html>
      <html lang="zh-CN">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>短剧-小红书信息源 - {date_str}</title>
      <style>
      * {{ margin: 0; padding: 0; box-sizing: border-box; }}
      body {{ font-family: -apple-system, sans-serif; background: #1a1a1a; color: #e8e4df; padding: 2rem; }}
      .header {{ text-align: center; padding: 2rem 0; }}
      .header h1 {{ font-size: 2rem; color: #FF2442; }}
      .header p {{ color: #9a9590; margin-top: 0.5rem; }}
      .stats {{ display: flex; justify-content: center; gap: 2rem; padding: 1rem; margin: 1rem 0; }}
      .stat-item {{ text-align: center; }}
      .stat-value {{ font-size: 1.5rem; font-weight: bold; color: #FF2442; }}
      .stat-label {{ font-size: 0.8rem; color: #9a9590; }}
      .cards {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 1.5rem; max-width: 1200px; margin: 2rem auto; }}
      .category-card {{ background: #2d2d2d; border-radius: 12px; padding: 1.5rem; }}
      .card-header {{ display: flex; align-items: center; gap: 0.8rem; margin-bottom: 1rem; padding-bottom: 0.8rem; border-bottom: 1px solid #3d3d3d; }}
      .card-number {{ font-size: 1.5rem; font-weight: bold; color: #FF2442; }}
      .card-category {{ flex: 1; font-size: 1.1rem; }}
      .card-count {{ color: #9a9590; font-size: 0.9rem; }}
      .article-item {{ padding: 0.6rem 0; border-bottom: 1px solid #3d3d3d; display: flex; gap: 0.8rem; }}
      .article-item:last-child {{ border-bottom: none; }}
      .article-cover {{ width: 60px; height: 60px; border-radius: 6px; object-fit: cover; flex-shrink: 0; }}
      .article-info {{ flex: 1; min-width: 0; }}
      .article-title {{ color: #e8e4df; font-size: 0.9rem; line-height: 1.4; display: block; }}
      .article-title:hover {{ color: #FF2442; text-decoration: underline; cursor: pointer; }}
      a.article-title {{ text-decoration: none; }}
      a.article-title:hover {{ color: #FF2442; text-decoration: underline; }}
      .article-meta {{ display: flex; justify-content: space-between; margin-top: 0.3rem; font-size: 0.75rem; color: #9a9590; }}
      .metrics {{ display: flex; gap: 0.8rem; }}
      .footer {{ text-align: center; padding: 2rem; color: #666; font-size: 0.8rem; }}
      .reveal {{ animation: fadeIn 0.5s ease-in; }}
      @keyframes fadeIn {{ from {{ opacity: 0; transform: translateY(20px); }} to {{ opacity: 1; transform: translateY(0); }} }}
      </style>
      </head>
      <body>
      <div class="header">
          <h1>📕 短剧-小红书信息源</h1>
          <p>{date_cn} | 共 {total_count} 部热门短剧</p>
      </div>
      <div class="stats">
          <div class="stat-item"><div class="stat-value">{topic_count}</div><div class="stat-label">题材</div></div>
          <div class="stat-item"><div class="stat-value">{total_count}</div><div class="stat-label">短剧</div></div>
          <div class="stat-item"><div class="stat-value">{format_number(int(avg_likes))}</div><div class="stat-label">平均互动</div></div>
          <div class="stat-item"><div class="stat-value">{format_number(total_likes)}</div><div class="stat-label">总互动</div></div>
      </div>
      <div class="cards">{category_cards}</div>
      <div class="footer">Generated at {timestamp} by 短剧-小红书信息源 Skill<br>数据说明:每日15:00更新前一天的数据 | 数据来源:红狐Hub</div>
      </body>
      </html>'''
      
          with open(html_file, 'w', encoding='utf-8') as f:
              f.write(html_content)
          return html_file
      
      
      # ============ 缓存 ============
      def load_cache():
          """加载缓存数据(1小时有效期)"""
          if not os.path.exists(CACHE_FILE):
              return None
          try:
              with open(CACHE_FILE, 'r', encoding='utf-8') as f:
                  cache_data = json.load(f)
                  if time.time() - cache_data.get("timestamp", 0) < 3600:
                      return cache_data.get("items")
          except Exception:
              pass
          return None
      
      
      def save_cache(items):
          """保存缓存数据"""
          os.makedirs(CACHE_DIR, exist_ok=True)
          cache_data = {"timestamp": time.time(), "items": items}
          try:
              with open(CACHE_FILE, 'w', encoding='utf-8') as f:
                  json.dump(cache_data, f, ensure_ascii=False, indent=2)
          except Exception:
              pass
      
      
      # ============ 主流程 ============
      def main():
          global OUTPUT_DIR
          parser = argparse.ArgumentParser(description="短剧-小红书信息源日报生成工具 (v2.3增强版)")
          parser.add_argument("--topics", type=str, help="题材关键词,逗号分隔;全部不符合短剧题材词时不请求接口直接提示")
          parser.add_argument("--count", type=int, default=200, help="扫描作品数量")
          parser.add_argument("--date", type=str, help="指定日期 YYYY-MM-DD;超出有效范围或未更新时提示,等待用户确认")
          parser.add_argument("--start-time", type=str, help="开始时间 YYYY-MM-DD HH:MM:SS")
          parser.add_argument("--end-time", type=str, help="结束时间 YYYY-MM-DD HH:MM:SS")
          parser.add_argument("--latest", action="store_true", help="使用最新有数据的日期(用户确认后执行)")
          parser.add_argument("--output-dir", type=str, default=OUTPUT_DIR, help="输出目录")
          parser.add_argument("--api-key", type=str, help="指定 API Key")
          parser.add_argument("--subscribe", action="store_true", help="开启每日订阅")
          parser.add_argument("--unsubscribe", action="store_true", help="关闭每日订阅")
          parser.add_argument("--from-cache", action="store_true", help="使用缓存数据")
          args = parser.parse_args()
      
          if args.subscribe:
              print("✅ 已开启每日订阅,日报将自动保存至:", OUTPUT_DIR)
              return
          if args.unsubscribe:
              print("✅ 已关闭每日订阅")
              return
      
          if args.output_dir:
              OUTPUT_DIR = args.output_dir
      
          api_key = args.api_key or get_api_key()
      
          # ---- v2.3 前置校验①:分类/关键词是否符合短剧题材词库(不满足时不请求任何接口)----
          topics = None
          if args.topics:
              raw_topics = [t.strip() for t in args.topics.split(",") if t.strip()]
              valid_topics, invalid_topics, recommends = check_topics(raw_topics)
              if invalid_topics:
                  print(f"⚠️ 关键词 {invalid_topics} 不满足短剧查询条件(短剧按题材/剧情词匹配标题,非短剧题材词无法查询)")
                  print(f"💡 推荐相关分类和关键词:{'、'.join(recommends[:10])}")
                  if valid_topics:
                      print(f"✅ 已保留有效关键词 {valid_topics} 继续查询,无效关键词已自动忽略")
                      topics = valid_topics
                  else:
                      print("🔇 所有关键词/分类均不满足短剧查询条件,本次未调用任何接口;请使用上述推荐词重新查询")
                      return
              else:
                  topics = valid_topics
      
          # ---- 确定查询日期(保留确认逻辑:无数据不自动回退,先提示等待用户确认)----
          if args.start_time:
              start_time = args.start_time
              date_str = args.start_time[:10]
              end_time = args.end_time or f"{date_str} 23:59:59"
          elif args.latest:
              # 用户确认后执行:按15:00规则定位最新可用日期(不做自动回退探测)
              date_str = calculate_latest_date()
              start_time = f"{date_str} 00:00:00"
              end_time = f"{date_str} 23:59:59"
              print(f"✅ 已定位最新可用日期: {date_str}")
          elif args.date:
              date_str = args.date
              # v2.3 前置校验:日期格式与有效查询范围
              date_ok, date_msg, date_suggest = check_date(date_str)
              if not date_ok:
                  print(f"⚠️ {date_msg}")
                  if date_suggest:
                      print(f"💡 推荐查询时间范围:{date_suggest}(数据每日15:00更新前一天)")
              # ⛔ 确认逻辑:目标日期无数据时,提示用户并等待确认,禁止自动执行
              has_data, latest_date = validate_date(date_str)
              if not has_data:
                  print(f"⚠️ {date_str}数据尚未更新")
                  print(f"数据更新规则:每日15:00更新前一天的数据")
                  print(f"当前可查询的最新日期:{latest_date}")
                  print(f"\n是否需要查询{latest_date}的数据?")
                  return  # 等待用户确认后带 --latest 重跑
              start_time = f"{date_str} 00:00:00"
              end_time = f"{date_str} 23:59:59"
          else:
              date_str = calculate_latest_date()
              start_time = f"{date_str} 00:00:00"
              end_time = f"{date_str} 23:59:59"
      
          # 使用自定义时间(如果提供)
          if args.start_time:
              start_time = args.start_time
              date_str = args.start_time[:10]
          if args.end_time:
              end_time = args.end_time
      
          print(f"🔍 正在查询 {date_str} 的小红书短剧数据...")
      
          items, meta = fetch_playlet_data(
              topics=topics,
              start_time=start_time,
              end_time=end_time,
              count=args.count,
              use_cache=args.from_cache,
          )
      
          if not items:
              reason = meta.get("reason", "unknown")
              hint = {
                  "no_data": "数据源当日无数据(未更新或缺失)",
                  "keyword_no_match": "查询条件(题材词)在该日期无匹配作品",
                  "api_error": "接口调用异常",
              }.get(reason, "未知原因")
              print(f"📭 未查询到相关数据 [原因: {hint}]")
              if reason == "keyword_no_match":
                  print("💡 建议: 改用常见题材词(穿越/霸总/重生/悬疑/甜宠等),或去掉 --topics 查询全部(需确认后执行)")
              elif reason == "no_data":
                  print("💡 建议: 数据源当日无数据,可等15:00更新后重试;或告知后使用 --latest 查询最近有数据的日期")
              return
      
          print(f"✅ 共获取 {len(items)} 部短剧作品")
      
          # 题材聚类
          clusters = cluster_by_topic(items)
          print(f"📊 聚类为 {len(clusters)} 个题材方向")
      
          # 生成HTML日报
          html_file = generate_html_report(items, clusters, date_str)
          print(f"📄 日报已生成:{html_file}")
      
          # 自动打开
          webbrowser.open(f"file://{html_file}")
      
          # 输出终端摘要
          print(f"\n## 短剧-小红书信息源 · {date_str} 日报\n")
          print(f"**扫描 {len(items)} 部热门短剧,聚类 {len(clusters)} 个题材方向**\n")
      
          print("### 题材概览\n")
          print("| 题材 | 数量 | 占比 | 爆款亮点 |")
          print("|------|------|------|---------|")
          for topic, topic_items in sorted(clusters.items(), key=lambda x: len(x[1]), reverse=True):
              top_item = topic_items[0] if topic_items else {}
              title = top_item.get("title", "无标题")
              likes = top_item.get("likeCount", 0)
              print(f"| #{topic} | {len(topic_items)}部 | {len(topic_items)/len(items)*100:.1f}% | 《{title[:20]}》{format_number(likes)}赞 |")
      
      
      if __name__ == "__main__":
          main()
      
  • README.en.md 5.8 KB
    # Playlet Xiaohongshu Feed / playlet-xhs-feed
    
    ---
    
    ## Introduction
    
    Daily scanning of trending Xiaohongshu short drama content, filtering popular posts by engagement metrics, intelligently clustering by genre, and generating a visual daily report with creative insights — helping short drama creators capture the pulse of trending content.
    
    **Core Value**
    
    - 📊 Daily automatic scanning of Xiaohongshu short drama content, filtering trending works by engagement
    - 🏷️ Intelligent genre clustering (time travel / CEO romance / rebirth / suspense, etc.), dynamically determined by content each day
    - 📈 Generates a visual daily report with cover images, engagement data, and creative insights
    - 🔍 Supports targeted queries by genre, creator, and time range
    - 🔔 One-click subscription for automatic daily report delivery to local folder
    
    **Who It's For**
    
    - 🎬 Short Drama Screenwriters/Producers — Capture trending signals and improve pitch accuracy
    - 📊 Short Drama Operations/MCNs — Track competitor performance and refine operational strategies
    - 🔍 Content Strategists/Data Analysts — Produce structured trend reports to support content decisions
    
    ---
    
    ## Features
    
    ### Core Features
    
    - 📊 **Trending Discovery** — Filter high-engagement short drama content from Xiaohongshu, pinpointing high-heat posts
    - 🏷️ **Genre Clustering** — Auto-identify genres (time travel / CEO romance / rebirth / suspense, etc.), dynamically determined each day
    - 🔍 **Smart Querying** — Query all short dramas by default, auto-expanding genres in batches when data is sparse to save API quota
    - 🎯 **Custom Queries** — Target specific genres, creators, or keywords for flexible niche exploration
    - 📈 **Creative Insights** — Analyze trending title patterns, genre trends, and top creator performance
    - 🎨 **Visual Daily Report** — Dark-themed page with cover images, engagement data, and direct post links
    - 🔔 **One-Click Subscription** — Enable daily auto-generation with reports saved to local folder
    
    ### Highlights
    
    - ⚡ **Smart Date Detection** — Built-in 15:00 daily update rule auto-checks data availability, no manual calculation needed
    - 📱 **Xiaohongshu Native** — Post links auto-generated as `xiaohongshu.com/explore/`, zero-engagement fields hidden automatically
    - 🔒 **Secure & Reliable** — API Key accessed only via environment variable, hardcoding strictly prohibited
    
    ---
    
    ## API Key Acquisition & Security
    
    - This skill requires the environment variable: `REDFOX_API_KEY`.
    - `REDFOX_API_KEY` is provided by [RedFoxHub](https://redfox.hk/settings/api-keys?source=github) (`https://redfox.hk`).
    - Please visit [RedFoxHub](https://redfox.hk?source=github) to register and obtain your `REDFOX_API_KEY`.
    - Configure the environment variable `REDFOX_API_KEY` on your device before using this skill.
    - Before providing your key, verify its source, available scope, validity period, and whether reset/revocation is supported.
    - Do not hardcode or expose the key in plaintext within code, prompts, logs, or output files.
    
    ---
    
    ## Usage Guide
    
    Simply describe your needs in natural language — no commands to memorize.
    
    ### Quick Reference
    
    | Intent | Example Prompt | Outcome |
    |--------|---------------|---------|
    | Get latest daily report | "Generate the latest Xiaohongshu short drama daily report" / "Show me short drama trends" | Fetches the latest available daily report with genre clustering and trend analysis |
    | Query a historical date | "Show me the June 10 Xiaohongshu short drama report" | Generates a daily report for the specified date |
    | Query by genre | "Show me time travel short drama trends" / "Check CEO romance and sweet romance genres" | Targeted genre query with automatic deduplication and clustering |
    | Creative trend analysis | "Analyze recent short drama creative trends" / "What's trending in short dramas" | Outputs genre trends, title pattern analysis, top creator rankings, and cross-genre suggestions |
    | Enable daily subscription | "Turn on daily short drama report subscription" | Reports auto-generated daily and saved to local folder |
    
    ### Output Example
    
    Once the daily report is generated, you will receive:
    
    - **📊 Genre Overview Table** — Count, share, and highlight pieces for each genre
    - **🔥 Emerging Signals** — Low-volume but high-engagement potential genres
    - **📝 Trending Title Patterns** — Frequent title patterns, occurrence count, and average engagement
    - **👤 Top Creator Rankings** — Creator work count, total engagement, and representative pieces
    - **📈 Genre Trend Report** — Detailed analysis and creative suggestions for top genres
    - **🌐 Cross-Genre Suggestions** — Genre fusion trend observations
    
    A visual report page is also generated, with cover images, engagement data, and direct post links, automatically opened in your browser.
    
    ---
    
    ## Use Cases
    
    | Scenario | Role | Example Prompt | Benefit |
    |----------|------|---------------|---------|
    | Content Inspiration | Screenwriter/Producer | "Generate the latest Xiaohongshu short drama daily report" | Understand trending genres and topics to guide creative decisions |
    | Competitive Monitoring | Operations/MCN | "How are CEO romance dramas performing lately" | Track competitor performance and refine strategies |
    | Trend Analysis | Strategist/Analyst | "Analyze recent short drama creative trends" | Produce structured trend reports to support content strategy |
    
    ---
    
    ## Important Data Notes
    
    - **Data Update Time**: Daily at 15:00, updating the previous day's data
    - Before 15:00, the latest available date is two days prior (T-2); after 15:00, it is the previous day (T-1)
    - When target date data has not yet been updated, users are prompted for confirmation before any API call
    - Data source: RedFoxHub API, based on Xiaohongshu platform (platform=3)
    - Data tracking: API requests include a `source` field set to `短剧小红书信息源-GitHub` for usage attribution
    
  • README.md 5.5 KB
    # 短剧-小红书信息源 / playlet-xhs-feed
    
    ---
    
    ## 简介
    
    每日自动扫描小红书短剧爆款内容,按互动量筛选热门笔记,智能聚类题材方向后生成可视化日报,同步提供创作趋势分析,帮助短剧创作者精准把握流量风口。
    
    **核心价值**
    
    - 📊 每日自动扫描小红书短剧内容,按互动量筛选爆款作品
    - 🏷️ 智能聚类题材方向(穿越/霸总/重生/悬疑等),每天分类由内容动态决定
    - 📈 生成包含封面图、互动数据与创作洞察的可视化日报
    - 🔍 支持按题材、达人、时间范围定向查询
    - 🔔 一键订阅,日报自动保存到本地文件夹
    
    **适用对象**
    
    - 🎬 短剧编剧/制作人 — 精准把握流量风口,提升选题命中率
    - 📊 短剧运营/MCN — 追踪竞品动态,优化自身运营策略
    - 🔍 内容策划/数据分析师 — 产出结构化趋势报告,支撑内容战略决策
    
    ---
    
    ## 功能特性
    
    ### 核心功能
    
    - 📊 **爆款发现** — 从小红书短剧中按互动量筛选热门内容,精准定位高热度短剧笔记
    - 🏷️ **题材聚类** — 自动识别题材方向(穿越/霸总/重生/悬疑等),每天分类由内容动态决定
    - 🔍 **智能查询** — 默认查询全部短剧,数据不足时自动扩展题材批量查询,节省接口额度
    - 🎯 **自定义查询** — 可指定任意题材、达人、关键词定向查询,灵活覆盖短剧细分方向
    - 📈 **创作洞察** — 分析爆款标题特征、题材趋势、达人表现,深度挖掘创作规律
    - 🎨 **可视化日报** — 深色主题页面,封面图 + 互动数据 + 笔记直链,直观展示每日短剧热点
    - 🔔 **一键订阅** — 开启每日自动产出,日报自动保存至本地文件夹
    
    ### 特色亮点
    
    - 🧭 **前置输入校验** — 查询前先校验分类/关键词是否符合短剧题材词库、日期是否在有效查询范围;全部不满足时不请求接口,直接提示并推荐相关词
    - ⚡ **智能日期判断** — 内置每日 15:00 更新规则,自动检测目标日期数据可用性,无需手动推算
    - 📱 **小红书原生适配** — 作品链接自动拼接 `xiaohongshu.com/explore/`,互动数据为零的字段自动隐藏
    - 🔒 **安全可靠** — API Key 仅通过环境变量获取,禁止硬编码,保障数据安全
    
    ---
    
    ## 密钥获取与安全说明
    
    - 本技能需要使用环境变量:`REDFOX_API_KEY`。
    - `REDFOX_API_KEY` 由 [红狐 hub](https://redfox.hk/settings/api-keys?source=github) (`https://redfox.hk`)提供。
    - 请前往 [红狐 hub](https://redfox.hk?source=github) 注册账号,获取 `REDFOX_API_KEY`。
    - 配置设备环境变量 `REDFOX_API_KEY` 后使用本技能。
    - 在提供密钥前,请先确认密钥来源、可用范围、有效期及是否支持重置/撤销。
    - 禁止在代码、提示词、日志或输出文件中硬编码/明文暴露密钥。
    
    ---
    
    ## 使用指南
    
    直接用自然语言描述需求,无需记忆命令。
    
    ### 常用说法速查
    
    | 意图 | 示例话术 | 效果 |
    |------|---------|------|
    | 获取最新日报 | "帮我生成最新的短剧小红书日报" / "查询短剧热点" | 自动获取最新可用日期的爆款日报,含题材聚类与趋势分析 |
    | 查询历史日期 | "查询 6 月 10 日的短剧小红书日报" | 生成指定日期的爆款内容日报 |
    | 按题材定向查询 | "看看穿越题材的短剧趋势" / "查询霸总和甜宠题材" | 定向查询指定题材,结果自动去重聚类 |
    | 创作趋势分析 | "分析近期短剧创作趋势" / "短剧爆款有哪些" | 输出题材趋势、爆款标题特征、达人排行与跨题材建议 |
    | 开启每日订阅 | "帮我开启每日短剧日报订阅" | 日报每日自动生成并保存到本地文件夹 |
    
    ### 输出示例
    
    日报生成后,你将获得:
    
    - **📊 题材概览表** — 各题材数量、占比及头部作品亮点
    - **🔥 新兴起量信号** — 数量少但互动高的潜力题材
    - **📝 爆款标题特征** — 高频标题模式、出现次数与平均互动
    - **👤 核心达人榜** — 达人作品数、总互动与代表作
    - **📈 题材趋势报告** — Top 题材的详细分析与创作建议
    - **🌐 跨题材对比建议** — 题材融合趋势观察
    
    同时生成一个可视化日报页面,包含封面图、互动数据和笔记直链,自动在浏览器中打开。
    
    ---
    
    ## 使用场景
    
    | 场景 | 角色 | 示例问法 | 收益 |
    |------|------|---------|------|
    | 短剧选题参考 | 短剧编剧/制作人 | "帮我生成最新的短剧小红书日报" | 了解热门题材和爆款趋势,指导选题决策 |
    | 竞品动态监控 | 短剧运营/MCN | "看看霸总题材最近表现怎么样" | 追踪竞品账号表现,优化运营策略 |
    | 内容趋势分析 | 内容策划/分析师 | "分析近一周短剧创作趋势" | 产出结构化趋势报告,支撑内容战略决策 |
    
    ---
    
    ## 重要数据说明
    
    - **数据更新时间**:每日 15:00 更新前一天的数据
    - 15:00 前,最新可用日期为前天(T-2);15:00 后,最新可用日期为昨天(T-1)
    - 目标日期数据尚未更新时,会先提示用户确认,不会自动调用接口(保留确认逻辑,禁止自动回退)
    - 关键词/分类不符合短剧题材词库时,不请求任何接口,直接提示并推荐相关词
    - 数据来源:红狐 Hub API,基于小红书平台(platform=3)
    - 数据追踪:API 请求通过 `source` 字段标识为 `短剧小红书信息源-GitHub`,用于数据来源统计
    
  • SKILL.md 19 KB
    ---
    name: playlet-xhs-feed
    description: "短剧-小红书信息源 — 每日扫描小红书短剧爆款内容,按互动量筛选热门笔记,智能聚类题材方向后生成包含封面、互动数据与创作洞察的HTML日报。支持按题材(穿越/霸总/重生等)、达人、时间范围定向查询。⚠️数据每日15:00更新前一天数据,目标日期无数据时必须先告知用户并等待确认后才能调用接口,禁止自动获取。⚠️查询前先校验分类/关键词是否符合短剧题材词库,不满足时不请求接口直接提示并推荐相关词。当用户需要短剧小红书日报、小红书短剧爆款、短剧热点、短剧创作趋势或自定义题材查询时使用。"
    ---
    
    # 短剧-小红书信息源
    
    ## 简介
    
    **短剧-小红书信息源**是一款专为短剧创作者设计的小红书爆款内容追踪工具,参考AI-B站信息源的功能和样式设计。
    
    通过红狐Hub API,你可以:
    - 📊 每日自动扫描小红书短剧内容,按互动量筛选爆款作品
    - 🏷️ 智能聚类题材方向(穿越/霸总/重生/悬疑等)
    - 📈 生成包含封面图、互动数据与创作洞察的可视化HTML日报
    - 🔍 支持按题材、达人、时间范围定向查询
    
    适用于短剧编剧、制作人、运营人员等需要把握小红书短剧流量风口的场景。
    
    > ⚠️ **重要-确认逻辑**:数据每日15:00更新前一天数据,目标日期无数据时**禁止自动调用接口/禁止自动回退**,必须先告知用户并等待确认后才能执行。
    >
    > 🧭 **重要-前置校验**:查询前先校验分类/关键词是否符合短剧题材词库(穿越/霸总/重生/甜宠/悬疑/逆袭等)、日期是否在有效查询范围;关键词/分类全部不满足时**不请求任何接口**,直接提示"关键词不满足查询条件"并推荐相关分类和关键词。
    >
    > ⛔ **输出规范**:日报生成后,对话回复**必须严格按照输出模板输出 md 内容**,禁止任何自由发挥、省略或口语化文字。详见下方「📊 输出格式」。
    
    ## 功能特性
    
    ### 🎯 核心功能
    
    | 功能模块 | 能力描述 | 核心价值 |
    |---------|---------|----------|
    | 📊 爆款发现 | 从小红书短剧中按互动量筛选热门内容 | 精准定位高热度短剧笔记 |
    | 🏷️ 题材聚类 | 自动识别题材方向(穿越/霸总/重生/悬疑等) | 每天题材分类由内容动态决定 |
    | 🔍 智能查询 | 默认查询全部短剧,数据不足时自动扩展题材批量查询 | 节省接口额度,高效获取数据 |
    | 🎯 自定义查询 | 用户可指定任意题材/达人/关键词定向查询 | 灵活覆盖任意短剧细分方向 |
    | 📈 创作洞察 | 分析爆款标题特征、题材趋势、达人表现 | 深度挖掘创作规律 |
    | 🎨 可视化日报 | 深色主题HTML,封面图+互动数据+笔记直链 | 直观展示每日短剧热点 |
    | 🔔 一键订阅 | `--subscribe` 开启每日自动产出 | 日报自动攒在本地文件夹 |
    
    ### ✨ 特色亮点
    
    - **🧭 前置输入校验(v2.3)**:查询前先判断分类/关键词是否符合短剧题材词库、日期是否在有效查询范围;全部不满足时**不请求任何接口**,直接提醒"关键词不满足查询条件"并推荐相关分类和关键词;混合词保留有效关键词查询
    - **⚡ 智能日期判断**:脚本内置 `DATA_UPDATE_HOUR = 15` 常量,调用接口前自动检测目标日期是否在无数据区间,无数据时提示用户等待确认
    - **🧠 结构化空因**:空结果时明确输出原因分类(数据源无数据/关键词无匹配/接口异常)并给出下一步建议
    - **🔄 自动扩展题材**:全量查询数据不足(<100条)时,自动追加穿越→霸总→重生→悬疑→甜宠→逆袭定向查询补充
    - **🔁 空结果重试**:单题材查询为空/异常时,间隔4秒自动重试1次
    - **📱 小红书适配**:作品链接自动拼接 `xiaohongshu.com/explore/{photoId}`,互动数据为0的字段自动隐藏,HEIF封面自动转JPG,加载失败自动fallback默认封面
    - **🔒 安全可靠**:API Key通过环境变量 `REDFOX_API_KEY` 获取,禁止硬编码
    
    ## 一键安装
    
    ### 前置条件
    
    - Python 3 运行环境
    - 已注册红狐Hub账号并获取 API Key
    
    ### 安装步骤
    
    #### 1. 获取 API Key
    
    前往 [红狐Hub 官网](https://redfox.hk?source=github) 注册,登录后在个人中心获取,格式为 `ak_xxxxxxxx`。新注册用户获赠免费积分。
    
    #### 2. 配置环境变量
    
    **macOS/Linux**:
    ```bash
    echo 'export REDFOX_API_KEY=ak_xxxxxxxx' >> ~/.zshrc
    source ~/.zshrc
    ```
    
    **Windows**(PowerShell):
    ```powershell
    [Environment]::SetEnvironmentVariable("REDFOX_API_KEY", "ak_xxxxxxxx", "User")
    ```
    配置后需**重启终端**使环境变量生效。
    
    #### 3. 验证配置
    
    ```bash
    # macOS/Linux
    echo $REDFOX_API_KEY
    
    # Windows
    echo %REDFOX_API_KEY%
    ```
    
    ### 环境变量配置
    
    | 变量名 | 必填 | 说明 |
    |--------|------|------|
    | `REDFOX_API_KEY` | 是 | 红狐Hub API访问密钥,通过 `X-API-KEY` 请求头鉴权 |
    
    ## 工作流程
    
    ### 第零步:日期有效性预检(必须执行,先于任何接口调用)
    
    > ⛔ **核心规则:未经用户确认,禁止调用任何数据接口,禁止自动执行 `--latest`**
    >
    > **绝对不能**在用户未确认的情况下自动执行脚本获取数据(与B站版本不同,B站为自动兜底,本技能保留确认逻辑)。
    
    **数据更新规则**:每日15:00更新前一天的数据
    - 15:00前:最新可用日期 = T-2(前天)
    - 15:00后:最新可用日期 = T-1(昨天)
    
    **执行流程(每次查询前强制执行)**:
    
    1. **前置校验-日期(v2.3)**:检查日期格式(YYYY-MM-DD)且不晚于今天;超出有效查询范围时提醒并推荐最近可用日期
    2. 获取当前系统日期 T 和当前时间,按15:00规则计算最新可用日期
    3. 判断用户请求的目标日期是否在无数据区间(即 > 最新可用日期)
    4. **若目标日期有数据**(≤ 最新可用日期):直接执行查询,无需额外确认
    5. **若目标日期无数据**(> 最新可用日期),向用户输出以下提示,并**等待用户明确确认**后才能执行(带 `--latest` 参数);若用户拒绝,则不执行任何接口调用:
    
    ```
    **⚠️{查询日期}数据尚未更新**
    数据更新规则:每日15:00更新前一天的数据
    当前可查询的最新日期:{最新可查询到数据的日期}
    
    是否需要查询{最新可查询到数据的日期}的数据?
    ```
    
    **示例对话**:
    
    ```
    用户:查询今天的短剧小红书日报
    Agent:⚠️2026-06-16数据尚未更新
          数据更新规则:每日15:00更新前一天的数据
          当前可查询的最新日期:2026-06-14
    
          是否需要查询2026-06-14的数据?
    用户:好的
    Agent:(执行 python3 daily_report.py --latest)
    ```
    
    ### 前置校验:分类/关键词(v2.3,先于日期处理)
    
    > 查询前先判断用户输入的关键词/分类是否符合**短剧题材词库**(穿越/霸总/重生/甜宠/悬疑/逆袭/年代/战神/古装/都市/科幻等)。
    
    - **全部不符合**:不请求任何接口(含日期探活),直接提示"关键词不满足短剧查询条件"并**推荐相关分类和关键词**后停止
    - **部分不符合(混合词)**:保留有效关键词继续查询,无效关键词自动忽略并提示
    - **全部符合**:正常查询
    
    推荐逻辑:优先从无效词中提取包含的题材词(如"穿越重生"→推荐穿越、重生),再补热门题材词。
    
    ### 第一步:生成爆款日报
    
    ```bash
    # 生成最新一期日报(用户确认后执行)
    python3 "$SKILL_PATH/assets/daily_report.py" --latest
    
    # 生成指定日期日报(历史日期已有数据,无需确认)
    python3 "$SKILL_PATH/assets/daily_report.py" --date 2026-06-10
    
    # 自定义题材查询(用户指定方向,自动校验关键词)
    python3 "$SKILL_PATH/assets/daily_report.py" --topics "穿越,霸总,重生,悬疑" --latest
    
    # 订阅 / 取消订阅
    python3 "$SKILL_PATH/assets/daily_report.py" --subscribe
    python3 "$SKILL_PATH/assets/daily_report.py" --unsubscribe
    ```
    
    > **查询策略**:默认查询全部短剧内容(pageSize=200),数据不足(<100条)时自动追加热门题材(穿越→霸总→重生→悬疑→甜宠→逆袭),所有题材通过批量接口一次性查询。用户自定义题材时仅使用用户提供的有效题材词,同样批量查询。
    
    > **日期智能判断**:脚本内置 `DATA_UPDATE_HOUR = 15` 常量(每日15:00更新前一天数据),调用接口前自动检测目标日期是否在无数据区间。作为双保险,Agent 在第零步已提前拦截,避免脚本层的交互提示被忽略。
    
    ### 第二步:执行创作趋势分析
    
    日报生成后,**必须**基于聚类结果自动执行创作趋势分析:
    
    1. 读取题材聚类结果,选取TOP 5热门题材
    2. 分析每个题材的爆款数量、平均互动数据、头部作品特征
    3. 识别新兴起量题材(数量少但互动高)
    4. 输出结构化创作趋势报告
    
    生成的HTML日报保存在 `~/Downloads/QoderReports/`,自动浏览器打开。终端同步输出题材分类表格 + 创作趋势分析报告。
    
    ## 📊 输出格式(强制执行)
    
    > ⛔ **严格执行规则**:
    > - 以下模板是**唯一合法输出格式**,禁止任何自由发挥、省略、简化或重新组织
    > - 禁止输出模板中未定义的额外内容(如"我来帮你…""以下是…"等口语化文字)
    > - 禁止合并、跳过任何板块,即使某板块数据为"暂无"也必须保留该板块标题
    > - **每次获取日报后,对话回复必须严格按此模板输出 md 内容,不得包含模板以外的任何文字**
    
    每次运行日报后,对话输出**必须严格**按以下模板原样输出(仅替换 `{...}` 占位符):
    
    ```
    ## 短剧-小红书信息源 · {日期} 日报
    
    **扫描 {N} 部热门短剧,聚类 {M} 个题材方向**
    
    ---
    
    ### 题材概览
    
    | 题材 | 数量 | 占比 | 爆款亮点 |
    |------|------|------|---------|
    | #{题材名} | {N}部 | {X}% | 头部作品亮点描述 |
    | ... | ... | ... | ... |
    
    ---
    
    ### 创作趋势分析
    
    **一、新兴起量信号**
    
    - 🔥 **#{题材}** — 仅{N}部但均互动{X}+,描述
    (若无新兴题材,输出:暂无新兴起量信号)
    
    **二、爆款标题特征**
    
    | 特征模式 | 出现次数 | 典型案例 | 平均互动 |
    |---------|---------|---------|---------|
    | {特征1} | {N}次 | 《{标题}》 | {X}w |
    | ... | ... | ... | ... |
    (若无标题数据,输出:暂无爆款标题数据)
    
    **三、核心达人榜**
    
    | 达人 | 作品数 | 总互动 | 代表作 |
    |------|--------|--------|--------|
    | @{达人} | {N}部 | {X}w | 《{作品}》 |
    | ... | ... | ... | ... |
    (若无达人数据,输出:暂无核心达人数据)
    
    **四、题材趋势报告**
    
    **题材**:#{题材1}
    **作品数**:{N}部
    **平均互动**:{X}w
    **头部作品**:《{标题}》-{互动}w
    
    **题材特征**:{描述该题材的共性特征}
    **创作建议**:{针对该题材的创作建议}
    
    **五、#{题材2}**
    
    (同上格式)
    
    **六、#{题材3}**
    
    (同上格式)
    
    **七、跨题材对比建议**
    
    - **{题材}** — 建议同步关注{相关题材}的联动创作,观察题材融合趋势
    (若无建议,输出:暂无跨题材对比建议)
    
    ---
    
    **日报地址**:{HTML文件绝对路径}
    
    > 数据说明:每日15:00更新昨天的数据
    ```
    
    > 以上格式为**强制规范**,所有字段不可省略,板块标题(一、二、三、四、五、六、七)必须保留。若某模块无数据则在该板块内标注"暂无",不得删除板块本身。
    
    ## 参数说明
    
    | 参数 | 说明 | 默认值 |
    |------|------|--------|
    | `--topics "关键词,..."` | 自定义题材查询,逗号分隔;全部不符合短剧题材词库时不请求接口直接提示+推荐;混合词保留有效词 | 全量查询 |
    | `--count N` | 扫描作品数量,满足即停 | `200` |
    | `--date YYYY-MM-DD` | 指定日期(超出有效范围/未更新时提示并询问,等待确认;历史日期已有数据无需确认) | 今天 |
    | `--start-time` | 自定义开始时间 YYYY-MM-DD HH:MM:SS(覆盖 --date 推算) | — |
    | `--end-time` | 自定义结束时间 YYYY-MM-DD HH:MM:SS(覆盖 --date 推算) | — |
    | `--latest` | 自动使用最新有数据的日期(用户确认后执行,不扣积分) | — |
    | `--output-dir` | 输出目录 | `~/Downloads/QoderReports` |
    | `--api-key` | 指定 API Key | — |
    | `--subscribe` | 开启每日订阅 | — |
    | `--unsubscribe` | 关闭每日订阅 | — |
    | `--from-cache` | 使用缓存数据,不扣积分 | — |
    
    ## 💬 自定义题材查询场景
    
    除默认短剧日报外,用户可指定任意题材组合进行定向查询与分析:
    
    ```bash
    # 查询穿越题材热门短剧
    python3 "$SKILL_PATH/assets/daily_report.py" --topics "穿越,时空,重生"
    
    # 查询霸总/甜宠题材
    python3 "$SKILL_PATH/assets/daily_report.py" --topics "霸总,甜宠,总裁,虐恋"
    
    # 查询悬疑/反转题材
    python3 "$SKILL_PATH/assets/daily_report.py" --topics "悬疑,推理,反转,惊悚"
    ```
    
    **自定义查询逻辑**:
    - 查询前先校验关键词/分类是否符合短剧题材词库(穿越/霸总/重生/甜宠/悬疑/逆袭/年代/战神/古装等)
    - 全部不满足时**不请求任何接口**,提醒"关键词不满足短剧查询条件"并**推荐相关分类和关键词**,停止
    - 混合词场景保留有效关键词继续查询,无效关键词自动忽略并提示
    - 查询结果自动去重,题材聚类、趋势分析均基于查询结果生成,与用户关注方向强关联
    
    ## 使用场景
    
    ### 场景一:短剧创作者选题参考
    
    **角色**:短剧编剧/制作人
    
    **需求**:了解小红书短剧市场的热门题材和爆款趋势,指导选题决策
    
    **使用方式**:
    1. 每日确认后执行 `--latest` 获取最新日报
    2. 分析题材聚类结果,关注新兴起量信号
    3. 参考爆款标题特征和题材趋势报告
    
    **预期收益**:精准把握流量风口,提升选题命中率
    
    ---
    
    ### 场景二:运营团队竞品监控
    
    **角色**:短剧运营/MCN机构
    
    **需求**:追踪竞品账号在小红书的表现,分析爆款内容特征
    
    **使用方式**:
    1. 使用 `--topics` 定向查询竞品所在题材
    2. 关注核心达人榜,识别头部竞品账号
    3. 开启 `--subscribe` 每日自动攒日报
    
    **预期收益**:及时掌握竞品动态,优化自身运营策略
    
    ---
    
    ### 场景三:内容趋势分析
    
    **角色**:内容策划/数据分析师
    
    **需求**:分析小红书短剧的内容趋势,为内容规划提供数据支撑
    
    **使用方式**:
    1. 结合 `--start-time` 和 `--end-time` 批量分析时间范围数据
    2. 利用创作趋势分析的5个维度深度挖掘规律
    3. 关注跨题材对比建议,发现题材融合机会
    
    **预期收益**:产出结构化趋势报告,支撑内容战略决策
    
    ## 项目架构
    
    ### 目录结构
    
    ```
    短剧-小红书信息源/
    ├── SKILL.md                      # Skill主文档(本文件)
    ├── README.md                     # 项目说明文档
    ├── scripts/                      # 脚本源码
    │   └── playlet_xhs_daily.py      # 日报生成脚本(开发版,与assets同步)
    ├── assets/                       # Skill运行时资源
    │   ├── daily_report.py           # 日报生成脚本(运行时使用,v2.3增强版)
    │   └── default_cover.png         # 默认封面图(加载失败时的fallback)
    └── references/                   # 参考文档
        ├── core_workflow.md          # 核心执行流程、格式模板、日期判断逻辑
        └── examples.md               # 使用示例与常见用法组合
    ```
    
    ### 技术栈
    
    | 组件 | 技术 | 说明 |
    |------|------|------|
    | 运行环境 | Python 3 | 脚本语言 |
    | 数据源 | 红狐Hub API | `X-API-KEY` 请求头鉴权,`source` 字段追踪来源 |
    | API端点 | `https://redfox.hk/story/api/parseWork/queryPlayletMsgs` | POST请求 |
    | 平台标识 | `platform=3` | 小红书(1=抖音,2=视频号,6=B站) |
    | 来源标识 | `source` | `"短剧小红书信息源-GitHub"` |
    | 数据存储 | JSON缓存 | `~/.workbuddy/cache/playlet_xhs_data.json` |
    | 输出格式 | HTML + 终端Markdown | 深色主题,小红书红(#FF2442) |
    | 输出目录 | `~/Downloads/QoderReports` | 可自定义 |
    
    ### 数据流转
    
    ```
    用户请求 → 前置输入校验(分类/关键词/日期) → 日期预检(15:00规则)
      ↓(关键词/分类全部不满足)        ↓(日期未更新/超范围)
    不请求接口,直接提示+推荐停止   提示用户并等待确认(禁止自动回退)
      ↓(用户确认后)
    API调用(批量查询+重试+去重+排序,数据不足自动扩展题材)
      ↓
    题材聚类(关键词匹配)
      ↓
    HTML日报生成 + 终端摘要输出
      ↓
    创作趋势分析(5个维度)
    ```
    
    ## 常见问答
    
    ### 安装相关问题
    
    **Q1: 安装时提示 "未找到 REDFOX_API_KEY 环境变量" 怎么办?**
    
    A: 请按以下步骤检查:
    1. 确认 API Key 已正确配置(Windows: `[Environment]::SetEnvironmentVariable("REDFOX_API_KEY", "ak_xxx", "User")`)
    2. 配置后需**重启终端**使环境变量生效
    3. 验证: `echo %REDFOX_API_KEY%` 应输出你的Key值
    
    **Q2: API Key 如何获取?**
    
    A: 前往 [红狐Hub 官网](https://redfox.hk?source=github) 注册账号,登录后在个人中心获取,格式为 `ak_xxxxxxxx`。新注册用户获赠免费积分。
    
    ---
    
    ### 使用相关问题
    
    **Q3: 数据多久更新一次?**
    
    A: 每日15:00更新前一天的数据。
    - 15:00前:最新可用日期 = 前天(T-2)
    - 15:00后:最新可用日期 = 昨天(T-1)
    
    **Q4: 查询指定日期提示"数据尚未更新"怎么办?**
    
    A: 目标日期尚未更新时,脚本**不会自动回退**,而是提示当前可查询的最新日期并询问"是否需要查询最新日期的数据?",**等待用户确认后**才执行。这是本技能保留的确认逻辑(与B站版本的自动兜底不同)。
    
    **Q5: 关键词/分类不满足短剧题材词库时怎么办?**
    
    A: 查询前脚本会**先校验关键词/分类**:全部不符合短剧题材词库(如"智能辅助驾驶""火星人"等非题材词)时,**不请求任何接口**,直接提示"关键词不满足短剧查询条件"并**推荐相关分类和关键词**(穿越/霸总/重生/甜宠/悬疑/逆袭等);混合词场景保留有效关键词继续查询。
    
    **Q6: 查询不到数据怎么办?**
    
    A: 可能原因:
    1. 目标日期尚无数据(未到15:00更新时间)
    2. 关键词在该日期无匹配作品(建议改用题材词或去掉 --topics 查询全部,需确认后执行)
    3. API Key权限不足或积分耗尽
    4. 网络连接问题
    
    建议先使用 `--from-cache` 检查缓存,或换一个日期尝试。
    
    ---
    
    ### 故障排除
    
    **Q7: Windows PowerShell 执行报 UnicodeEncodeError?**
    
    A: 脚本输出包含emoji,需设置UTF-8编码:
    ```powershell
    $env:PYTHONIOENCODING='utf-8'
    python assets/daily_report.py --latest
    ```
    
    **Q8: HTML日报中图片加载失败?**
    
    A: 脚本已内置fallback机制,加载失败时自动显示默认封面图(`assets/default_cover.png`)。部分封面图URL使用HEIF格式,脚本会自动转换为JPG格式提高兼容性。
    
    ---
    
    ### 安全与许可
    
    **Q9: 数据安全如何保障?**
    
    A:
    - API Key仅通过环境变量获取,禁止硬编码
    - 数据来源唯一:仅使用红狐Hub API,禁止自主采集
    - 缓存文件存储在本地 `~/.workbuddy/cache/`
    
    ## 📚 参考文档
    
    - [references/core_workflow.md](references/core_workflow.md) — 核心执行流程、输出格式模板、日期判断逻辑、字段映射、平台适配说明、字段映射修正记录、改造验证清单
    - [references/examples.md](references/examples.md) — 使用示例与常见用法组合
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related