douyin-subscribe
抖音账号订阅追踪 — 通过抖音号订阅账号(最多20个),Agent 每日 9:00 自动拉取并生成 HTML 报告。账号 ID 直接内置于自动化命令中,无需文件存储。支持多抖音号批量订阅,自动生成精美 HTML 作品报告,终端/Markdown 表格按账号依次展示作品标题、收藏、评论、分享、点赞、发布时间等数据。当用户订阅抖音账号、追踪抖音作品更新、监控抖音竞品账号时使用。触发词:抖音订阅、抖音账号订阅、抖音订阅追踪、抖音作品订阅、抖音账号监控、抖音作品追踪、抖音每日推送、抖音日报。
Install
npx skills add https://github.com/redfox-data/redfox-community/tree/main/skills/douyin-subscribe
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install redfox-data-redfox-community@llmmart
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
抖音账号订阅追踪 / douyin-subscribe
简介
通过抖音号订阅竞对、同类和关注账号,每天自动获取最新作品数据,生成可视化报告,轻松掌握抖音动态。
核心价值
- 一键订阅:发送抖音号即可订阅,无需复杂配置,最多支持 20 个账号
- 每日自动推送:每天早上 9:00 自动拉取订阅账号的最新作品,无需手动操作
- 可视化报告:自动生成精美 HTML 报告,支持预览和分享,数据一目了然
- 全维度数据:收藏数、评论数、分享数、点赞数、发布时间,关键指标全覆盖
适用对象
- 📊 内容运营 — 追踪竞品账号动态,及时发现爆款内容趋势
- 🎬 短视频创作者 — 监控同赛道头部账号,获取创作灵感与数据参考
- 🏢 品牌 / MCN — 批量管理关注账号,每日自动收到作品日报
功能特性
核心功能
- 抖音号直接订阅:通过抖音号订阅账号,简单透明
- 每日自动拉取:订阅后每天 9:00 自动获取最新作品数据
- HTML 可视化报告:自动生成精美报告文件,含数据总结与爆款 TOP5
- 作品超链接:标题和账号名均可点击跳转详情页
- 智能日期策略:默认查前一天,无数据自动回溯近 7 天,也支持自定义日期范围
- 多账号分组展示:按账号依次分组,同账号内按分享数降序排列
密钥获取与安全说明
- 本技能需要使用环境变量:
REDFOX_API_KEY。 REDFOX_API_KEY由 红狐 hub (https://redfox.hk)提供。- 请前往 红狐 hub 注册账号,获取
REDFOX_API_KEY。 - 配置设备环境变量
REDFOX_API_KEY后使用本技能。 - 在提供密钥前,请先确认密钥来源、可用范围、有效期及是否支持重置/撤销。
- 禁止在代码、提示词、日志或输出文件中硬编码/明文暴露密钥。
使用指南
直接用自然语言说出你想订阅的抖音号即可,无需记忆命令。
常用说法速查
| 意图 | 示例话术 | 效果 |
|---|---|---|
| 订阅账号 | 「订阅 Fish688688」 | 自动验证并订阅,展示最新作品数据 |
| 批量订阅 | 「订阅 abc123, def456, ghi789」 | 一次订阅多个账号 |
| 查看特定日期 | 「查一下 2026-06-01 到 2026-06-05 的作品」 | 按日期范围拉取历史作品 |
| 取消订阅 | 「取消订阅 Fish688688」 | 从订阅列表中移除该账号 |
输出示例
订阅成功后,你将收到:
- Markdown 作品表格:按账号分组展示,含收藏数、评论数、分享数、点赞数
- HTML 可视化报告:自动保存至
report/目录,含数据总结与爆款 TOP5 - 总结统计:突出账号表现 + 高频话题 TOP5
使用场景
| 场景 | 角色 | 示例问法 | 收益 |
|---|---|---|---|
| 竞品监控 | 内容运营 | 「订阅竞品的抖音号,每天看他们的作品」 | 每日自动获取竞品动态,不错过爆款 |
| 赛道追踪 | 短视频创作者 | 「订阅这几个头部账号,追踪他们的内容」 | 及时掌握赛道趋势,获取创作灵感 |
| 批量管理 | MCN / 品牌 | 「把这些账号都订阅上,统一管理」 | 最多 20 个账号批量订阅,日报自动推送 |
| 历史回溯 | 内容分析师 | 「查一下上周这些账号的作品数据」 | 支持自定义日期范围查询历史数据 |
Skill manifest
抖音账号订阅追踪
📝 简介
通过抖音号订阅竞对、同类和关注账号(最多 20 个),自动抓取最新作品。账号 ID 直接传入命令行参数,不依赖任何本地文件存储,自动化任务中硬编码所有已订阅的抖音号。
✨ 功能特性
| 功能模块 | 能力描述 | 核心价值 |
|---|---|---|
| 账号直传 | 通过 --accounts 参数直接传入抖音号,无需文件存储 |
简单透明,命令即文档 |
| 每日自动推送 | 自动化任务内置账号 ID,每天早上 9:00 自动执行 | 定时获取,无需手动操作 |
| HTML 报告 | fetch 时自动生成精美 HTML 报告文件,支持预览和分享 | 可视化浏览,一目了然 |
| 作品数据 | 收藏数、评论数、分享数、点赞数、发布时间 | 全维度数据,一目了然 |
| 按账号展示 | 终端表格按账号依次分组展示 | 清晰直观,便于对比 |
| 日期回溯 | 支持指定历史日期查询 | 追溯往期作品数据 |
🔑 鉴权
获取 API Key
请前往 红狐hub 获取API KEY
配置 API Key
方案1: 以OpenClaw为例,将REDFOX_API_KEY添加到~/.openclaw/openclaw.json中:
{ "env": { "REDFOX_API_KEY": "ak_xxxx..." } }
方案2: 终端配置
export REDFOX_API_KEY="ak_xxxx..."
⚙️ 使用方式
拉取作品(核心命令)
# 基础用法:直接传入抖音号列表
python3 "$SKILL_PATH/scripts/subscribe.py" fetch --accounts "YuZhouXiaoLi1220,Fish688688"
# 生成 HTML 报告(自动保存到 report/ 目录)
python3 "$SKILL_PATH/scripts/subscribe.py" fetch --accounts "YuZhouXiaoLi1220,Fish688688" --html
# 指定 HTML 报告输出路径
python3 "$SKILL_PATH/scripts/subscribe.py" fetch --accounts "YuZhouXiaoLi1220,Fish688688" --html --html-path ~/Desktop/report.html
# Markdown 格式输出(供 Agent 展示)
python3 "$SKILL_PATH/scripts/subscribe.py" fetch --accounts "YuZhouXiaoLi1220,Fish688688" --markdown
# 指定单日日期
python3 "$SKILL_PATH/scripts/subscribe.py" fetch --accounts "YuZhouXiaoLi1220,Fish688688" --html --date 2026-06-01
# 指定日期范围
python3 "$SKILL_PATH/scripts/subscribe.py" fetch --accounts "YuZhouXiaoLi1220,Fish688688" --html --date-start 2026-06-01 --date-end 2026-06-05
参数说明
| 命令 | 参数 | 说明 |
|---|---|---|
fetch |
--accounts |
抖音号列表(必填或核心参数),逗号分隔,如 "id1,id2,id3" |
--date |
指定单日日期 YYYY-MM-DD | |
--date-start |
指定日期范围开始 YYYY-MM-DD | |
--date-end |
指定日期范围结束 YYYY-MM-DD | |
--markdown |
输出 Markdown 格式表格 | |
--html |
生成 HTML 报告文件(保存到 report/ 目录) | |
--html-path |
自定义 HTML 报告输出路径 | |
| 全局 | --api-key |
指定 API Key |
依赖
| 依赖 | 安装命令 |
|---|---|
requests |
pip3 install requests |
🤖 每日自动推送
用户订阅账号后,Agent 会创建一个每日定时任务,每天早上 9:00 自动执行。任务命令中直接包含所有已订阅的抖音号 ID:
# 自动化任务中的实际命令(账号已内置)
python3 subscribe.py fetch --accounts "YuZhouXiaoLi1220,Fish688688" --html
工作原理
- 用户发送抖音号 → Agent 将其写入自动化命令的
--accounts参数 - 每天 9:00 定时触发 → 命令自带账号 ID → 直接调 API 拉数据
- 自动生成 HTML 报告 → 保存至
report/目录 - 不依赖任何本地 JSON 文件
🔧 账号管理规则(Agent 必须遵守)
核心原则:所有订阅状态 = 自动化命令中的
--accounts参数。无文件存储。
订阅新账号流程
当用户提供新的抖音号时,Agent 必须按以下步骤操作:
Step 1:查询现有自动化任务
使用 automation_update mode=list 获取所有「抖音订阅」相关的自动化任务,读取每个任务的 --accounts 参数中的账号列表。
Step 2:判断是否追加 / 新建
| 条件 | 操作 |
|---|---|
| 现有账号数 + 新账号数 ≤ 20 | 追加到现有自动化任务的 --accounts 参数(去重后用逗号拼接) |
| 现有账号数 + 新账号数 > 20 | 创建新的自动化任务,将超出部分放入新任务的 --accounts |
| 用户明确要求取消某账号 | 从对应自动化任务的 --accounts 中移除该 ID |
Step 3:更新/创建自动化任务
- 追加场景:调用
automation_update mode=update,修改现有任务的prompt字段中的--accounts值 - 新建场景:调用
automation_update mode=create,新建一个独立的每日定时任务(同样 9:00 执行),名称可加序号如「抖音订阅作品日报 #2」
Step 4:立即拉取一次
无论追加还是新建,都必须立即执行一次 fetch(带 --html)展示最新结果给用户。
多任务示例
假设已有 18 个账号,用户又提供了 5 个新账号:
现有任务 #1: --accounts "id1,id2,...,id18" (18个)
新增 5 个: id19,id20,id21,id22,id23
→ 任务 #1 更新为: --accounts "id1,...,id18,id19,id20" (20个 ✅)
→ 新建任务 #2: --accounts "id21,id22,id23" (3个 ✅)
每个独立任务都会在每天 9:00 各自执行,各自生成 HTML 报告。
取消订阅
从对应任务的 --accounts 中移除该 ID 即可。如果移除后某个任务剩余 0 个账号,则删除该自动化任务。
📋 交互规范
订阅流程
静默执行原则:所有中间步骤(fetch 验证、add 订阅、automation_update)均不得向用户输出任何过程性提示(如「正在验证账号」「账号验证通过」「自动订阅中」等)。仅向用户展示最终结果。
- 用户发送抖音号 → Agent 静默执行
fetch --accounts "抖音号" --html --markdown验证账号 - 判断账号状态(规则 1):
- 接口返回正常数据:
- 静默执行 add 订阅(见下一步)
- 近 30 天无数据,自动回溯半年后仍无数据:
- 告知用户:「抱歉您订阅的“xxx”账号近半年都未发布过作品,如果需要数据核查可联系工作人员邮箱 redfoxdata@proton.me 处理」
- 询问用户:「是否订阅明日推送?」
- 用户回答「订阅」→ 静默执行 add 订阅
- 用户未确认 → 不订阅
- 接口返回正常数据:
- 静默查询现有自动化任务的
--accounts列表,判断追加 or 新建:- 现有账号数 + 新账号数 ≤ 20 → 追加到现有任务的
--accounts - 现有账号数 + 新账号数 > 20 → 新建一个自动化任务放溢出部分
- 现有账号数 + 新账号数 ≤ 20 → 追加到现有任务的
- 静默更新/创建自动化任务
- 若 fetch 返回了作品数据:
- 自动打开 HTML 报告预览(
preview_url) - 对话中展示 Markdown 作品表格
- 提示:「如需查看特定时间段的作品,可以告诉我,如“查一下 2026-06-01 到 2026-06-05 的作品”」
- 自动打开 HTML 报告预览(
- 若 fetch 无作品数据(账号存在但时段内无更新):仅告知用户无更新,不生成 HTML 报告
- 输出顺序:先按账号名升序分组,同账号内按分享数降序排列
- 若某个账号在指定时间范围内无更新作品,直接告知用户:「账号名:该时间段内无更新作品」
重要:仅当 fetch 返回实际作品数据时,才需要三输出——Markdown 表格 + HTML 报告预览 + HTML 报告文件。无作品数据时不生成 HTML 报告。
对话输出格式(订阅/拉取时必须遵循)
Agent 执行 fetch 后,必须在对话回复中按以下格式输出数据(不是只依赖脚本终端输出):
格式一:账号概览表
## 📊 订阅报告
| 项目 | 值 |
|------|-----|
| 新订阅 | `新账号名`(粉丝量) |
| 当前总账号 | N 个 |
| 本次拉取作品 | N 条 |
| 自动化任务 | 已更新 ✅ 每天 9:00 自动执行 |
### 账号概览
| 账号 | 粉丝量 | 领域 | 代表作数据 |
|------|--------|------|-----------|
| AccountA | xxx | 领域 | 🏆 代表作:收藏x / 点赞x / 分享x |
格式二:每个账号的详细作品表(Markdown)
### ▸ AccountName(粉丝: xxx)
| 作品标题 | 收藏数 | 评论数 | 分享数 | 点赞数 | 发布时间 |
|---------|--------|--------|--------|--------|---------|
| 标题1 | 10.8w | 2.8k | 10.3w | 19.3w | 06-08 |
要求:
- 每个账号一个子标题 + 一个完整 Markdown 表格
- 标题列使用 API 返回的
content字段完整原文(脚本内部映射为title),不截断 - 数值列使用
format_number后的可读格式(如 10.8w、2.3k) - 发布时间截取到
MM-DD HH:mm - 按分享数降序排列
格式三:总结统计(必须附加在所有表格之后)
### 对上述作品的总结
> 1. **突出的账号表现**:...
> 2. **高频事件 TOP5**:...
输出完整性清单
每次订阅/拉取,对话中必须包含以下全部内容:
- 账号概览表(含总览信息)
- 每个账号一个 Markdown 作品详情表
- 无更新账号提示(如有)
- 总结统计分析
- 规则 2 适用:已订阅账号昨日无数据时,结尾附加邮箱核查提示
- 规则 3 适用:被频率限制拒绝的账号提示
- 仅当有作品数据时:自动打开 HTML 报告预览(preview_url)+ HTML 报告文件已生成说明(附文件路径)
抖音号要求
接口通过 uniqueName(抖音展示ID)查询,脚本将用户提供的抖音号统一按 uniqueName 传入,userId 和 shortId 固定为空字符串。订阅时必须提供抖音号。
重要: 若用户只输入账号名称(如"李佳琦""老高與小茉")而未提供抖音号,必须提示:
"抖音账号名称存在多个重名情况,请提供准确的抖音号以便精准订阅。"
并附上获取抖音号的示例图供用户参考:

数据规则
- 每个自动化任务最多 20 个账号(超限时 Agent 自动创建新任务,详见「账号管理规则」)
- 规则 1:未订阅账号(首次验证查询):
- 默认查询近 30 天数据
- 若近 30 天无数据,自动回溯近半年(180 天)
- 若近半年仍无数据,告知用户:「抱歉您订阅的“xxx”账号近半年都未发布过作品,如果需要数据核查可联系工作人员邮箱 redfoxdata@proton.me 处理」
- 规则 2:已订阅账号(日常拉取):
- 默认查前一天数据(T-1)
- 若昨日无数据,告知用户:「昨日该账号暂未发布作品」
- 结尾固定提示:「如果需要数据核查可联系工作人员邮箱 redfoxdata@proton.me 处理」
- 规则 3:失败阈值保护:
- 同一抖音号在 6 小时内累计 3 次 API 调用失败后,后续请求将被拒绝
- 拒绝时提示:「当前账号订阅已超过失败阈值,请联系客服邮箱 redfoxdata@proton.me 处理」
- 距上次失败超过 6 小时 / 同一账号查询成功,计数归零,恢复正常调用
- 用户指定日期不回溯:若用户明确指定了日期或日期范围,查不到数据时直接告知用户,不自动回溯
- 支持日期范围查询:用户可指定起止日期(
--date-start YYYY-MM-DD --date-end YYYY-MM-DD)拉取历史作品 - 每账号最多返回 10 条作品
- 排序方式:账号名升序 → 同账号内分享数降序
- 无数据提示:若某个账号在指定时间范围内无更新作品,脚本会输出「账号名:该时间段内无更新作品」,Agent 需将此信息原样转达给用户
- 接口不支持多账号同时查询,脚本自动拆分后逐账号调用并整合数据
输出表格结构(脚本终端格式,仅供参考)
脚本终端输出原始格式如下,但 Agent 对话中必须使用「对话输出格式」中的 Markdown 表格重新排版:
▸ Fish688688(粉丝: 249w+)
──────────────────────────────────────────────────────────────
作品标题 收藏数 评论数 分享数 点赞数 发布时间
──────────────────────────────────────────────────────────────
今晚图省事... 10.8w 2.8k 10.3w 19.3w 05-21 21:40
作品标题使用 API content 字段(脚本内部映射为 title),链接使用 opusUrl 字段(脚本内部映射为 workUrl)。Markdown 输出中标题自动渲染为可点击超链接:[标题](workUrl)。
注意:当前广域库 API 不返回
followerCount(粉丝数),账号概览中粉丝量显示为-。
总结统计
表格展示完毕后,必须严格按以下格式附加总结:
对上述作品的总结:
- 突出的账号表现:指出数据最亮眼的账号及其代表作品
- 高频事件 TOP5:提炼所有作品中提到次数最多的 5 个事件/话题,并归纳每个事件对应的不同观点(不足 5 个可提炼的,按实际数量呈现,禁止虚构)
Files (redfox-community)
-
references
-
test_api_params.json 1.1 KB
{ "test_cases": [ { "name": "罗永浩(uniqueName)", "payload": { "uniqueName": "luoyonghao", "pageNum": 1, "pageSize": 10 } }, { "name": "罗永浩(userId)", "payload": { "userId": "3822358551859599", "pageNum": 1, "pageSize": 10 } }, { "name": "纯数字抖音号(userId模式)", "payload": { "userId": "3822358551859599", "pageNum": 1, "pageSize": 10 } } ], "api": { "url": "https://redfox.hk/story/api/dy/data/listWorkByAccount", "method": "POST", "headers": { "REDFOX_API_KEY": "ak_xxx", "Content-Type": "application/json" } }, "field_mapping": { "旧→新": { "title": "content", "workUrl": "opusUrl", "accountName": "authorName", "secUid": "authorSecUid", "followerCount": "(缺失)" }, "新API新增字段": [ "videoId", "videoType", "duration", "coverUrl", "tagList", "imageUrlList", "authorUid", "authorShortId", "authorUniqueId", "authorAvatarUrl" ] } }
-
-
scripts
-
subscribe.py 54.4 KB
#!/usr/bin/env python3 """ 抖音账号订阅追踪 — 订阅抖音账号,每日自动推送最新作品 ======================================================== 订阅关注的抖音账号,自动抓取最新作品数据,表格化展示。 Usage: python3 subscribe.py add "5437662" --name "李佳琦" --category "竞对账号" python3 subscribe.py add "5437662,13232311,3234242" python3 subscribe.py remove "5437662" python3 subscribe.py list python3 subscribe.py fetch python3 subscribe.py fetch --date 2026-06-01 """ import argparse import json import os import re import sys import time from collections import Counter, defaultdict from datetime import datetime, timedelta from pathlib import Path try: import requests HAS_REQUESTS = True except ImportError: HAS_REQUESTS = False # ─── 配置 ───────────────────────────────────────────────────────────────────────── API_URL = "https://redfox.hk/story/api/dy/data/listWorkByAccount" ENV_KEY = "REDFOX_API_KEY" SUBSCRIPTIONS_FILE = Path.home() / ".qoder" / "douyin_subscriptions.json" MAX_SUBSCRIPTIONS = 20 # 失败计数文件(规则3:同一参数 6h 内 3 次失败后拒绝调用) FAILURES_FILE = Path.home() / ".qoder" / "douyin_subscribe_failures.json" SUPPORT_EMAIL = "redfoxdata@proton.me" RATE_LIMIT_MAX_FAILURES = 3 RATE_LIMIT_WINDOW_HOURS = 6 DEFAULT_CATEGORIES = ["竞对账号", "同类账号", "关注账号"] # ─── 终端颜色 ────────────────────────────────────────────────────────────────────── GREEN = "\033[92m" YELLOW = "\033[93m" RED = "\033[91m" CYAN = "\033[96m" BOLD = "\033[1m" RESET = "\033[0m" def info(msg): print(f"{GREEN}[✓]{RESET} {msg}") def warn(msg): print(f"{YELLOW}[!]{RESET} {msg}") def error(msg): print(f"{RED}[✗]{RESET} {msg}") def step(msg): print(f"{CYAN}[→]{RESET} {msg}") # ─── API Key 管理 ────────────────────────────────────────────────────────────────── def get_api_key(cli_key=None): """Get API key: CLI arg > env var. 必须配置,否则报错退出。""" if cli_key: return cli_key env_key = os.environ.get(ENV_KEY) if env_key: return env_key error("未找到 REDFOX_API_KEY,请先配置 API Key:") print(f" {CYAN}方案1(推荐):{RESET}export REDFOX_API_KEY=ak_你的密钥") print(f" {CYAN}方案2:{RESET}命令行参数 --api-key ak_你的密钥") print(f" {CYAN}获取地址:{RESET}https://redfox.hk/settings/api-keys?source=github") sys.exit(1) # ─── 订阅数据管理 ────────────────────────────────────────────────────────────────── def load_subscriptions(): """加载订阅列表""" if not SUBSCRIPTIONS_FILE.exists(): return [] try: data = json.loads(SUBSCRIPTIONS_FILE.read_text(encoding="utf-8")) return data.get("subscriptions", []) except (json.JSONDecodeError, OSError): return [] def save_subscriptions(subscriptions): """保存订阅列表""" SUBSCRIPTIONS_FILE.parent.mkdir(parents=True, exist_ok=True) data = {"subscriptions": subscriptions, "updatedAt": datetime.now().isoformat()} SUBSCRIPTIONS_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") def find_subscription(identifier): """查找某个订阅 — 按 accountId 查找""" subs = load_subscriptions() for sub in subs: if sub["accountId"] == identifier: return sub return None def add_subscriptions(account_ids, account_name="", category="关注账号"): """添加订阅 — 支持逗号分隔的多个抖音号,拆分后逐个添加""" ids = [aid.strip() for aid in account_ids.split(",") if aid.strip()] if not ids: error("请提供至少一个有效的抖音号") return False subs = load_subscriptions() added_count = 0 for aid in ids: if len(subs) >= MAX_SUBSCRIPTIONS: error(f"已达订阅上限({MAX_SUBSCRIPTIONS} 个),请先取消一些订阅后再添加") break existing = find_subscription(aid) if existing: warn(f"已订阅过抖音号「{aid}」({existing.get('accountName', '')}),跳过") continue subs.append({ "accountId": aid, "accountName": account_name or aid, "category": category, "subscribedAt": datetime.now().isoformat(), }) added_count += 1 info(f"已订阅抖音号「{aid}」{f'— {account_name}' if account_name else ''} — 分类: {category}") if added_count > 0: save_subscriptions(subs) info(f"本次新增 {added_count} 个订阅,当前共 {len(subs)}/{MAX_SUBSCRIPTIONS} 个") else: warn("本次未新增任何订阅") return added_count > 0 def remove_subscription(identifier): """移除订阅 — 按 accountId 移除""" subs = load_subscriptions() target = None for i, sub in enumerate(subs): if sub["accountId"] == identifier: target = subs.pop(i) break if not target: warn(f"未找到抖音号「{identifier}」的订阅") return False save_subscriptions(subs) name = target.get("accountName", "") name_info = f"({name})" if name and name != identifier else "" info(f"已取消订阅「{identifier}」{name_info}") info(f"当前共订阅 {len(subs)}/{MAX_SUBSCRIPTIONS} 个抖音账号") return True def list_subscriptions(): """列出所有订阅""" subs = load_subscriptions() if not subs: warn("当前没有订阅任何抖音账号") print(f"\n 使用 '{CYAN}add{RESET}' 命令添加订阅:") print(f" python3 subscribe.py add \"<抖音号>\" --name \"<账号名>\"") print(f" python3 subscribe.py add \"<抖音号1>,<抖音号2>\" # 支持多个") return groups = defaultdict(list) for sub in subs: groups[sub.get("category", "关注账号")].append(sub) print(f"\n{BOLD}当前订阅 ({len(subs)}/{MAX_SUBSCRIPTIONS}):{RESET}\n") for cat in DEFAULT_CATEGORIES + ["其他"]: items = groups.pop(cat, []) if not items: if cat in DEFAULT_CATEGORIES: groups.pop(cat, None) continue cat_color = {"竞对账号": RED, "同类账号": YELLOW, "关注账号": CYAN}.get(cat, CYAN) print(f" {cat_color}{BOLD}▸ {cat}{RESET}") for item in items: subscribed_at = item.get("subscribedAt", "")[:10] if item.get("subscribedAt") else "" display_line = f" {item['accountName']} (抖音号: {item['accountId']})" if subscribed_at: display_line += f" 订阅于 {subscribed_at}" print(display_line) for cat, items in groups.items(): if not items: continue print(f" {CYAN}▸ {cat}{RESET}") for item in items: print(f" {item['accountName']} (抖音号: {item['accountId']})") print() # ─── 数据获取 ────────────────────────────────────────────────────────────────────── # ─── 失败计数 / 频率限制(规则 3)─────────────────────────────────────────────── def _load_failures(): """加载失败记录""" if FAILURES_FILE.exists(): try: return json.loads(FAILURES_FILE.read_text(encoding="utf-8")) except Exception: pass return {} def _save_failures(failures): """保存失败记录""" FAILURES_FILE.parent.mkdir(parents=True, exist_ok=True) FAILURES_FILE.write_text(json.dumps(failures, ensure_ascii=False, indent=2), encoding="utf-8") def _check_rate_limit(account_id): """检查是否超过失败阈值(6h 内 3 次失败),返回 (blocked: bool, message: str)""" failures = _load_failures() key = account_id.strip().lower() record = failures.get(key) if not record: return False, "" last_fail_time = record.get("lastFailTime", 0) fail_count = record.get("count", 0) # 距上次失败超过 6 小时,计数归零 if time.time() - last_fail_time > RATE_LIMIT_WINDOW_HOURS * 3600: del failures[key] _save_failures(failures) return False, "" # 6h 内失败 >= 3 次,拒绝调用 if fail_count >= RATE_LIMIT_MAX_FAILURES: return True, f"当前账号「{account_id}」订阅已超过失败阈值,请联系客服邮箱 {SUPPORT_EMAIL} 处理" return False, "" def _record_failure(account_id): """记录一次失败""" failures = _load_failures() key = account_id.strip().lower() record = failures.get(key, {"count": 0, "lastFailTime": 0}) # 距上次失败超过 6 小时,重置计数 if time.time() - record.get("lastFailTime", 0) > RATE_LIMIT_WINDOW_HOURS * 3600: record = {"count": 0, "lastFailTime": 0} record["count"] += 1 record["lastFailTime"] = time.time() failures[key] = record _save_failures(failures) def _record_success(account_id): """成功后计数归零""" failures = _load_failures() key = account_id.strip().lower() if key in failures: del failures[key] _save_failures(failures) def _filter_works_by_date(works, date_str=None, date_start=None, date_end=None): """客户端按发布时间过滤作品(新 API 不支持服务端日期过滤)""" if not date_str and not (date_start and date_end): return works filtered = [] for w in works: pub = w.get("publishTime") or "" if not pub: continue pub_date = pub[:10] # YYYY-MM-DD if date_start and date_end: if date_start <= pub_date <= date_end: filtered.append(w) elif date_str: if pub_date == date_str: filtered.append(w) return filtered def _map_work_fields(work): """将新 API 字段映射为脚本内部统一字段名,保持下游逻辑不变""" work["title"] = work.get("content") or work.get("title") or "" work["workUrl"] = work.get("opusUrl") or work.get("workUrl") or "" work["accountName"] = work.get("authorName") or work.get("accountName") or "" work["secUid"] = work.get("authorSecUid") or work.get("secUid") or "" work["followerCount"] = work.get("authorFansCount") or work.get("followerCount") return work def fetch_account_works(session, account_id, date_str=None, date_start=None, date_end=None): """获取单个抖音账号的作品列表 — 通过广域库 API(listWorkByAccount)""" if not account_id: warn("无效的抖音号: 空") return [] # 规则3:检查频率限制 blocked, block_msg = _check_rate_limit(account_id) if blocked: warn(block_msg) return "__rate_limited__" payload = { "uniqueName": account_id, "userId": "", "shortId": "", "pageNum": 1, "pageSize": 10, "source": "抖音账号订阅追踪" } try: resp = session.post(API_URL, json=payload, timeout=20) result = resp.json() except requests.exceptions.Timeout: warn(f"请求超时: 抖音号 {account_id}") _record_failure(account_id) return [] except Exception as e: warn(f"请求失败: 抖音号 {account_id}: {e}") _record_failure(account_id) return [] code = result.get("code") if code == 3108: warn("触发频率限制,等待 5s...") time.sleep(5) try: resp = session.post(API_URL, json=payload, timeout=20) result = resp.json() code = result.get("code") except Exception: _record_failure(account_id) return [] if code not in (200, 2000): if code in (3106, 3107): error(f"API Key 错误 (code {code}): {result.get('msg', '')}") elif code: warn(f"API 返回错误 (code {code}): {result.get('msg', '')} — 抖音号 {account_id}") _record_failure(account_id) return [] # 规则3:查询成功,计数归零 _record_success(account_id) data_raw = result.get("data", {}) if not data_raw: return [] if isinstance(data_raw, list): works = data_raw elif isinstance(data_raw, dict): works = data_raw.get("list") or data_raw.get("articles") or data_raw.get("records") or [] else: works = [] # 映射新 API 字段为统一字段名 for work in works: _map_work_fields(work) # 客户端日期过滤(新 API 不支持服务端日期过滤) if date_str or (date_start and date_end): works = _filter_works_by_date(works, date_str, date_start, date_end) for work in works[:10]: work["_accountId"] = account_id return works[:10] def fetch_all_works(session, subscriptions, date_str=None, date_start=None, date_end=None): """拉取所有订阅账号的作品 — 返回 (works, empty_accounts, rate_limited_accounts)""" all_works = [] empty_accounts = [] rate_limited_accounts = [] total = len(subscriptions) for i, sub in enumerate(subscriptions, 1): aid = sub.get("accountId", "") name = sub.get("accountName", aid) if not aid: warn(f"跳过无抖音号的订阅: {name}") continue print(f"\r {CYAN}[→]{RESET} 拉取: {name} ({aid}) ({i}/{total})", end="", flush=True) works = fetch_account_works(session, aid, date_str, date_start, date_end) if works == "__rate_limited__": # 规则3:被频率限制拒绝 rate_limited_accounts.append(name) elif works: # 优先使用 API 返回的真实 accountName(而非传入的 ID) real_name = works[0].get("accountName") or name for work in works: work["_accountName"] = real_name work["_category"] = sub.get("category", "关注账号") all_works.extend(works) else: empty_accounts.append(name) if i < total: time.sleep(0.3) print() return all_works, empty_accounts, rate_limited_accounts # ─── 数字格式化 ──────────────────────────────────────────────────────────────────── def format_number(n): """格式化数字: 1234 -> 1.2k, 12345 -> 1.2w""" if n is None: return "0" try: n = int(n) except (ValueError, TypeError): return str(n) if n >= 10000: return f"{n / 10000:.1f}w" if n >= 1000: return f"{n / 1000:.1f}k" return str(n) def format_fans(n): """格式化粉丝数""" if n is None: return "-" try: n = int(n) except (ValueError, TypeError): return str(n) if n >= 10000: return f"{n / 10000:.0f}w+" return str(n) # ─── CJK 宽度工具 ───────────────────────────────────────────────────────────────── def _display_width(text): """计算字符串在终端中的显示宽度(CJK 字符占 2 格)""" width = 0 for ch in str(text): cp = ord(ch) if (0x4E00 <= cp <= 0x9FFF or 0x3400 <= cp <= 0x4DBF or 0x20000 <= cp <= 0x2A6DF or 0x2A700 <= cp <= 0x2B73F or 0x2B740 <= cp <= 0x2B81F or 0x2B820 <= cp <= 0x2CEAF or 0xF900 <= cp <= 0xFAFF or 0x2F800 <= cp <= 0x2FA1F or 0x3000 <= cp <= 0x303F or 0xFF01 <= cp <= 0xFF60 or 0xFFE0 <= cp <= 0xFFE6): width += 2 else: width += 1 return width def _pad(text, width): """按显示宽度右填充空格""" text = str(text) return text + ' ' * max(0, width - _display_width(text)) def _rpad(text, width): """按显示宽度左填充空格(右对齐)""" text = str(text) return ' ' * max(0, width - _display_width(text)) + text def _truncate(text, max_width): """按显示宽度截断,超出加 ..""" text = str(text) if _display_width(text) <= max_width: return text result = '' w = 0 for ch in text: cw = 2 if ord(ch) > 127 else 1 if w + cw + 2 > max_width: break result += ch w += cw return result + '..' # ─── 终端表格展示 ────────────────────────────────────────────────────────────────── def print_terminal_table(works): """在终端打印作品表格 — 按账号依次展示""" if not works: warn("没有获取到任何作品") return # 按分类 → 账号分组 cat_groups = defaultdict(list) for work in works: cat = work.get("_category", "关注账号") if cat not in DEFAULT_CATEGORIES: cat = "关注账号" cat_groups[cat].append(work) cat_colors = {"竞对账号": RED, "同类账号": YELLOW, "关注账号": CYAN} cat_icons = {"竞对账号": "⚔", "同类账号": "◎", "关注账号": "★"} # 列宽定义(终端显示宽度) W_TITLE = 36 W_NUM = 8 W_TIME = 16 SEP_WIDTH = W_TITLE + W_NUM * 4 + W_TIME SEP = '─' * SEP_WIDTH total_shown = 0 for cat in DEFAULT_CATEGORIES: arts = cat_groups.get(cat, []) if not arts: continue # 按账号分组 account_groups = defaultdict(list) for art in arts: account_key = art.get("_accountName") or art.get("authorName") or art.get("_accountId", "未知") account_groups[account_key].append(art) cat_color = cat_colors.get(cat, CYAN) cat_icon = cat_icons.get(cat, "●") print(f"\n {cat_color}{BOLD}{cat_icon} {cat}{RESET} — {len(arts)} 条作品") for account_name, account_works in sorted(account_groups.items(), key=lambda x: x[0]): # 按分享数降序 account_works.sort(key=lambda a: -(int(a.get("shareCount", 0) or 0))) fans = format_fans(account_works[0].get("followerCount")) print(f"\n {BOLD}▸ {account_name}(粉丝: {fans}){RESET}") print(f" {YELLOW}{SEP}{RESET}") header = (f" {_pad('作品标题', W_TITLE)}" f"{_rpad('收藏数', W_NUM)}{_rpad('评论数', W_NUM)}" f"{_rpad('分享数', W_NUM)}{_rpad('点赞数', W_NUM)}" f" {_pad('发布时间', W_TIME)}") print(f" {YELLOW}{header}{RESET}") print(f" {YELLOW}{SEP}{RESET}") for work in account_works: title = work.get("title") or "无标题" title_d = _truncate(title, W_TITLE) collects = format_number(work.get("collectCount")) comments = format_number(work.get("commentCount")) shares = format_number(work.get("shareCount")) likes = format_number(work.get("likeCount")) pub_time = work.get("publishTime") or "-" if len(pub_time) > 16: pub_time = pub_time[:16] pub_short = pub_time[5:16] if len(pub_time) >= 16 else pub_time row = (f" {_pad(title_d, W_TITLE)}" f"{_rpad(collects, W_NUM)}{_rpad(comments, W_NUM)}" f"{_rpad(shares, W_NUM)}{_rpad(likes, W_NUM)}" f" {_pad(pub_short, W_TIME)}") print(row) total_shown += 1 remaining = len(works) - total_shown if remaining > 0: print(f"\n {YELLOW}... 还有 {remaining} 条作品未展示{RESET}") print() # ─── Markdown 表格输出 ───────────────────────────────────────────────────────────── # ─── 数据总结生成 ──────────────────────────────────────────────────────────────── # 常见中文停用词(用于话题提取时过滤) _STOP_WORDS_CN = set([ "这是", "不是", "一个", "没有", "可以", "这个", "那个", "什么", "怎么", "为什么", "一样", "大家", "你们", "他们", "我们", "自己", "今天", "明天", "昨天", "已经", "还是", "就是", "如果", "因为", "所以", "但是", "虽然", "不过", "一下", "一点", "很多", "非常", "比较", "真的", "现在", "不要", "也是", "只是", "这种", "那种", "这些", "那些", "一定", "这么", "那么", "吗", "呢", "吧", "啊", "哦", "嗯", "呀", "哈", "@", "#", " ", ",", "。", "!", "?", "、", ":", ";", "\n", "\r", "...", "..", "~", "~", "|", "|", "/", "\\", "(", ")", "(", ")", "🔥", "😭", "✨", "🤜", "🤛", "👍", "❤", "💪", ]) # 常见无意义后缀(话题中的垃圾词) _TOPIC_NOISE = set([ "四大名著", "青年创作者成长计划", "抖音商城", "抖音美食", "抖音", "西游记", "水浒传", ]) def _extract_phrases(title, min_len=2, max_len=6): """从标题中提取候选中文短语(连续中文字符序列)""" phrases = [] buf = "" for ch in title: if '\u4e00' <= ch <= '\u9fff': buf += ch else: if len(buf) >= min_len: for l in range(min_len, min(max_len + 1, len(buf) + 1)): for i in range(len(buf) - l + 1): sub = buf[i:i + l] if sub not in _STOP_WORDS_CN and sub not in _TOPIC_NOISE: phrases.append(sub) buf = "" if len(buf) >= min_len: for l in range(min_len, min(max_len + 1, len(buf) + 1)): for i in range(len(buf) - l + 1): sub = buf[i:i + l] if sub not in _STOP_WORDS_CN and sub not in _TOPIC_NOISE: phrases.append(sub) return phrases def _group_topics(phrase_counter): """合并语义相近的话题:长短语优先,去重子串""" # 按频率降序,同频时长短语优先 items = sorted(phrase_counter.items(), key=lambda x: (-x[1], -len(x[0]))) result = [] for phrase, count in items: if count < 2: continue # 检查是否为已采纳短语的子串(长短语优先) is_sub = False for accepted, _ in result: if phrase in accepted: is_sub = True break if is_sub: continue # 检查是否为已采纳短语的父串(替换短短语) expanded = False for i, (accepted, ac_count) in enumerate(result): if accepted in phrase: result[i] = (phrase, count + ac_count) expanded = True break if expanded: continue result.append((phrase, count)) if len(result) >= 5: break return result def generate_summary(works): """根据作品数据生成 HTML + 纯文本双版本总结""" if not works: return "", "" # 1. 按账号聚合统计 account_stats = defaultdict(lambda: {"total_likes": 0, "total_shares": 0, "total_collects": 0, "count": 0, "best_title": "", "best_likes": 0}) for w in works: name = w.get("_accountName") or "未知" likes = int(w.get("likeCount", 0) or 0) shares = int(w.get("shareCount", 0) or 0) collects = int(w.get("collectCount", 0) or 0) s = account_stats[name] s["total_likes"] += likes s["total_shares"] += shares s["total_collects"] += collects s["count"] += 1 if likes > s["best_likes"]: s["best_likes"] = likes s["best_title"] = (w.get("title") or "无标题")[:40] # 2. 突出的账号表现(按总点赞数降序 TOP3) top_accounts = sorted(account_stats.items(), key=lambda x: -x[1]["total_likes"])[:3] # 3. 爆款作品 TOP5(按综合互动量降序) for w in works: w["_engagement"] = (int(w.get("likeCount", 0) or 0) + int(w.get("shareCount", 0) or 0) * 3 + int(w.get("collectCount", 0) or 0) * 2 + int(w.get("commentCount", 0) or 0) * 4) top_works = sorted(works, key=lambda x: -x["_engagement"])[:5] # 4. 高频话题提取 phrase_counter = Counter() for w in works: title = w.get("title", "") for phrase in _extract_phrases(title): phrase_counter[phrase] += 1 top_topics = _group_topics(phrase_counter)[:5] # ── 生成 HTML 版本 ── html = '<div class="summary-block">\n' html += ' <h3>📈 对上述作品的总结</h3>\n' # 突出的账号表现 html += ' <div class="highlight"><strong>突出的账号表现:</strong></div>\n' html += ' <ul class="top-list">\n' for i, (name, s) in enumerate(top_accounts, 1): html += f' <li><span><span class="rank">#{i}</span>{name} · 代表作「{s["best_title"]}」</span><span class="metric">点赞 {format_number(s["best_likes"])} · 总分享 {format_number(s["total_shares"])} · {s["count"]}条作品</span></li>\n' html += ' </ul>\n' # 爆款作品 TOP5 html += ' <div class="highlight"><strong>爆款作品 TOP5(综合互动量):</strong></div>\n' html += ' <ul class="top-list">\n' for i, w in enumerate(top_works, 1): title = (w.get("title") or "无标题")[:35] account = w.get("_accountName") or "未知" likes = format_number(w.get("likeCount")) shares = format_number(w.get("shareCount")) html += f' <li><span><span class="rank">#{i}</span>{title} <small style="color:#999">— {account}</small></span><span class="metric">点赞 {likes} · 分享 {shares}</span></li>\n' html += ' </ul>\n' # 高频话题 TOP5 if top_topics: html += ' <div class="highlight"><strong>高频话题 TOP5:</strong></div>\n' html += ' <ul class="top-list">\n' for i, (topic, count) in enumerate(top_topics, 1): html += f' <li><span><span class="rank">#{i}</span>{topic}</span><span class="metric">出现 {count} 次</span></li>\n' html += ' </ul>\n' html += '</div>\n' # ── 生成纯文本版本(Markdown 格式,供对话展示)── text = "" text += "\n> 1. **突出的账号表现**:\n" for i, (name, s) in enumerate(top_accounts, 1): text += f"> - #{i} **{name}** · 代表作「{s['best_title']}」- 点赞 {format_number(s['best_likes'])} · 总分享 {format_number(s['total_shares'])} · {s['count']}条作品\n" text += ">\n> 2. **爆款作品 TOP5(综合互动量)**:\n" for i, w in enumerate(top_works, 1): title = (w.get("title") or "无标题")[:35] account = w.get("_accountName") or "未知" likes = format_number(w.get("likeCount")) shares = format_number(w.get("shareCount")) text += f"> - #{i} 「{title}」— {account} · 点赞 {likes} · 分享 {shares}\n" if top_topics: text += ">\n> 3. **高频话题 TOP5**:\n" for i, (topic, count) in enumerate(top_topics, 1): text += f"> - #{i} **{topic}**(出现 {count} 次)\n" return html, text # ─── HTML 报告生成 ───────────────────────────────────────────────────────────────── def generate_html_report(works, subscriptions, date_label, empty_accounts, output_path=None, rate_limited_accounts=None): """生成 HTML 报告文件,返回 html_file_path""" rate_limited_accounts = rate_limited_accounts or [] if not works and not empty_accounts and not rate_limited_accounts: return None today = datetime.now().strftime('%Y-%m-%d') # 确定输出路径 if output_path: out_dir = Path(output_path).parent html_file = Path(output_path) else: # 优先使用 SKILL_PATH/report/,其次用脚本所在目录/report/ skill_dir = os.environ.get("SKILL_PATH", "") if skill_dir: out_dir = Path(skill_dir) / 'report' else: out_dir = Path(__file__).resolve().parent.parent / 'report' # 从作品数据提取账号名称,用于文件名 account_names = list(dict.fromkeys( w.get("accountName") or w.get("_account_id", "unknown") for w in works )) if len(account_names) == 1: name_part = account_names[0] elif len(account_names) > 1: name_part = "_".join(account_names[:3]) if len(account_names) > 3: name_part += f"_等{len(account_names)}账号" else: name_part = "抖音订阅" # 文件名安全处理:替换非法字符 safe_name = re.sub(r'[\\/:*?"<>|]', '_', name_part) html_file = out_dir / f'{safe_name}_{today}_report.html' out_dir.mkdir(parents=True, exist_ok=True) # 分组数据 cat_groups = defaultdict(list) for work in works: cat = work.get("_category", "关注账号") if cat not in DEFAULT_CATEGORIES: cat = "关注账号" cat_groups[cat].append(work) cat_colors = { "竞对账号": ("#e74c3c", "#fadbd8"), "同类账号": ("#f39c12", "#fdebd0"), "关注账号": ("#3498db", "#d6eaf8"), } # 构建 HTML html = f"""<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>抖音账号作品报告 — {datetime.now().strftime('%Y-%m-%d')}</title> <style> * {{ margin: 0; padding: 0; box-sizing: border-box; }} body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; background: #f5f6fa; color: #2c3e50; line-height: 1.6; }} .container {{ max-width: 1100px; margin: 0 auto; padding: 20px; }} .header {{ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); color: white; padding: 32px 28px; border-radius: 12px; margin-bottom: 24px; text-align: center; }} .header h1 {{ font-size: 24px; margin-bottom: 6px; }} .header .subtitle {{ font-size: 14px; opacity: 0.75; }} .stats-bar {{ display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 24px; }} .stat-card {{ flex: 1; min-width: 140px; background: white; border-radius: 10px; padding: 16px 20px; box-shadow: 0 1px 4px rgba(0,0,0,.06); text-align: center; }} .stat-card .number {{ font-size: 28px; font-weight: 700; }} .stat-card .label {{ font-size: 13px; color: #7f8c8d; margin-top: 4px; }} .stat-card.works .number {{ color: #3498db; }} .stat-card.accounts .number {{ color: #2ecc71; }} .stat-card.empty .number {{ color: #e74c3c; }} .category-section {{ margin-bottom: 28px; }} .category-title {{ font-size: 18px; font-weight: 700; padding: 8px 16px; border-radius: 8px 8px 0 0; display: inline-block; }} .account-block {{ background: white; border-radius: 10px; margin-bottom: 16px; box-shadow: 0 1px 4px rgba(0,0,0,.06); overflow: hidden; }} .account-header {{ padding: 14px 20px; border-bottom: 1px solid #eee; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; }} .account-name {{ font-size: 16px; font-weight: 700; }} .account-fans {{ font-size: 13px; color: #7f8c8d; }} .account-fans strong {{ color: #e74c3c; }} .table-wrap {{ overflow-x: auto; padding: 0 20px 16px; }} table {{ width: 100%; border-collapse: collapse; font-size: 14px; }} th {{ background: #f8f9fa; padding: 10px 12px; text-align: left; font-weight: 600; color: #555; border-bottom: 2px solid #dee2e6; white-space: nowrap; }} th.right {{ text-align: right; }} td {{ padding: 10px 12px; border-bottom: 1px solid #f1f3f5; vertical-align: top; }} td.right {{ text-align: right; font-variant-numeric: tabular-nums; }} tr:hover {{ background: #f8f9ff; }} td.title {{ max-width: 360px; }} td.title a {{ color: #2c3e50; text-decoration: none; font-weight: 500; }} td.title a:hover {{ color: #3498db; text-decoration: underline; }} .empty-notice {{ background: #fff3cd; border-left: 4px solid #f39c12; padding: 12px 20px; border-radius: 6px; margin-bottom: 16px; font-size: 14px; color: #856404; }} .summary-block {{ background: white; border-radius: 10px; margin-bottom: 16px; box-shadow: 0 1px 4px rgba(0,0,0,.06); padding: 20px 24px; }} .summary-block h3 {{ margin: 0 0 12px; font-size: 16px; color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 8px; display: inline-block; }} .summary-block .highlight {{ background: #eaf6ff; border-left: 4px solid #3498db; padding: 10px 16px; border-radius: 6px; margin: 10px 0; font-size: 14px; color: #2c3e50; line-height: 1.6; }} .summary-block .top-list {{ list-style: none; padding: 0; margin: 8px 0; }} .summary-block .top-list li {{ padding: 8px 12px; border-bottom: 1px solid #f1f3f5; font-size: 14px; display: flex; justify-content: space-between; }} .summary-block .top-list li:last-child {{ border-bottom: none; }} .summary-block .rank {{ color: #e74c3c; font-weight: 700; margin-right: 8px; }} .summary-block .metric {{ color: #7f8c8d; font-size: 13px; }} .footer {{ text-align: center; padding: 20px; color: #999; font-size: 13px; border-top: 1px solid #e8e8e8; margin-top: 20px; }} .badge {{ display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; }} @media (max-width: 768px) {{ .container {{ padding: 12px; }} .header {{ padding: 20px 16px; }} .stats-bar {{ gap: 8px; }} .stat-card {{ min-width: 100px; padding: 12px; }} .stat-card .number {{ font-size: 22px; }} table {{ font-size: 12px; }} th, td {{ padding: 8px 6px; }} td.title {{ max-width: 180px; }} }} </style> </head> <body> <div class="container"> <div class="header"> <h1>📊 抖音账号作品报告</h1> <div class="subtitle">生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M')} | {date_label}</div> </div> <div class="stats-bar"> <div class="stat-card accounts"> <div class="number">{len(subscriptions)}</div> <div class="label">订阅账号</div> </div> <div class="stat-card works"> <div class="number">{len(works)}</div> <div class="label">作品总数</div> </div> <div class="stat-card empty"> <div class="number">{len(empty_accounts)}</div> <div class="label">无更新账号</div> </div> </div> """ # 分类展示 for cat in DEFAULT_CATEGORIES: arts = cat_groups.get(cat, []) if not arts: continue cat_color, cat_bg = cat_colors.get(cat, ("#3498db", "#d6eaf8")) cat_icons = {"竞对账号": "⚔️", "同类账号": "◎", "关注账号": "⭐"} # 按账号分组 account_groups = defaultdict(list) for art in arts: account_key = art.get("_accountName") or art.get("authorName") or art.get("_accountId", "未知") account_groups[account_key].append(art) html += f""" <div class="category-section"> <div class="category-title" style="background:{cat_bg};color:{cat_color};"> {cat_icons.get(cat, '●')} {cat} — {len(arts)} 条作品 </div> """ for account_name, account_works in sorted(account_groups.items(), key=lambda x: x[0]): account_works.sort(key=lambda a: -(int(a.get("shareCount", 0) or 0))) fans = format_fans(account_works[0].get("followerCount")) sec_uid = account_works[0].get("secUid") or "" account_link = f"https://www.douyin.com/user/{sec_uid}" if sec_uid else "" name_html = f'<a href="{account_link}" target="_blank" style="color:inherit;text-decoration:none;">{account_name}</a>' if account_link else account_name html += f""" <div class="account-block"> <div class="account-header"> <span class="account-name">📌 {name_html}</span> <span class="account-fans">粉丝: <strong>{fans}</strong> | 作品: {len(account_works)} 条</span> </div> <div class="table-wrap"> <table> <thead> <tr> <th>作品标题</th> <th class="right">收藏数</th> <th class="right">评论数</th> <th class="right">分享数</th> <th class="right">点赞数</th> <th>发布时间</th> </tr> </thead> <tbody> """ for work in account_works: title = work.get("title") or "无标题" video_url = work.get("workUrl") or work.get("videoUrl", "#") collects = format_number(work.get("collectCount")) comments = format_number(work.get("commentCount")) shares = format_number(work.get("shareCount")) likes = format_number(work.get("likeCount")) pub_time = work.get("publishTime") or "-" if len(pub_time) > 16: pub_time = pub_time[:16] title_display = title.replace("\n", " ") html += f""" <tr> <td class="title"><a href="{video_url}" target="_blank">{title_display}</a></td> <td class="right">{collects}</td> <td class="right">{comments}</td> <td class="right">{shares}</td> <td class="right">{likes}</td> <td>{pub_time}</td> </tr> """ html += """ </tbody> </table> </div> </div> """ # 无更新账号 if empty_accounts: html += '<div class="category-section">\n' for name in empty_accounts: html += f'<div class="empty-notice">⚠️ <strong>{name}</strong>:该时间段内无更新作品</div>\n' html += '</div>\n' # 被频率限制的账号 if rate_limited_accounts: html += '<div class="category-section">\n' for name in rate_limited_accounts: html += (f'<div class="empty-notice" style="border-left-color:#e67e22;">' f'⚠️ <strong>{name}</strong>:该账号订阅已超过失败阈值,请联系客服邮箱处理。</div>\n') html += '</div>\n' # ── 数据总结区块(占位,脚本结束时替换为实际内容)── if works: html += '\n<!-- SUMMARY_PLACEHOLDER -->\n' html += f""" <div class="footer"> 订阅账号: {len(subscriptions)} 个 | 作品总数: {len(works)} 条 | 无更新: {len(empty_accounts)} 个 | 限流: {len(rate_limited_accounts)} 个<br> 由抖音账号订阅追踪自动生成 | redfox.hk </div> </div> </body> </html>""" html_file.write_text(html, encoding="utf-8") return str(html_file) def print_markdown_table(works): """输出纯 Markdown 表格,供 Agent 直接展示给用户""" if not works: print("没有获取到任何作品") return cat_groups = defaultdict(list) for work in works: cat = work.get("_category", "关注账号") if cat not in DEFAULT_CATEGORIES: cat = "关注账号" cat_groups[cat].append(work) for cat in DEFAULT_CATEGORIES: arts = cat_groups.get(cat, []) if not arts: continue account_groups = defaultdict(list) for art in arts: account_key = art.get("_accountName") or art.get("authorName") or art.get("_accountId", "未知") account_groups[account_key].append(art) print(f"\n### {cat}({len(arts)} 条作品)") for account_name, account_works in sorted(account_groups.items(), key=lambda x: x[0]): account_works.sort(key=lambda a: -(int(a.get("shareCount", 0) or 0))) fans = format_fans(account_works[0].get("followerCount")) sec_uid = account_works[0].get("secUid") or "" account_link = f"https://www.douyin.com/user/{sec_uid}" if sec_uid else "" if account_link: print(f"\n**[{account_name}]({account_link})**(粉丝: {fans})") else: print(f"\n**{account_name}**(粉丝: {fans})") print() print("| 作品标题 | 收藏数 | 评论数 | 分享数 | 点赞数 | 发布时间 |") print("|----------|--------|--------|--------|--------|----------|") for work in account_works: title = work.get("title") or "无标题" work_url = work.get("workUrl") or work.get("videoUrl") or "" title_safe = title.replace("|", "\\|").replace("\n", " ") if work_url: title_d = f"[{title_safe}]({work_url})" else: title_d = title_safe collects = format_number(work.get("collectCount")) comments = format_number(work.get("commentCount")) shares = format_number(work.get("shareCount")) likes = format_number(work.get("likeCount")) pub_time = work.get("publishTime") or "-" if len(pub_time) > 16: pub_time = pub_time[:16] pub_short = pub_time[5:16] if len(pub_time) >= 16 else pub_time print(f"| {title_d} | {collects} | {comments} | {shares} | {likes} | {pub_short} |") print() # ─── 主流程 ──────────────────────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser( description="抖音账号订阅追踪 — 订阅抖音账号,追踪最新作品", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python3 subscribe.py add "5437662" --name "李佳琦" --category "竞对账号" python3 subscribe.py add "5437662,13232311,3234242" python3 subscribe.py remove "5437662" python3 subscribe.py list python3 subscribe.py fetch python3 subscribe.py fetch --date 2026-06-01 """, ) subparsers = parser.add_subparsers(dest="command", help="可用命令") # ── add 子命令 ── add_parser = subparsers.add_parser("add", help="添加订阅(需提供抖音号)") add_parser.add_argument("account_ids", help="抖音号,支持逗号分隔多个(如 5437662,13232311)") add_parser.add_argument("--name", dest="account_name", default="", help="账号名(可选,仅用于显示)") add_parser.add_argument("--category", default="关注账号", choices=DEFAULT_CATEGORIES + ["其他"], help="分类标签(默认: 关注账号)") # ── remove 子命令 ── remove_parser = subparsers.add_parser("remove", help="取消订阅") remove_parser.add_argument("account_id", help="抖音号") # ── list 子命令 ── subparsers.add_parser("list", help="列出所有订阅") # ── fetch 子命令 ── fetch_parser = subparsers.add_parser("fetch", help="拉取最新作品") fetch_parser.add_argument("--accounts", default="", help="抖音号列表,逗号分隔(如 YuZhouXiaoLi1220,Fish688688)") fetch_parser.add_argument("--date", default="", help="指定单日日期 YYYY-MM-DD(默认: 最新数据)") fetch_parser.add_argument("--date-start", default="", help="日期范围开始 YYYY-MM-DD") fetch_parser.add_argument("--date-end", default="", help="日期范围结束 YYYY-MM-DD") fetch_parser.add_argument("--markdown", action="store_true", help="输出 Markdown 格式表格(供 Agent 展示)") fetch_parser.add_argument("--html", action="store_true", help="生成 HTML 报告文件") fetch_parser.add_argument("--html-path", default="", help="HTML 报告输出路径") # ── 全局参数 ── parser.add_argument("--api-key", help="API Key") args = parser.parse_args() # ── Banner ── banner = f"""{CYAN}{BOLD} ╔══════════════════════════════════════╗ ║ 抖音账号订阅 · 作品追踪 ║ ║ 竞对 · 同类 · 关注 · 一网打尽 ║ ╚══════════════════════════════════════╝{RESET} """ print(banner) # ── 检查依赖 ── if not HAS_REQUESTS: error("缺少 requests 库,请安装: pip3 install requests") sys.exit(1) # ── 分发命令 ── if args.command == "add": add_subscriptions(args.account_ids, args.account_name, args.category) return if args.command == "remove": remove_subscription(args.account_id) return if args.command == "list": list_subscriptions() return if args.command == "fetch": # ── 获取账号列表:优先 --accounts 参数,其次 JSON 文件 ── accounts_arg = getattr(args, 'accounts', '') or '' is_adhoc_query = bool(accounts_arg) # 区分规则1/2的关键标志 if accounts_arg: # 直接从命令行参数构建订阅列表(无需文件存储) raw_ids = [aid.strip() for aid in accounts_arg.split(",") if aid.strip()] subscriptions = [ {"accountId": aid, "accountName": aid, "category": "关注账号"} for aid in raw_ids ] else: subscriptions = load_subscriptions() if not subscriptions: error("未指定任何抖音账号,请使用 --accounts 参数传入抖音号") print(f"\n 示例: {CYAN}python3 subscribe.py fetch --accounts \"YuZhouXiaoLi1220,Fish688688\" --html{RESET}") sys.exit(1) api_key = get_api_key(cli_key=args.api_key) session = requests.Session() session.verify = True session.headers.update({ "Content-Type": "application/json", "REDFOX_API_KEY": api_key, }) date_str = args.date or "" date_start = getattr(args, 'date_start', '') or '' date_end = getattr(args, 'date_end', '') or '' user_specified_date = bool(date_str or date_start or date_end) # ── 规则 1 & 2:根据是否已订阅,决定默认查询策略 ── if not user_specified_date: if is_adhoc_query: # 规则1:未订阅账号,默认查近 30 天 fallback_start = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d') fallback_end = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') date_label = f"(近 30 天: {fallback_start} 至 {fallback_end})" else: # 规则2:已订阅账号,默认查 T-1 yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') date_str = yesterday date_label = f"(日期: {date_str})" else: if date_start and date_end: date_label = f"(日期范围: {date_start} 至 {date_end})" elif date_str: date_label = f"(日期: {date_str})" else: date_label = "(最新数据)" step(f"从 {len(subscriptions)} 个抖音账号拉取作品{date_label}...") print() if is_adhoc_query and not user_specified_date and not rate_limited_accounts: # 规则1:查近 30 天 works, empty_accounts, rate_limited_accounts = fetch_all_works( session, subscriptions, None, fallback_start, fallback_end ) else: works, empty_accounts, rate_limited_accounts = fetch_all_works( session, subscriptions, date_str or None, date_start or None, date_end or None ) # ── 规则 1:近 30 天无数据,自动回溯近半年 ── if is_adhoc_query and not works and not user_specified_date and not rate_limited_accounts: info("近 30 天无更新作品,自动回溯近半年...") print() half_year_start = (datetime.now() - timedelta(days=180)).strftime('%Y-%m-%d') half_year_end = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') date_label = f"(回溯半年: {half_year_start} 至 {half_year_end})" step(f"从 {len(subscriptions)} 个抖音账号拉取作品{date_label}...") print() works, empty_accounts, rate_limited_accounts = fetch_all_works( session, subscriptions, None, half_year_start, half_year_end ) date_str = "" date_start = half_year_start date_end = half_year_end # ── 规则 2:已订阅账号 T-1 无数据时的提示 ── if not is_adhoc_query and not works and not user_specified_date and not rate_limited_accounts: for name in empty_accounts: print(f"\n {YELLOW}[!]{RESET} 「{name}」昨日暂未发布作品") print(f"\n {CYAN}💡 如需数据核查可联系工作人员邮箱 {SUPPORT_EMAIL} 处理{RESET}") # ── 规则 3:被频率限制拒绝的账号提示 ── for name in rate_limited_accounts: print(f"\n {RED}[✗]{RESET} 「{name}」当前账号订阅已超过失败阈值,请联系客服邮箱 {SUPPORT_EMAIL} 处理") if not works and not empty_accounts and not rate_limited_accounts: warn("未获取到任何作品,可能是账号暂无数据或 API 暂时不可用") sys.exit(1) # ── 规则 1:未订阅账号近半年无数据的最终提示 ── if is_adhoc_query and not works and not rate_limited_accounts: for name in empty_accounts: print(f"\n {YELLOW}[!]{RESET} 抱歉您订阅的「{name}」账号近半年都未发布过作品") print(f"\n {CYAN}💡 如果需要数据核查可联系工作人员邮箱 {SUPPORT_EMAIL} 处理{RESET}") if works: info(f"拉取完成: 共 {len(works)} 条作品") # ── 未收录账号提示 ── NOT_FOUND_MSG = "未查询到相关账号:当前 Skill 仅收录热门账号。如需定制数据,可邮件联系红狐数据咨询:redfoxdata@proton.me" # ── 输出(终端 / Markdown / HTML) ── want_html = getattr(args, 'html', False) html_path_arg = getattr(args, 'html_path', '') or '' # 仅当有作品数据时才生成 HTML 报告 if want_html and works: html_file = generate_html_report( works, subscriptions, date_label, empty_accounts, output_path=html_path_arg or None, rate_limited_accounts=rate_limited_accounts ) if html_file: # ── 生成总结并嵌入 HTML(替换占位符)── summary_html, summary_text = generate_summary(works) raw = Path(html_file).read_text(encoding="utf-8") raw = raw.replace("<!-- SUMMARY_PLACEHOLDER -->", summary_html) Path(html_file).write_text(raw, encoding="utf-8") # ── 自动打开 HTML 报告 ── try: import subprocess subprocess.Popen(["open", str(html_file)]) except Exception: pass info(f"HTML 报告已生成: {html_file}") # ── 输出 Markdown 表格(带链接,供 Agent 对话展示)── print_markdown_table(works) for name in empty_accounts: warn(f"「{name}」该时间段内无更新作品") # ── 输出纯文本总结(供 Agent 对话展示,与 HTML 内容一致)── if summary_text: print(f"\n{GREEN}=== 作品总结(与 HTML 报告一致)==={RESET}") print(summary_text) elif want_html and not works: info("无作品数据,跳过 HTML 报告生成") # 仍然输出提示信息 for name in empty_accounts: warn(f"「{name}」该时间段内无更新作品") elif getattr(args, 'markdown', False): if works: print_markdown_table(works) # ── 输出纯文本总结 ── _, summary_text = generate_summary(works) if summary_text: print(f"\n### 对上述作品的总结") print(summary_text) for name in empty_accounts: print(f"\n**{name}**:该时间段内无更新作品") print(f"\n订阅账号: {len(subscriptions)} 个 | 作品总数: {len(works)} 条") else: if works: print_terminal_table(works) for name in empty_accounts: warn(f"「{name}」该时间段内无更新作品") print(f"\n{GREEN}{BOLD}✓ 完成!{RESET}") print(f" 订阅账号: {len(subscriptions)} 个") print(f" 作品总数: {len(works)} 条") return # ── 无命令 ── parser.print_help() print(f"\n{CYAN}快速开始:{RESET}") print(f" add <抖音号> — 添加订阅(支持逗号分隔多个)") print(f" remove <抖音号> — 取消订阅") print(f" list — 查看订阅") print(f" fetch — 拉取作品") if __name__ == "__main__": main()
-
-
README.en.md 4.1 KB
# Douyin Account Subscription Tracker / douyin-subscribe --- ## Introduction Subscribe to competitor, peer, and follow accounts via Douyin ID. Automatically fetch the latest content data every day, generate visual reports, and stay on top of Douyin trends effortlessly. **Core Value** - **One-click Subscribe**: Send a Douyin ID to subscribe — no complex setup, supports up to 20 accounts - **Daily Auto-push**: Automatically fetches the latest content from subscribed accounts every day at 9:00 AM — no manual effort required - **Visual Reports**: Auto-generates polished HTML reports with preview and sharing support — data at a glance - **Full-spectrum Data**: Favorites, comments, shares, likes, and publish time — all key metrics covered **Ideal For** - 📊 **Content Operators** — Track competitor account activity and spot trending content in real time - 🎬 **Short-video Creators** — Monitor top accounts in your niche for inspiration and data-driven insights - 🏢 **Brands / MCNs** — Batch-manage followed accounts and receive daily content digests automatically --- ## Features ### Core Features - **Direct Douyin ID Subscription**: Subscribe using Douyin IDs — simple and transparent - **Daily Auto-fetch**: Automatically retrieves the latest content data every day at 9:00 AM after subscribing - **HTML Visual Reports**: Auto-generates polished report files with data summaries and Top 5 trending content - **Clickable Links**: Both titles and account names link directly to detail pages - **Smart Date Strategy**: Defaults to the previous day; auto-fallback to the last 7 days if no data; also supports custom date ranges - **Grouped Multi-account Display**: Grouped by account name, sorted by share count in descending order within each group --- ## 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 an account and obtain your `REDFOX_API_KEY`. - Configure the `REDFOX_API_KEY` environment variable on your device before using this skill. - Before providing your key, please verify its source, applicable scope, validity period, and whether reset/revocation is supported. - Do not hardcode or expose your key in plaintext in code, prompts, logs, or output files. --- ## Usage Guide Simply tell us the Douyin ID you want to subscribe to in natural language — no commands to memorize. ### Quick Reference | Intent | Example | Result | | ------ | ------- | ------ | | Subscribe | "Subscribe to Fish688688" | Auto-validates and subscribes, displays latest content | | Batch Subscribe | "Subscribe to abc123, def456, ghi789" | Subscribes to multiple accounts at once | | View Specific Dates | "Show me content from 2026-06-01 to 2026-06-05" | Fetches historical content by date range | | Unsubscribe | "Unsubscribe Fish688688" | Removes the account from your subscription list | ### Output Example After subscribing, you will receive: - **Markdown Content Table**: Grouped by account, showing favorites, comments, shares, and likes - **HTML Visual Report**: Auto-saved to the `report/` directory, including data summaries and Top 5 trending content - **Summary Statistics**: Standout account performance + Top 5 trending topics --- ## Use Cases | Scenario | Role | Example Request | Benefit | | -------- | ---- | --------------- | ------- | | Competitor Monitoring | Content Operator | "Subscribe to competitor Douyin IDs and check their content daily" | Get daily competitor updates automatically, never miss trending content | | Niche Tracking | Short-video Creator | "Subscribe to these top accounts and track their content" | Stay current with niche trends and gain creative inspiration | | Batch Management | MCN / Brand | "Subscribe to all these accounts and manage them together" | Batch subscribe up to 20 accounts with automatic daily digests | | Historical Review | Content Analyst | "Show me last week's content data for these accounts" | Query historical data with custom date ranges | --- -
README.md 3.5 KB
# 抖音账号订阅追踪 / douyin-subscribe --- ## 简介 通过抖音号订阅竞对、同类和关注账号,每天自动获取最新作品数据,生成可视化报告,轻松掌握抖音动态。 **核心价值** - **一键订阅**:发送抖音号即可订阅,无需复杂配置,最多支持 20 个账号 - **每日自动推送**:每天早上 9:00 自动拉取订阅账号的最新作品,无需手动操作 - **可视化报告**:自动生成精美 HTML 报告,支持预览和分享,数据一目了然 - **全维度数据**:收藏数、评论数、分享数、点赞数、发布时间,关键指标全覆盖 **适用对象** - 📊 **内容运营** — 追踪竞品账号动态,及时发现爆款内容趋势 - 🎬 **短视频创作者** — 监控同赛道头部账号,获取创作灵感与数据参考 - 🏢 **品牌 / MCN** — 批量管理关注账号,每日自动收到作品日报 --- ## 功能特性 ### 核心功能 - **抖音号直接订阅**:通过抖音号订阅账号,简单透明 - **每日自动拉取**:订阅后每天 9:00 自动获取最新作品数据 - **HTML 可视化报告**:自动生成精美报告文件,含数据总结与爆款 TOP5 - **作品超链接**:标题和账号名均可点击跳转详情页 - **智能日期策略**:默认查前一天,无数据自动回溯近 7 天,也支持自定义日期范围 - **多账号分组展示**:按账号依次分组,同账号内按分享数降序排列 --- ## 密钥获取与安全说明 - 本技能需要使用环境变量:`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` 后使用本技能。 - 在提供密钥前,请先确认密钥来源、可用范围、有效期及是否支持重置/撤销。 - 禁止在代码、提示词、日志或输出文件中硬编码/明文暴露密钥。 --- ## 使用指南 直接用自然语言说出你想订阅的抖音号即可,无需记忆命令。 ### 常用说法速查 | 意图 | 示例话术 | 效果 | | ---- | -------- | ---- | | 订阅账号 | 「订阅 Fish688688」 | 自动验证并订阅,展示最新作品数据 | | 批量订阅 | 「订阅 abc123, def456, ghi789」 | 一次订阅多个账号 | | 查看特定日期 | 「查一下 2026-06-01 到 2026-06-05 的作品」 | 按日期范围拉取历史作品 | | 取消订阅 | 「取消订阅 Fish688688」 | 从订阅列表中移除该账号 | ### 输出示例 订阅成功后,你将收到: - **Markdown 作品表格**:按账号分组展示,含收藏数、评论数、分享数、点赞数 - **HTML 可视化报告**:自动保存至 `report/` 目录,含数据总结与爆款 TOP5 - **总结统计**:突出账号表现 + 高频话题 TOP5 --- ## 使用场景 | 场景 | 角色 | 示例问法 | 收益 | | ---- | ---- | -------- | ---- | | 竞品监控 | 内容运营 | 「订阅竞品的抖音号,每天看他们的作品」 | 每日自动获取竞品动态,不错过爆款 | | 赛道追踪 | 短视频创作者 | 「订阅这几个头部账号,追踪他们的内容」 | 及时掌握赛道趋势,获取创作灵感 | | 批量管理 | MCN / 品牌 | 「把这些账号都订阅上,统一管理」 | 最多 20 个账号批量订阅,日报自动推送 | | 历史回溯 | 内容分析师 | 「查一下上周这些账号的作品数据」 | 支持自定义日期范围查询历史数据 | --- -
SKILL.md 14.4 KB
--- name: douyin-subscribe display_name: 抖音账号订阅追踪 display_name_en: Douyin Account Subscribe description: 抖音账号订阅追踪 — 通过抖音号订阅账号(最多20个),Agent 每日 9:00 自动拉取并生成 HTML 报告。账号 ID 直接内置于自动化命令中,无需文件存储。支持多抖音号批量订阅,自动生成精美 HTML 作品报告,终端/Markdown 表格按账号依次展示作品标题、收藏、评论、分享、点赞、发布时间等数据。当用户订阅抖音账号、追踪抖音作品更新、监控抖音竞品账号时使用。触发词:抖音订阅、抖音账号订阅、抖音订阅追踪、抖音作品订阅、抖音账号监控、抖音作品追踪、抖音每日推送、抖音日报。 description_zh: 抖音账号订阅追踪工具,订阅后每日9点自动拉取最新作品并生成HTML报告。 description_en: Subscribe to Douyin accounts and get their latest works pulled automatically at 9:00 every day with a polished HTML report. category: data version: 1.1.0 author: 红狐数据 --- # 抖音账号订阅追踪 ## 📝 简介 通过抖音号订阅竞对、同类和关注账号(最多 20 个),自动抓取最新作品。**账号 ID 直接传入命令行参数,不依赖任何本地文件存储**,自动化任务中硬编码所有已订阅的抖音号。 --- ## ✨ 功能特性 | 功能模块 | 能力描述 | 核心价值 | |---------|---------|---------| | 账号直传 | 通过 `--accounts` 参数直接传入抖音号,无需文件存储 | 简单透明,命令即文档 | | 每日自动推送 | 自动化任务内置账号 ID,每天早上 9:00 自动执行 | 定时获取,无需手动操作 | | HTML 报告 | fetch 时自动生成精美 HTML 报告文件,支持预览和分享 | 可视化浏览,一目了然 | | 作品数据 | 收藏数、评论数、分享数、点赞数、发布时间 | 全维度数据,一目了然 | | 按账号展示 | 终端表格按账号依次分组展示 | 清晰直观,便于对比 | | 日期回溯 | 支持指定历史日期查询 | 追溯往期作品数据 | --- ## 🔑 鉴权 ### 获取 API Key 请前往 [红狐hub](https://redfox.hk/settings/api-keys?source=github) 获取API KEY ### 配置 API Key 方案1: 以OpenClaw为例,将REDFOX_API_KEY添加到~/.openclaw/openclaw.json中: ```bash { "env": { "REDFOX_API_KEY": "ak_xxxx..." } } ``` 方案2: 终端配置 ```bash export REDFOX_API_KEY="ak_xxxx..." ``` --- ## ⚙️ 使用方式 ### 拉取作品(核心命令) ```bash # 基础用法:直接传入抖音号列表 python3 "$SKILL_PATH/scripts/subscribe.py" fetch --accounts "YuZhouXiaoLi1220,Fish688688" # 生成 HTML 报告(自动保存到 report/ 目录) python3 "$SKILL_PATH/scripts/subscribe.py" fetch --accounts "YuZhouXiaoLi1220,Fish688688" --html # 指定 HTML 报告输出路径 python3 "$SKILL_PATH/scripts/subscribe.py" fetch --accounts "YuZhouXiaoLi1220,Fish688688" --html --html-path ~/Desktop/report.html # Markdown 格式输出(供 Agent 展示) python3 "$SKILL_PATH/scripts/subscribe.py" fetch --accounts "YuZhouXiaoLi1220,Fish688688" --markdown # 指定单日日期 python3 "$SKILL_PATH/scripts/subscribe.py" fetch --accounts "YuZhouXiaoLi1220,Fish688688" --html --date 2026-06-01 # 指定日期范围 python3 "$SKILL_PATH/scripts/subscribe.py" fetch --accounts "YuZhouXiaoLi1220,Fish688688" --html --date-start 2026-06-01 --date-end 2026-06-05 ``` ### 参数说明 | 命令 | 参数 | 说明 | |------|------|------| | `fetch` | **`--accounts`** | **抖音号列表(必填或核心参数),逗号分隔,如 `"id1,id2,id3"`** | | | `--date` | 指定单日日期 YYYY-MM-DD | | | `--date-start` | 指定日期范围开始 YYYY-MM-DD | | | `--date-end` | 指定日期范围结束 YYYY-MM-DD | | | `--markdown` | 输出 Markdown 格式表格 | | | `--html` | 生成 HTML 报告文件(保存到 report/ 目录) | | | `--html-path` | 自定义 HTML 报告输出路径 | | 全局 | `--api-key` | 指定 API Key | ### 依赖 | 依赖 | 安装命令 | |------|----------| | `requests` | `pip3 install requests` | --- ## 🤖 每日自动推送 用户订阅账号后,Agent 会创建一个**每日定时任务**,**每天早上 9:00** 自动执行。任务命令中**直接包含所有已订阅的抖音号 ID**: ```bash # 自动化任务中的实际命令(账号已内置) python3 subscribe.py fetch --accounts "YuZhouXiaoLi1220,Fish688688" --html ``` ### 工作原理 1. 用户发送抖音号 → Agent 将其写入自动化命令的 `--accounts` 参数 2. 每天 9:00 定时触发 → 命令自带账号 ID → 直接调 API 拉数据 3. 自动生成 HTML 报告 → 保存至 `report/` 目录 4. **不依赖任何本地 JSON 文件** --- ## 🔧 账号管理规则(Agent 必须遵守) > **核心原则:所有订阅状态 = 自动化命令中的 `--accounts` 参数。无文件存储。** ### 订阅新账号流程 当用户提供新的抖音号时,Agent **必须**按以下步骤操作: #### Step 1:查询现有自动化任务 使用 `automation_update mode=list` 获取所有「抖音订阅」相关的自动化任务,读取每个任务的 `--accounts` 参数中的账号列表。 #### Step 2:判断是否追加 / 新建 | 条件 | 操作 | |------|------| | 现有账号数 + 新账号数 **≤ 20** | **追加到现有自动化任务的 `--accounts` 参数**(去重后用逗号拼接) | | 现有账号数 + 新账号数 **> 20** | **创建新的自动化任务**,将超出部分放入新任务的 `--accounts` | | 用户明确要求取消某账号 | 从对应自动化任务的 `--accounts` 中移除该 ID | #### Step 3:更新/创建自动化任务 - **追加场景**:调用 `automation_update mode=update`,修改现有任务的 `prompt` 字段中的 `--accounts` 值 - **新建场景**:调用 `automation_update mode=create`,新建一个独立的每日定时任务(同样 9:00 执行),名称可加序号如「抖音订阅作品日报 #2」 #### Step 4:立即拉取一次 无论追加还是新建,**都必须立即执行一次 fetch**(带 `--html`)展示最新结果给用户。 ### 多任务示例 假设已有 18 个账号,用户又提供了 5 个新账号: ``` 现有任务 #1: --accounts "id1,id2,...,id18" (18个) 新增 5 个: id19,id20,id21,id22,id23 → 任务 #1 更新为: --accounts "id1,...,id18,id19,id20" (20个 ✅) → 新建任务 #2: --accounts "id21,id22,id23" (3个 ✅) ``` 每个独立任务都会在每天 9:00 各自执行,各自生成 HTML 报告。 ### 取消订阅 从对应任务的 `--accounts` 中移除该 ID 即可。如果移除后某个任务剩余 0 个账号,则删除该自动化任务。 --- ## 📋 交互规范 ### 订阅流程 > **静默执行原则:所有中间步骤(fetch 验证、add 订阅、automation_update)均不得向用户输出任何过程性提示(如「正在验证账号」「账号验证通过」「自动订阅中」等)。仅向用户展示最终结果。** 1. **用户发送抖音号** → Agent **静默执行 `fetch --accounts "抖音号" --html --markdown`** 验证账号 2. **判断账号状态**(规则 1): - **接口返回正常数据**: - **静默执行** add 订阅(见下一步) - **近 30 天无数据,自动回溯半年后仍无数据**: - 告知用户:「抱歉您订阅的“xxx”账号近半年都未发布过作品,如果需要数据核查可联系工作人员邮箱 redfoxdata@proton.me 处理」 - **询问用户**:「是否订阅明日推送?」 - 用户回答「订阅」→ **静默执行** add 订阅 - 用户未确认 → 不订阅 3. **静默查询现有自动化任务**的 `--accounts` 列表,判断追加 or 新建: - 现有账号数 + 新账号数 ≤ 20 → 追加到现有任务的 `--accounts` - 现有账号数 + 新账号数 > 20 → 新建一个自动化任务放溢出部分 4. **静默更新/创建自动化任务** 5. **若 fetch 返回了作品数据**: - 自动打开 HTML 报告预览(`preview_url`) - 对话中展示 Markdown 作品表格 - 提示:「如需查看特定时间段的作品,可以告诉我,如“查一下 2026-06-01 到 2026-06-05 的作品”」 6. **若 fetch 无作品数据**(账号存在但时段内无更新):仅告知用户无更新,不生成 HTML 报告 7. 输出顺序:**先按账号名升序**分组,同账号内按**分享数降序**排列 8. 若某个账号在指定时间范围内无更新作品,直接告知用户:「**账号名**:该时间段内无更新作品」 > **重要:仅当 fetch 返回实际作品数据时,才需要三输出——Markdown 表格 + HTML 报告预览 + HTML 报告文件。无作品数据时不生成 HTML 报告。** ### 对话输出格式(订阅/拉取时必须遵循) Agent 执行 fetch 后,**必须在对话回复中按以下格式输出数据**(不是只依赖脚本终端输出): #### 格式一:账号概览表 ```markdown ## 📊 订阅报告 | 项目 | 值 | |------|-----| | 新订阅 | `新账号名`(粉丝量) | | 当前总账号 | N 个 | | 本次拉取作品 | N 条 | | 自动化任务 | 已更新 ✅ 每天 9:00 自动执行 | ### 账号概览 | 账号 | 粉丝量 | 领域 | 代表作数据 | |------|--------|------|-----------| | AccountA | xxx | 领域 | 🏆 代表作:收藏x / 点赞x / 分享x | ``` #### 格式二:每个账号的详细作品表(Markdown) ```markdown ### ▸ AccountName(粉丝: xxx) | 作品标题 | 收藏数 | 评论数 | 分享数 | 点赞数 | 发布时间 | |---------|--------|--------|--------|--------|---------| | 标题1 | 10.8w | 2.8k | 10.3w | 19.3w | 06-08 | ``` 要求: - 每个账号一个子标题 + 一个完整 Markdown 表格 - 标题列使用 API 返回的 `content` 字段完整原文(脚本内部映射为 `title`),不截断 - 数值列使用 `format_number` 后的可读格式(如 10.8w、2.3k) - 发布时间截取到 `MM-DD HH:mm` - 按**分享数降序**排列 #### 格式三:总结统计(必须附加在所有表格之后) ```markdown ### 对上述作品的总结 > 1. **突出的账号表现**:... > 2. **高频事件 TOP5**:... ``` #### 输出完整性清单 每次订阅/拉取,对话中**必须包含**以下全部内容: - [ ] 账号概览表(含总览信息) - [ ] 每个账号一个 Markdown 作品详情表 - [ ] 无更新账号提示(如有) - [ ] 总结统计分析 - [ ] **规则 2 适用**:已订阅账号昨日无数据时,结尾附加邮箱核查提示 - [ ] **规则 3 适用**:被频率限制拒绝的账号提示 - [ ] **仅当有作品数据时**:自动打开 HTML 报告预览(preview_url)+ HTML 报告文件已生成说明(附文件路径) ### 抖音号要求 接口通过 `uniqueName`(抖音展示ID)查询,脚本将用户提供的抖音号统一按 `uniqueName` 传入,`userId` 和 `shortId` 固定为空字符串。订阅时**必须提供抖音号**。 **重要:** 若用户只输入账号名称(如"李佳琦""老高與小茉")而未提供抖音号,必须提示: > "抖音账号名称存在多个重名情况,请提供准确的**抖音号**以便精准订阅。" 并附上获取抖音号的示例图供用户参考:  ### 数据规则 - **每个自动化任务最多 20 个账号**(超限时 Agent 自动创建新任务,详见「账号管理规则」) - **规则 1:未订阅账号(首次验证查询)**: - 默认查询近 30 天数据 - 若近 30 天无数据,自动回溯近半年(180 天) - 若近半年仍无数据,告知用户:「抱歉您订阅的“xxx”账号近半年都未发布过作品,如果需要数据核查可联系工作人员邮箱 redfoxdata@proton.me 处理」 - **规则 2:已订阅账号(日常拉取)**: - 默认查前一天数据(T-1) - 若昨日无数据,告知用户:「昨日该账号暂未发布作品」 - 结尾固定提示:「如果需要数据核查可联系工作人员邮箱 redfoxdata@proton.me 处理」 - **规则 3:失败阈值保护**: - 同一抖音号在 6 小时内累计 3 次 API 调用失败后,后续请求将被拒绝 - 拒绝时提示:「当前账号订阅已超过失败阈值,请联系客服邮箱 redfoxdata@proton.me 处理」 - 距上次失败超过 6 小时 / 同一账号查询成功,计数归零,恢复正常调用 - **用户指定日期不回溯**:若用户明确指定了日期或日期范围,查不到数据时直接告知用户,不自动回溯 - **支持日期范围查询**:用户可指定起止日期(`--date-start YYYY-MM-DD --date-end YYYY-MM-DD`)拉取历史作品 - 每账号最多返回 **10 条**作品 - 排序方式:账号名升序 → 同账号内**分享数降序** - **无数据提示**:若某个账号在指定时间范围内无更新作品,脚本会输出「**账号名**:该时间段内无更新作品」,Agent 需将此信息原样转达给用户 - 接口不支持多账号同时查询,脚本自动拆分后逐账号调用并整合数据 ### 输出表格结构(脚本终端格式,仅供参考) 脚本终端输出原始格式如下,但 **Agent 对话中必须使用「对话输出格式」中的 Markdown 表格重新排版**: ``` ▸ Fish688688(粉丝: 249w+) ────────────────────────────────────────────────────────────── 作品标题 收藏数 评论数 分享数 点赞数 发布时间 ────────────────────────────────────────────────────────────── 今晚图省事... 10.8w 2.8k 10.3w 19.3w 05-21 21:40 ``` 作品标题使用 API `content` 字段(脚本内部映射为 `title`),链接使用 `opusUrl` 字段(脚本内部映射为 `workUrl`)。Markdown 输出中标题自动渲染为可点击超链接:`[标题](workUrl)`。 > **注意**:当前广域库 API 不返回 `followerCount`(粉丝数),账号概览中粉丝量显示为 `-`。 ### 总结统计 表格展示完毕后,必须严格按以下格式附加总结: > 对上述作品的总结: > 1. **突出的账号表现**:指出数据最亮眼的账号及其代表作品 > 2. **高频事件 TOP5**:提炼所有作品中提到次数最多的 5 个事件/话题,并归纳每个事件对应的不同观点(不足 5 个可提炼的,按实际数量呈现,禁止虚构)
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.