Claude Skill

overseas-trending-search

海外跨平台热文搜索 — 输入任意关键词(中文/英文),一次搜索 X / TikTok / YouTube 三平台热门内容,每平台 Top N(默认5)统一列表(平台/标题/作者/播放/点赞/评论/发布时间/链接),终端分组表格 + CSV 导出 + 交互式 HTML 报告(卡片/表格双视图)。按关键词语言智能优先同语言内容(中文词→中文优先,英文词→英文优先,其余语言兜底)。当用户需要搜索海外平台热点、跨平台内容对比、话题/竞品舆情监控、选题素材采集时使用。

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_overseas-trending-search-5e7b435.zip · 26 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/overseas-trending-search
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

海外跨平台热文搜索 / overseas-trending-search


简介

海外跨平台热文搜索是一款跨平台热点洞察工具,输入任意关键词(中文/英文),即可一键搜索 X、TikTok、YouTube 三大海外平台的热门内容,智能识别关键词语言并优先展示同语言结果,输出统一字段列表与交互式 HTML 报告。

核心价值

  • 🔍 三平台覆盖:一键搜索 X / TikTok / YouTube 三大海外平台的热门内容
  • 🌐 智能语言优先:中文关键词优先中文内容,英文关键词优先英文内容,解决跨语言歧义
  • 📊 多格式输出:终端分组表格 + CSV 数据导出 + 交互式 HTML 报告(卡片/表格双视图)
  • ⚡ 灵活排序:支持按播放数/点赞数/评论数/发布时间自由排序,每平台独立取 Top N

适用对象

  • ✍️ 内容创作者 — 每日热点追踪,获取选题灵感与素材
  • 📈 市场运营 — 跨平台内容对比,了解不同平台内容形态差异
  • 🏢 品牌方 — 竞品舆情监控,查看海外用户真实讨论
  • 📊 数据分析师 — 话题事件追踪,量化跨平台传播效果

功能特性

核心功能

  • 🔍 三平台搜索:覆盖 X、TikTok、YouTube 三大海外平台的关键词搜索
  • 🌐 语言智能优先:中文关键词各平台优先展示中文内容,英文关键词优先英文内容,其余语言兜底
  • 📋 统一字段列表:平台、标题、作者、播放数、点赞数、评论数、发布时间、链接,跨平台一致
  • 📊 多维度排序:支持按播放数/点赞数/评论数/发布时间排序,每平台独立取 Top N
  • 📄 多格式输出:终端分组表格展示、CSV 数据导出、交互式 HTML 报告(卡片/表格双视图)
  • 🔗 多关键词支持:英文逗号分隔多个关键词,一次搜索同时命中
  • 🔄 错误容忍:某平台异常时自动跳过,不影响其他平台结果

使用指南

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

常用说法速查

意图 示例话术 效果
热点搜索 「搜索 AI 在海外平台的热门内容」 三平台搜索 + 终端表格 + CSV + HTML
指定平台 「帮我看 TikTok 上关于 AI 的热门视频」 仅搜索指定平台
跨平台对比 「对比 X 和 YouTube 上 iPhone 16 的热度」 指定平台对比分析
多关键词 「搜索 人工智能,AI agent 的海外热文」 同时搜索多个关键词
竞品监控 「搜一下 Tesla 在海外的讨论」 品牌竞品舆情追踪
素材采集 「导出 AI 相关的海外热文 CSV」 导出 CSV 做选题库

输出示例

搜索完成后,你将依次收到:

终端分组表格 → 按平台分组展示 Top N 结果,含标题、作者、播放/点赞/评论/发布时间、链接 CSV 导出 → 结构化数据导出,便于数据分析与存档 HTML 报告 → 交互式报告,支持卡片/表格双视图切换,可离线保存


使用场景

场景 角色 示例问法 收益
每日热点追踪 内容创作者 「搜索 AI 今天的热门内容」 掌握三平台当日最热内容
跨平台对比 市场运营 「对比 X 和 YouTube 上 Vision Pro 的热度」 了解不同平台内容形态差异
话题事件追踪 品牌方 「搜 新能源,EV 近 7 天传播情况」 追踪特定话题跨平台传播
竞品舆情监控 数据分析师 「搜一下 Tesla 在海外的真实讨论」 看海外用户真实反馈
素材灵感采集 自媒体运营 「导出 AI 相关海外热文 CSV」 导出数据做选题库

Skill manifest

海外跨平台热文搜索

输入任意关键词(中文/英文),一键搜索 X / TikTok / YouTube 三平台热门内容,每平台按点赞/播放/评论/发布时间取 Top N(默认 5),输出统一字段列表:平台、标题、作者、播放数、点赞数、评论数、发布时间、链接。

API 请求均携带 ChinaTrendingDigest-RedSkill 标识。需先配置 API Key,通过环境变量 REDFOX_API_KEY 或 --api-key 参数传入。 架构为平台适配器模式:新平台(如 YouTube)只需新增适配器文件,主流程零改动。


使用场景

场景 示例
每日热点追踪 每天跑一次 "AI",掌握三平台当日最热内容
跨平台对比 同一关键词对比 X / TikTok / YouTube 的热度与内容形态差异
话题事件追踪 搜 "新能源,EV" 追踪特定话题跨平台传播
竞品/舆情监控 搜品牌或产品名,看海外用户的真实讨论与爆款反馈
素材灵感采集 导出 CSV 做选题库与数据分析
账号内容挖掘 TikTok 侧支持按作者作品列表下钻(userAwemeList)

使用方法

# 基础用法:单关键词(默认最近 24 小时,按播放数降序)
python3 "$SKILL_PATH/scripts/digest.py" "AI"

# 中文关键词 / 多关键词(英文逗号分隔)
python3 "$SKILL_PATH/scripts/digest.py" "人工智能,AI agent"

# 放宽时间窗口到最近 3 天(TikTok 热门多为历史内容,建议放宽)
python3 "$SKILL_PATH/scripts/digest.py" "AI" --days 3

# 只跑指定平台
python3 "$SKILL_PATH/scripts/digest.py" "AI" --platforms tiktok

# 按发布时间排序
python3 "$SKILL_PATH/scripts/digest.py" "AI" --sort time

# 按点赞数排序 / 调整每平台返回条数(默认 5)
python3 "$SKILL_PATH/scripts/digest.py" "AI" --sort likes --top 10

# 仅导出 CSV / 不自动打开浏览器
python3 "$SKILL_PATH/scripts/digest.py" "AI" --csv-only --no-open

CSV / HTML 默认保存在 ~/Downloads/QoderOverseasTrending/。


返回结果展示规范

向用户展示结果时,必须包含以下字段:

字段 说明
平台 X / TikTok / YouTube(带平台标识色)
标题 推文/视频文案摘要,仅展示前 20 字(HTML 表格悬停看全文)
作者 作者昵称
播放数 千/万简写(如 47.3w),展示在作者之后
点赞数 千/万简写(如 62.9w)
评论数 同上
发布时间 YYYY-MM-DD HH:mm
链接 可点击跳转原文

平台状态需在结果前明示:某平台上游故障时优雅降级并在报告中标注,不影响其他平台结果。

  • 条数约束:每个平台最多返回 --top N 条(默认 5),按 --sort 指标在平台组内降序;终端与 HTML 均按平台分组展示。
  • 语言优先分层:中文关键词时各平台组内优先展示中文内容、英文关键词优先英文内容,组内中/英文内容不足时以其他语言兜底;层内仍按排序指标降序。
  • HTML 报告支持「卡片 / 表格」双视图切换,表格视图含点赞/评论/播放/分享全字段。
  • YouTube 点赞/评论数由 videoDetail 详情接口逐条补全(默认全量覆盖搜索结果 20 条,本地化计数串已解析为整数)。

参数说明

参数 说明 默认值
keywords 关键词,中英文均可,多词英文逗号分隔(位置参数) —
--days 时间窗口:最近 N 天(0=不限) 1
--platforms 平台列表:x,tiktok,youtube 任意组合 x,tiktok,youtube
--sort 排序:views / likes / comments / time views
--top 每平台最多返回条数(0=不限) 5
--output-dir 输出目录 ~/Downloads/QoderOverseasTrending
--api-key 指定 RedFox API Key —
--csv-only 仅生成 CSV,不生成 HTML —
--no-open 不自动打开浏览器 —

API Key 配置

任选一种方式配置个人 Key:

方式 命令
环境变量(推荐) export REDFOX_API_KEY=ak_你的密钥
命令行参数 --api-key ak_你的密钥
配置文件 echo '{"api_key":"ak_你的密钥"}' > ~/.qoder/apis/redfox.json

注册地址:redfox.hk


架构说明

scripts/
├── digest.py            # 主编排器:关键词×平台采集 → 时间过滤 → 排序 → 输出
├── config.py            # RedFox 网关地址、密钥加载(三级优先级)
└── sources/             # 平台适配器(新增平台只需加一个文件并在 __init__ 登记)
    ├── base.py          # BaseSource:统一 schema + 递增延迟重试
    ├── x_source.py      # X:search/tweetDetail/tweetComments
    ├── tiktok_source.py # TikTok:searchVideo(一条请求自带点赞/评论数)
    └── youtube_source.py# YouTube:searchVideo 列表 + videoDetail 补点赞/评论

统一记录 schema:platform / title / url / author / likes / comments / views / publish_ts / publish_time / keyword / shares。

平台接口状态(2026-07 实测)

平台 接口 状态
X search / tweetDetail / tweetComments ✅ 全部可用;search 必填 searchType(Top/Latest),缺失会误报 3203
TikTok searchVideo / awemeDetail / userAwemeList ✅ 全部可用,searchVideo 自带全量互动数据
YouTube searchVideo / videoDetail / videoComments ✅ 全部可用;searchVideo 返回播放数与相对发布时间,点赞/评论由 videoDetail 补全(Top 10)

依赖

pip3 install requests

常见问题

Q:X 搜索返回 3203 报错? A:X search 接口的 searchType 为必填参数(Top 热门 / Latest 最新),缺失时 RedFox 会返回误导性的 3203「X能力调用失败」。本技能已内置该参数;另外过期/无效的 tweetId 调 tweetDetail 也会报 3203。

Q:TikTok 搜索需要调详情接口补数据吗? A:不需要。searchVideo 一次请求即返回点赞/评论/播放/分享数和发布时间,日常概要足够。

Q:YouTube 的点赞/评论数据从哪里来的? A:searchVideo 列表接口只返回播放数,点赞/评论需调 videoDetail 逐条补全。技能默认对搜索结果 20 条全量补详情(youtube_source.py 中 detail_top 可调小以省积分),因此所有视频都有真实点赞/评论数;仍为 0 代表该视频确实无点赞/评论。YouTube 平台本身不公开分享数,故分享列恒为 0。

Q:每平台返回多少条? A:默认每平台按排序指标(点赞/播放/评论/发布时间)取 Top 5,--top N 可调(0=不限)。各平台独立分组、组内降序,互不影响。

Q:为什么默认参数下看不到 TikTok 内容? A:TikTok searchVideo 返回的是热门视频,发布时间多为数天乃至数月前,默认 --days 1(最近 24 小时)会将其整体过滤。想看 TikTok 热门请加 --days 0 或 --days 7;过滤时终端会按平台提示被排除的条数。

Q:搜中文关键词,为什么 X 上的日文内容被排到后面了? A:技能内置语言优先分层:关键词为中文时,各平台组内优先展示中文内容、其次英文,日文等其他语言兜底(解决「南海」命中日文「南海電鉄」这类歧义问题);英文关键词同理优先英文内容。语言识别基于标题字符集(汉字/假名/谚文/拉丁字母),无第三方依赖。

Q:想做每日定时运行? A:可以让 AI 注册定时任务,每天自动执行 digest.py 并生成报告。

Files (redfox-community)
  • assets
    • report_template.html 9.7 KB · in bundle
  • scripts
    • sources
      • base.py 5 KB
        #!/usr/bin/env python3
        """
        平台适配器抽象基类 — 所有平台实现统一接口,主流程对平台无感知
        新增平台(如 YouTube)只需继承 BaseSource 并实现 search(),无需改动主流程
        """
        
        import re
        import time
        from datetime import datetime, timedelta
        
        
        class PlatformUnavailable(Exception):
            """平台上游能力不可用(如 RedFox 3203 能力故障),主流程应优雅降级跳过"""
            pass
        
        
        class BaseSource:
            """平台适配器基类"""
        
            #: 平台标识(x / tiktok / youtube)
            platform = ""
            #: 平台展示名
            display_name = ""
        
            def search(self, session, keyword):
                """
                按关键词搜索,返回归一化记录列表(make_record 产物)。
                平台上游故障时应抛出 PlatformUnavailable,由主流程降级处理。
                """
                raise NotImplementedError
        
            # ─── 归一化 ─────────────────────────────────────────────────────────────────
            def make_record(self, title, url, author, likes, comments, views,
                            publish_ts, keyword, extra=None):
                """生成统一记录 schema,主流程只认这个结构"""
                record = {
                    "platform": self.platform,
                    "platform_name": self.display_name,
                    "title": (title or "").strip() or "无标题",
                    "url": url or "",
                    "author": author or "未知作者",
                    "likes": self._to_int(likes),
                    "comments": self._to_int(comments),
                    "views": self._to_int(views),
                    "publish_ts": publish_ts or 0,
                    "publish_time": self._fmt_ts(publish_ts),
                    "keyword": keyword or "",
                }
                if extra:
                    record.update(extra)
                return record
        
            # ─── 工具 ───────────────────────────────────────────────────────────────────
            @staticmethod
            def _to_int(v):
                try:
                    return int(v or 0)
                except (ValueError, TypeError):
                    return 0
        
            @staticmethod
            def _fmt_ts(ts):
                try:
                    ts = int(ts)
                    if ts > 10**12:  # 毫秒级时间戳兼容
                        ts = ts // 1000
                    return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
                except (ValueError, TypeError, OSError):
                    return ""
        
            @staticmethod
            def _parse_ts(value):
                """兼容秒/毫秒时间戳、ISO 时间字符串与相对时间(如 "1 day ago"),返回秒级 int"""
                if value is None:
                    return 0
                if isinstance(value, (int, float)):
                    ts = int(value)
                    return ts // 1000 if ts > 10**12 else ts
                s = str(value).strip()
                if s.isdigit():
                    ts = int(s)
                    return ts // 1000 if ts > 10**12 else ts
                for fmt in ("%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%d %H:%M:%S", "%a %b %d %H:%M:%S %z %Y"):
                    try:
                        return int(datetime.strptime(s.replace("Z", "+0000"), fmt).timestamp())
                    except (ValueError, TypeError):
                        continue
                return BaseSource._parse_relative_ts(s)
        
            #: 相对时间单位 → 秒数(月/年为近似值,概要场景足够)
            _REL_UNITS = {
                "second": 1, "minute": 60, "hour": 3600, "day": 86400,
                "week": 604800, "month": 2592000, "year": 31536000,
            }
            _REL_UNITS_ZH = {
                "秒": 1, "分钟": 60, "小时": 3600, "天": 86400,
                "周": 604800, "个月": 2592000, "月": 2592000, "年": 31536000,
            }
        
            @staticmethod
            def _parse_relative_ts(s):
                """解析相对时间:"3 hours ago" / "Streamed 1 day ago" / "2 天前" 等"""
                m = re.search(r"(\d+)\s*(second|minute|hour|day|week|month|year)s?\s*ago", s, re.I)
                if m:
                    secs = int(m.group(1)) * BaseSource._REL_UNITS[m.group(2).lower()]
                    return int((datetime.now() - timedelta(seconds=secs)).timestamp())
                m = re.search(r"(\d+)\s*(个月|分钟|小时|秒|天|周|月|年)前", s)
                if m:
                    secs = int(m.group(1)) * BaseSource._REL_UNITS_ZH[m.group(2)]
                    return int((datetime.now() - timedelta(seconds=secs)).timestamp())
                return 0
        
            # ─── 稳健请求(递增延迟重试)────────────────────────────────────────────────
            def post_json(self, session, url, payload, max_retries=3, timeout=30):
                """
                POST JSON,失败(网络异常/非 JSON)递增延迟重试。
                返回解析后的 dict;彻底失败返回 None。
                """
                for attempt in range(max_retries):
                    try:
                        resp = session.post(url, json=payload, timeout=timeout)
                        return resp.json()
                    except Exception:
                        if attempt < max_retries - 1:
                            time.sleep(0.5 * (attempt + 1))
                return None
        
      • tiktok_source.py 2.3 KB
        #!/usr/bin/env python3
        """
        TikTok 平台适配器
        接口已实测(2026-07):
          searchVideo  POST {"keyword": "..."}  → 20 条/次,自带点赞/评论/播放/发布时间
          awemeDetail  POST {"awemeId": "..."}  → 单条详情(日常概要无需调用)
          userAwemeList POST {"secUserId": "..."} → 作者作品列表(备用)
        """
        
        import sys
        from pathlib import Path
        
        sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
        
        from config import TIKTOK_SEARCH_API, SOURCE, SUCCESS_CODES
        from .base import BaseSource, PlatformUnavailable
        
        
        class TikTokSource(BaseSource):
            platform = "tiktok"
            display_name = "TikTok"
        
            def search(self, session, keyword):
                result = self.post_json(session, TIKTOK_SEARCH_API,
                                        {"keyword": keyword, "source": SOURCE})
                if result is None:
                    raise PlatformUnavailable("TikTok 搜索请求失败(网络异常)")
        
                code = result.get("code")
                if code not in SUCCESS_CODES:
                    msg = result.get("msg", "")
                    if code == 3203:
                        raise PlatformUnavailable(f"TikTok 上游能力故障: {msg}")
                    raise PlatformUnavailable(f"TikTok 搜索接口错误 (code {code}): {msg}")
        
                data = result.get("data") or []
                if not isinstance(data, list):
                    return []
        
                records = []
                for item in data:
                    stats = item.get("statsData") or {}
                    author = item.get("authorData") or {}
                    record = self.make_record(
                        title=item.get("content") or "",
                        url=item.get("shareLink") or "",
                        author=author.get("userName") or author.get("userHandle") or "",
                        likes=stats.get("likeCount"),
                        comments=stats.get("commentTotal"),
                        views=stats.get("viewCount"),
                        publish_ts=self._parse_ts(item.get("publishTime")),
                        keyword=keyword,
                        extra={
                            "shares": self._to_int(stats.get("shareTotal")),
                            "work_id": str(item.get("workId") or ""),
                            "area": item.get("area") or "",
                            "cover": (item.get("videoData") or {}).get("coverImage") or "",
                        },
                    )
                    records.append(record)
                return records
        
      • x_source.py 2.7 KB
        #!/usr/bin/env python3
        """
        X (Twitter) 平台适配器
        接口已实测(2026-07):
          search  POST {"keyword": "...", "searchType": "Top", "cursor": ""}
                  → data.tweets[](20条/页)+ nextCursor 分页
                  单条自带 likeCount/replyCount/retweetCount/viewCount/createdAt
          tweetDetail / tweetComments:备用(评论内容分析场景)
        注意:searchType 为必填参数(Top=热门 / Latest=最新),缺失会报 3203 上游错误。
        """
        
        import sys
        from pathlib import Path
        
        sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
        
        from config import X_SEARCH_API, SOURCE, SUCCESS_CODES
        from .base import BaseSource, PlatformUnavailable
        
        
        class XSource(BaseSource):
            platform = "x"
            display_name = "X"
            #: Top=热门推文(互动量高)/ Latest=最新推文
            search_type = "Top"
        
            def search(self, session, keyword):
                payload = {
                    "keyword": keyword,
                    "searchType": self.search_type,
                    "cursor": "",
                    "source": SOURCE,
                }
                result = self.post_json(session, X_SEARCH_API, payload)
                if result is None:
                    raise PlatformUnavailable("X 搜索请求失败(网络异常)")
        
                code = result.get("code")
                if code not in SUCCESS_CODES:
                    msg = result.get("msg", "")
                    if code == 3203:
                        raise PlatformUnavailable(f"X 上游能力故障(RedFox 侧): {msg}")
                    raise PlatformUnavailable(f"X 搜索接口错误 (code {code}): {msg}")
        
                data = result.get("data") or {}
                tweets = data.get("tweets") if isinstance(data, dict) else None
                if not isinstance(tweets, list):
                    return []
        
                return [self._normalize_item(t, keyword) for t in tweets]
        
            def _normalize_item(self, item, keyword):
                user = item.get("user") or {}
                username = item.get("username") or user.get("username") or ""
                tweet_id = str(item.get("tweetId") or "")
                url = (f"https://x.com/{username}/status/{tweet_id}"
                       if username and tweet_id else "")
        
                return self.make_record(
                    title=item.get("text") or "",
                    url=url,
                    author=user.get("displayName") or username,
                    likes=item.get("likeCount"),
                    comments=item.get("replyCount"),
                    views=item.get("viewCount"),
                    publish_ts=self._parse_ts(item.get("createdAt")),
                    keyword=keyword,
                    extra={
                        "shares": self._to_int(item.get("retweetCount")),
                        "work_id": tweet_id,
                        "language": item.get("language") or "",
                        "followers": self._to_int(user.get("followers")),
                    },
                )
        
      • youtube_source.py 6.4 KB
        #!/usr/bin/env python3
        """
        YouTube 平台适配器
        接口已实测(2026-07):
          searchVideo   POST {"searchQuery": "..."}
              → data.videos[](20条/次):title / videoId / author / channelId /
                viewCount(原始整数)/ duration / publishedTime(相对时间,如 "1 day ago")/ thumbnails[]
          videoDetail   POST {"videoId": "..."}
              → likeCount / commentCount(本地化展示串,如 "1928万"、"244万")、
                date(中文日期,如 "2026年7月24日")、videoUrl / channelHandle / description
          videoComments POST {"videoId": "..."}
              → data.comments[] + continuationToken(评论内容分析场景备用)
        注意:searchVideo 不含点赞/评论数,需对候选视频调 videoDetail 补全;
              为控制积分消耗,默认只对 viewCount 前 detail_top 条补详情,其余回退为 0。
        """
        
        import re
        import sys
        import time
        from datetime import datetime
        from pathlib import Path
        
        sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
        
        from config import YOUTUBE_SEARCH_API, YOUTUBE_DETAIL_API, SOURCE, SUCCESS_CODES
        from .base import BaseSource, PlatformUnavailable
        
        
        class YouTubeSource(BaseSource):
            platform = "youtube"
            display_name = "YouTube"
            #: 对 viewCount 前 N 条调详情补点赞/评论。searchVideo 每次返回 20 条,
            #: 取 20 即全量补全——热文按点赞排序,漏拉详情会导致高赞视频排不上来
            detail_top = 20
        
            def search(self, session, keyword):
                result = self.post_json(session, YOUTUBE_SEARCH_API,
                                        {"searchQuery": keyword, "source": SOURCE})
                if result is None:
                    raise PlatformUnavailable("YouTube 搜索请求失败(网络异常)")
        
                code = result.get("code")
                if code not in SUCCESS_CODES:
                    msg = result.get("msg", "")
                    if code == 3203:
                        raise PlatformUnavailable(f"YouTube 上游能力故障: {msg}")
                    raise PlatformUnavailable(f"YouTube 搜索接口错误 (code {code}): {msg}")
        
                data = result.get("data") or {}
                videos = data.get("videos") if isinstance(data, dict) else None
                if not isinstance(videos, list):
                    return []
        
                # 按播放量排序,只对前 N 条调详情(点赞/评论数仅详情接口提供)
                videos = [v for v in videos if isinstance(v, dict)]
                videos.sort(key=lambda v: self._to_int(v.get("viewCount")), reverse=True)
                details = {}
                for v in videos[:self.detail_top]:
                    vid = str(v.get("videoId") or "")
                    if not vid:
                        continue
                    detail = self._fetch_detail(session, vid)
                    if detail:
                        details[vid] = detail
                    time.sleep(0.2)
        
                return [self._normalize_item(v, keyword, details.get(str(v.get("videoId") or "")))
                        for v in videos]
        
            def _fetch_detail(self, session, video_id):
                result = self.post_json(session, YOUTUBE_DETAIL_API, {"videoId": video_id})
                if result and result.get("code") in SUCCESS_CODES:
                    data = result.get("data")
                    if isinstance(data, dict) and data:
                        return data
                return None
        
            def _normalize_item(self, item, keyword, detail=None):
                detail = detail or {}
                vid = str(item.get("videoId") or "")
                cover = ""
                thumbs = item.get("thumbnails")
                if isinstance(thumbs, list) and thumbs:
                    best = max(thumbs,
                               key=lambda t: int(t.get("width") or 0) * int(t.get("height") or 0))
                    cover = best.get("url") or ""
        
                # 发布时间:优先详情精确日期("2026年7月24日"),回退列表相对时间("1 day ago")
                publish_ts = (self._parse_cn_date(detail.get("date"))
                              or self._parse_ts(item.get("publishedTime")))
        
                return self.make_record(
                    title=item.get("title") or detail.get("title") or "",
                    url=detail.get("videoUrl") or (f"https://www.youtube.com/watch?v={vid}" if vid else ""),
                    author=item.get("author") or detail.get("author") or "",
                    likes=self._parse_display_count(detail.get("likeCount")),
                    comments=self._parse_display_count(detail.get("commentCount")),
                    views=self._to_int(item.get("viewCount")) or self._parse_display_count(detail.get("viewCount")),
                    publish_ts=publish_ts,
                    keyword=keyword,
                    extra={
                        "shares": 0,
                        "work_id": vid,
                        "cover": cover,
                        "duration": item.get("duration") or "",
                        "channel_id": item.get("channelId") or "",
                        "channel_handle": detail.get("channelHandle") or "",
                    },
                )
        
            # ─── 本地化展示串解析 ────────────────────────────────────────────────────────
            @staticmethod
            def _parse_display_count(value):
                """
                解析 YouTube 本地化计数字符串 → int
                兼容:"1928万" / "1,797,749,387次观看" / "244万" / "1.9M" / "24K" / 原始整数
                """
                if value is None:
                    return 0
                if isinstance(value, (int, float)):
                    return int(value)
                text = str(value).strip().replace(",", "").replace(" ", "")
                m = re.match(r"^([\d.]+)(.*)$", text)
                if not m:
                    return 0
                try:
                    num = float(m.group(1))
                except ValueError:
                    return 0
                suffix = m.group(2).upper()
                if "亿" in suffix:
                    mult = 100_000_000
                elif "万" in suffix:
                    mult = 10_000
                elif "千" in suffix:
                    mult = 1_000
                elif suffix.startswith("B"):
                    mult = 1_000_000_000
                elif suffix.startswith("M"):
                    mult = 1_000_000
                elif suffix.startswith("K"):
                    mult = 1_000
                else:
                    mult = 1
                return int(num * mult)
        
            @staticmethod
            def _parse_cn_date(value):
                """中文日期 "2026年7月24日" → 秒级时间戳"""
                if not value:
                    return 0
                m = re.match(r"^(\d{4})年(\d{1,2})月(\d{1,2})日$", str(value).strip())
                if not m:
                    return 0
                try:
                    return int(datetime(int(m.group(1)), int(m.group(2)), int(m.group(3))).timestamp())
                except ValueError:
                    return 0
        
      • __init__.py 304 B
        from .base import BaseSource, PlatformUnavailable
        from .tiktok_source import TikTokSource
        from .x_source import XSource
        from .youtube_source import YouTubeSource
        
        # 平台注册表:新增平台只需在此登记
        SOURCES = {
            "x": XSource,
            "tiktok": TikTokSource,
            "youtube": YouTubeSource,
        }
        
    • config.py 3.2 KB
      #!/usr/bin/env python3
      """
      overseas-trending-search — 统一配置与密钥加载
      密钥加载优先级:--api-key 参数 > REDFOX_API_KEY 环境变量 > ~/.qoder/apis/redfox.json
      """
      
      import json
      import os
      from pathlib import Path
      
      # ─── RedFox 网关 ──────────────────────────────────────────────────────────────────
      BASE_URL = "https://redfox.hk/story/api"
      
      X_SEARCH_API = f"{BASE_URL}/x/search"
      X_DETAIL_API = f"{BASE_URL}/x/tweetDetail"
      X_COMMENTS_API = f"{BASE_URL}/x/tweetComments"
      
      TIKTOK_SEARCH_API = f"{BASE_URL}/tiktok/ability/searchVideo"
      TIKTOK_DETAIL_API = f"{BASE_URL}/tiktok/ability/awemeDetail"
      TIKTOK_USER_AWEME_API = f"{BASE_URL}/tiktok/ability/userAwemeList"
      
      # YouTube 接口(2026-07 实测):searchVideo 列表 + videoDetail 补点赞/评论
      YOUTUBE_SEARCH_API = f"{BASE_URL}/youtube/searchVideo"
      YOUTUBE_DETAIL_API = f"{BASE_URL}/youtube/videoDetail"
      YOUTUBE_COMMENTS_API = f"{BASE_URL}/youtube/videoComments"
      
      # ─── 密钥与渠道 ────────────────────────────────────────────────────────────────────
      CONFIG_FILE = Path.home() / ".qoder" / "apis" / "redfox.json"
      ENV_KEY = "REDFOX_API_KEY"
      SOURCE = "ChinaTrendingDigest-RedSkill"
      
      DEFAULT_OUTPUT_DIR = Path.home() / "Downloads" / "QoderOverseasTrending"
      SUCCESS_CODES = (200, 2000)
      
      
      def get_api_key(cli_key=None):
          """按优先级加载 RedFox API Key,找不到返回 None"""
          if cli_key:
              return cli_key
          env_key = os.environ.get(ENV_KEY)
          if env_key:
              return env_key
          if CONFIG_FILE.exists():
              try:
                  data = json.loads(CONFIG_FILE.read_text())
                  key = data.get("api_key")
                  if key:
                      return key
              except (json.JSONDecodeError, OSError):
                  pass
          return None
      
      
      def make_session(api_key):
          """构造带认证头的 requests.Session(RedFox 网关统一 X-API-KEY 认证)"""
          import requests
          session = requests.Session()
          session.headers.update({
              "Content-Type": "application/json",
              "X-API-KEY": api_key,
          })
          return session
      
      
      def print_no_key_guide():
          print("╔══════════════════════════════════════════════════════╗")
          print("║  未配置 API Key,请通过以下方式之一配置:            ║")
          print("║                                                      ║")
          print("║  export REDFOX_API_KEY=ak_你的密钥                   ║")
          print("║  python3 digest.py --api-key ak_你的密钥              ║")
          print("║  echo '{\"api_key\":\"ak_你的密钥\"}' > ~/.qoder/apis/redfox.json ║")
          print("║                                                      ║")
          print("║  注册获取 Key: https://redfox.hk/settings/api-keys   ║")
          print("╚══════════════════════════════════════════════════════╝")
      
    • digest.py 18.7 KB
      #!/usr/bin/env python3
      """
      海外跨平台热文搜索 — 主编排器
      ==============================
      按用户输入的关键词(中文/英文,逗号分隔多词),搜索 X / TikTok / YouTube
      热门内容,每平台取 Top N,归一化为统一列表(平台/标题/作者/播放/点赞/评论/发布时间/链接),
      输出终端分组表格 + CSV + 交互式 HTML 报告。
      
      Usage:
          python3 digest.py "AI"
          python3 digest.py "人工智能,AI agent" --days 3
          python3 digest.py "AI" --platforms tiktok --sort time
          python3 digest.py "AI" --sort views --top 5
          python3 digest.py "AI" --csv-only --no-open
      """
      
      import argparse
      import csv
      import json
      import os
      import subprocess
      import sys
      import time
      from datetime import datetime, timedelta
      from pathlib import Path
      
      sys.path.insert(0, str(Path(__file__).resolve().parent))
      
      from config import (DEFAULT_OUTPUT_DIR, get_api_key, make_session,
                          print_no_key_guide)
      from sources import SOURCES
      from sources.base import PlatformUnavailable
      
      # ─── 终端颜色 ──────────────────────────────────────────────────────────────────────
      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}")
      
      
      def format_number(n):
          n = int(n or 0)
          if n >= 10000:
              return f"{n/10000:.1f}w"
          if n >= 1000:
              return f"{n/1000:.1f}k"
          return str(n)
      
      
      # ─── 语言识别与优先分层 ──────────────────────────────────────────────────────────
      def detect_lang(text):
          """轻量语言识别(无第三方依赖):
          zh=含汉字且无假名(简/繁中文),ja=含假名,ko=含谚文,en=含拉丁字母,other=其余。
          中英混排(如「中文标题 with English」)按 zh 计。"""
          has_han = has_kana = has_hangul = has_latin = False
          for ch in text or "":
              o = ord(ch)
              if 0x4E00 <= o <= 0x9FFF or 0x3400 <= o <= 0x4DBF:
                  has_han = True
              elif 0x3040 <= o <= 0x30FF:
                  has_kana = True
              elif 0xAC00 <= o <= 0xD7AF:
                  has_hangul = True
              elif o < 0x80 and ch.isalpha():
                  has_latin = True
          if has_han:
              return "ja" if has_kana else "zh"
          if has_kana:
              return "ja"
          if has_hangul:
              return "ko"
          if has_latin:
              return "en"
          return "other"
      
      
      def keyword_pref_lang(keywords):
          """由关键词推断优先语言:含汉字→zh,纯拉丁→en,日文/韩文词同理;无法识别→None(不分层)。"""
          lang = detect_lang(" ".join(keywords))
          return lang if lang in ("zh", "en", "ja", "ko") else None
      
      
      def lang_tier(lang, pref):
          """语言分层:0=关键词同语言,1=中文/英文,2=其他语言。pref 为 None 时不分层。"""
          if not pref or lang == pref:
              return 0
          if lang in ("zh", "en"):
              return 1
          return 2
      
      
      # ─── 采集流程 ────────────────────────────────────────────────────────────────────
      def collect(session, keywords, platforms):
          """
          平台 × 关键词 循环采集。
          返回 (records, platform_status) — platform_status: {platform: "ok"/错误信息}
          """
          records = []
          seen = set()
          platform_status = {}
      
          for pf in platforms:
              source_cls = SOURCES.get(pf)
              if not source_cls:
                  warn(f"未知平台「{pf}」,已跳过(可选: {', '.join(SOURCES)})")
                  continue
              source = source_cls()
              pf_ok = False
      
              for kw in keywords:
                  step(f"[{source.display_name}] 搜索「{kw}」...")
                  try:
                      items = source.search(session, kw)
                  except PlatformUnavailable as e:
                      warn(f"[{source.display_name}] 不可用,已跳过: {e}")
                      platform_status[pf] = str(e)
                      break
                  except Exception as e:
                      warn(f"[{source.display_name}] 关键词「{kw}」搜索异常: {e}")
                      continue
      
                  added = 0
                  for r in items:
                      dedup_key = r.get("url") or f"{pf}:{r.get('work_id')}"
                      if dedup_key and dedup_key not in seen:
                          seen.add(dedup_key)
                          r["lang"] = detect_lang(r.get("title") or "")
                          records.append(r)
                          added += 1
                  step(f"  +{added} 条(全局 {len(records)} 条)")
                  pf_ok = True
                  time.sleep(0.3)
      
              if pf_ok:
                  platform_status[pf] = "ok"
      
          return records, platform_status
      
      
      def filter_by_days(records, days):
          """按时间窗口过滤(publish_ts 缺失或超窗的记录排除,按平台统计提示)"""
          if days <= 0:
              return records
          cutoff = (datetime.now() - timedelta(days=days)).timestamp()
          kept, dropped = [], []
          for r in records:
              ts = int(r.get("publish_ts") or 0)
              if ts and ts >= cutoff:
                  kept.append(r)
              else:
                  dropped.append(r)
          if dropped:
              drop_cnt, keep_cnt = {}, {}
              for r in dropped:
                  pf = r.get("platform") or "?"
                  drop_cnt[pf] = drop_cnt.get(pf, 0) + 1
              for r in kept:
                  pf = r.get("platform") or "?"
                  keep_cnt[pf] = keep_cnt.get(pf, 0) + 1
      
              def _name(pf):
                  return SOURCES[pf].display_name if pf in SOURCES else pf
      
              detail = ",".join(f"{_name(pf)} {n} 条" for pf, n in drop_cnt.items())
              warn(f"{len(dropped)} 条发布时间缺失或超出最近 {days} 天窗口,已排除({detail})")
              for pf, n in drop_cnt.items():
                  if not keep_cnt.get(pf):
                      warn(f"{_name(pf)} {n} 条均为窗口外的历史热门内容,如需查看请加 --days 0")
          return kept
      
      
      SORT_LABELS = {"likes": "点赞数", "views": "播放数",
                     "comments": "评论数", "time": "发布时间"}
      
      
      def _sort_key(r, sort_by):
          """排序键:主指标降序,播放数/发布时间作次序兜底。
          某平台主指标全为 0 时(如 YouTube 无点赞数)自动回退到播放数排序。"""
          views = int(r.get("views") or 0)
          likes = int(r.get("likes") or 0)
          comments = int(r.get("comments") or 0)
          ts = int(r.get("publish_ts") or 0)
          if sort_by == "time":
              return (ts, views, likes)
          if sort_by == "views":
              return (views, likes, ts)
          if sort_by == "comments":
              return (comments, views, likes, ts)
          return (likes, views, ts)  # likes
      
      
      def sort_records(records, sort_by, pref_lang=None):
          """先按指标降序,再按语言分层稳定排序:关键词同语言 > 中/英文 > 其他语言。"""
          records.sort(key=lambda r: _sort_key(r, sort_by), reverse=True)
          if pref_lang:
              records.sort(key=lambda r: lang_tier(r.get("lang"), pref_lang))
          return records
      
      
      def limit_per_platform(records, top, sort_by, pref_lang=None):
          """按平台分组 → 组内排序(语言分层 + 指标降序)→ 每组截取前 N(0=不限)→ 按平台顺序合并"""
          groups, order = {}, []
          for r in records:
              pf = r.get("platform") or "?"
              if pf not in groups:
                  groups[pf] = []
                  order.append(pf)
              groups[pf].append(r)
          merged = []
          for pf in order:
              g = sort_records(groups[pf], sort_by, pref_lang)
              merged.extend(g if top <= 0 else g[:top])
          return merged
      
      
      # ─── 终端表格 ──────────────────────────────────────────────────────────────────────
      def print_terminal_table(records, keywords, platform_status, sort_by, top, pref_lang=None):
          kw_label = " + ".join(keywords)
          sort_label = SORT_LABELS.get(sort_by, sort_by)
          top_label = f"每平台 Top {top}" if top > 0 else "不限条数"
          lang_label = {"zh": "中文优先", "en": "English 优先"}.get(pref_lang)
          lang_suffix = f" · {lang_label}" if lang_label else ""
      
          print(f"\n{BOLD}{'=' * 128}{RESET}")
          print(f"{BOLD}  海外跨平台热文搜索 · 「{kw_label}」· 共 {len(records)} 条 · {top_label} · 按{sort_label}降序{lang_suffix}{RESET}")
          print(f"{BOLD}{'=' * 128}{RESET}")
      
          status_parts = []
          for pf, st in platform_status.items():
              name = SOURCES[pf].display_name if pf in SOURCES else pf
              status_parts.append(f"{name}: {'✓' if st == 'ok' else '✗ ' + st}")
          if status_parts:
              print(f"  {CYAN}平台状态:{' | '.join(status_parts)}{RESET}")
      
          if not records:
              print(f"  {YELLOW}无结果。可尝试放宽时间窗口(--days 0 不限)或更换关键词{RESET}\n")
              return
      
          # 按平台分组展示(records 已按平台分组排好序)
          groups, order = {}, []
          for r in records:
              pf = r.get("platform") or "?"
              if pf not in groups:
                  groups[pf] = []
                  order.append(pf)
              groups[pf].append(r)
      
          header = (f"  {'序号':<4}{'平台':<8}{'标题':<24}{'作者':<14}"
                    f"{'播放':>7}{'点赞':>7}{'评论':>7}{'发布时间':<18}{'链接':<30}")
          idx = 0
          for pf in order:
              g = groups[pf]
              name = g[0].get("platform_name") or pf
              print(f"\n  {CYAN}{BOLD}▎{name}({len(g)} 条 · 按{sort_label}){RESET}")
              print(f"  {YELLOW}{'─' * 110}{RESET}")
              print(f"  {YELLOW}{header}{RESET}")
              print(f"  {YELLOW}{'─' * 110}{RESET}")
              for r in g:
                  idx += 1
                  pf_name = (r.get("platform_name") or r.get("platform") or "")[:6]
                  title = r.get("title", "")
                  title = (title[:20] + "…") if len(title) > 20 else title
                  author = r.get("author", "")
                  author = (author[:11] + "..") if len(author) > 13 else author
                  views = format_number(r.get("views"))
                  likes = format_number(r.get("likes")) if int(r.get("likes") or 0) else "-"
                  comments = format_number(r.get("comments")) if int(r.get("comments") or 0) else "-"
                  pub = (r.get("publish_time") or "")[:16]
                  url = r.get("url", "")
                  url = (url[:27] + "..") if len(url) > 29 else url
                  print(f"  {idx:<4}{pf_name:<8}{title:<24}{author:<14}"
                        f"{views:>7}{likes:>7}{comments:>7}{pub:<18}{url:<30}")
              print(f"  {YELLOW}{'─' * 110}{RESET}")
          print()
      
      
      # ─── CSV 导出 ──────────────────────────────────────────────────────────────────────
      def export_csv(records, keyword, output_dir):
          output_dir = Path(output_dir)
          output_dir.mkdir(parents=True, exist_ok=True)
          date_str = datetime.now().strftime("%Y-%m-%d")
          filepath = output_dir / f"海外热文_{keyword}_{date_str}.csv"
      
          fieldnames = ["平台", "标题", "作者", "播放/阅读数", "点赞数", "评论数",
                        "分享数", "发布时间", "链接", "命中关键词"]
          with open(filepath, "w", newline="", encoding="utf-8-sig") as f:
              writer = csv.DictWriter(f, fieldnames=fieldnames)
              writer.writeheader()
              for r in records:
                  writer.writerow({
                      "平台": r.get("platform_name", ""),
                      "标题": r.get("title", ""),
                      "作者": r.get("author", ""),
                      "播放/阅读数": r.get("views", 0),
                      "点赞数": r.get("likes", 0),
                      "评论数": r.get("comments", 0),
                      "分享数": r.get("shares", 0),
                      "发布时间": r.get("publish_time", ""),
                      "链接": r.get("url", ""),
                      "命中关键词": r.get("keyword", ""),
                  })
          return filepath
      
      
      # ─── HTML 报告 ────────────────────────────────────────────────────────────────────
      def generate_html(records, keyword, platform_status):
          template_path = Path(__file__).parent.parent / "assets" / "report_template.html"
          template = template_path.read_text(encoding="utf-8")
      
          status_label = " | ".join(
              f"{SOURCES[pf].display_name if pf in SOURCES else pf}: "
              f"{'正常' if st == 'ok' else st}"
              for pf, st in platform_status.items()
          )
          html = template
          html = html.replace("{{KEYWORD}}", keyword)
          html = html.replace("{{DATE}}", datetime.now().strftime("%Y-%m-%d"))
          html = html.replace("{{TIMESTAMP}}", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
          html = html.replace("{{TOTAL_COUNT}}", str(len(records)))
          html = html.replace("{{PLATFORM_STATUS}}", status_label)
          html = html.replace("{{INITIAL_DATA}}", json.dumps(records, ensure_ascii=False))
          return html
      
      
      # ─── 主流程 ────────────────────────────────────────────────────────────────────────
      def main():
          parser = argparse.ArgumentParser(
              description="海外跨平台热文搜索 — X / TikTok / YouTube",
              formatter_class=argparse.RawDescriptionHelpFormatter,
              epilog="""
      Examples:
        python3 digest.py "AI"
        python3 digest.py "人工智能,AI agent" --days 3
        python3 digest.py "AI" --platforms tiktok --sort time
        python3 digest.py "AI" --sort views --top 5
        python3 digest.py "AI" --csv-only --no-open
              """,
          )
          parser.add_argument("keywords", nargs="?", default="",
                              help="搜索关键词,中文/英文均可,多词用英文逗号分隔")
          parser.add_argument("--days", type=int, default=1,
                              help="时间窗口:最近 N 天(默认 1,0=不限)")
          parser.add_argument("--platforms", default="x,tiktok,youtube",
                              help="平台列表,逗号分隔(默认 x,tiktok,youtube)")
          parser.add_argument("--sort", default="views",
                              choices=["likes", "views", "comments", "time"],
                              help="排序: views(默认) / likes / comments / time")
          parser.add_argument("--top", type=int, default=5,
                              help="每平台最多返回条数(默认 5,0=不限)")
          parser.add_argument("--output-dir", help="输出目录(默认 ~/Downloads/QoderChinaDigest)")
          parser.add_argument("--api-key", help="RedFox API Key")
          parser.add_argument("--csv-only", action="store_true", help="仅生成 CSV")
          parser.add_argument("--no-open", action="store_true", help="不自动打开浏览器")
      
          args = parser.parse_args()
      
          banner = f"""{CYAN}{BOLD}
        ╔══════════════════════════════════════════════╗
        ║        海外跨平台热文搜索                      ║
        ║        Overseas Trending Search               ║
        ║        X / TikTok / YouTube · 每日热文一览     ║
        ╚══════════════════════════════════════════════╝{RESET}
      """
          print(banner)
      
          try:
              import requests  # noqa: F401
          except ImportError:
              error("缺少 requests 库,请安装: pip3 install requests")
              sys.exit(1)
      
          api_key = get_api_key(cli_key=args.api_key)
          if not api_key:
              print_no_key_guide()
              sys.exit(1)
      
          if not args.keywords:
              try:
                  args.keywords = input(f"{CYAN}请输入关键词(多词用逗号分隔): {RESET}").strip()
              except (EOFError, KeyboardInterrupt):
                  print()
                  sys.exit(0)
          keywords = [k.strip() for k in args.keywords.split(",") if k.strip()]
          if not keywords:
              error("关键词不能为空")
              sys.exit(1)
      
          platforms = [p.strip().lower() for p in args.platforms.split(",") if p.strip()]
          output_dir = os.path.expanduser(args.output_dir or str(DEFAULT_OUTPUT_DIR))
      
          session = make_session(api_key)
      
          # ── 采集 ──
          records, platform_status = collect(session, keywords, platforms)
      
          # ── 语言优先分层(中文词→中文优先,英文词→英文优先,其余语言兜底)──
          pref_lang = keyword_pref_lang(keywords)
      
          # ── 时间过滤 + 每平台 Top N ──
          records = filter_by_days(records, args.days)
          records = limit_per_platform(records, args.top, args.sort, pref_lang)
      
          # ── 终端表格 ──
          print_terminal_table(records, keywords, platform_status, args.sort, args.top, pref_lang)
      
          if not records:
              sys.exit(0)
      
          # ── 统计 ──
          total_likes = sum(int(r.get("likes") or 0) for r in records)
          pf_counter = {}
          for r in records:
              pf_counter[r.get("platform_name", "?")] = pf_counter.get(r.get("platform_name", "?"), 0) + 1
          pf_dist = " | ".join(f"{k} {v}" for k, v in pf_counter.items())
          print(f"  {BOLD}统计:{RESET} 共 {len(records)} 条 | {pf_dist} | 总点赞 {format_number(total_likes)}")
      
          # ── CSV ──
          main_kw = keywords[0]
          csv_path = export_csv(records, main_kw, output_dir)
          info(f"CSV 已保存: {csv_path}")
      
          # ── HTML ──
          if not args.csv_only:
              step("生成 HTML 报告 ...")
              html_content = generate_html(records, " + ".join(keywords), platform_status)
              html_path = Path(output_dir) / f"海外热文_{main_kw}_{datetime.now().strftime('%Y-%m-%d')}.html"
              Path(output_dir).mkdir(parents=True, exist_ok=True)
              html_path.write_text(html_content, encoding="utf-8")
              info(f"HTML 报告已保存: {html_path}")
      
              if not args.no_open:
                  step("打开浏览器...")
                  try:
                      subprocess.run(["open", str(html_path)], check=True)
                  except Exception:
                      print(f"  请手动打开: {html_path}")
      
          print(f"\n{GREEN}╔══════════════════════════════════════════════════╗{RESET}")
          print(f"{GREEN}║  ✓ 热文搜索完成!                                  ║{RESET}")
          print(f"{GREEN}╚══════════════════════════════════════════════════╝{RESET}\n")
      
      
      if __name__ == "__main__":
          main()
      
  • README.en.md 4 KB
    # Overseas Trending Search / overseas-trending-search
    
    ---
    
    ## Overview
    
    Overseas Trending Search is a cross-platform trend discovery tool. Enter any keyword (Chinese or English) to instantly search trending content across X, TikTok, and YouTube. The tool intelligently detects the keyword language and prioritizes same-language results, delivering a unified field list and interactive HTML report.
    
    **Core Value**
    
    - 🔍 **Three-Platform Coverage**: Search trending content across X, TikTok, and YouTube in one click
    - 🌐 **Smart Language Prioritization**: Chinese keywords surface Chinese content first, English keywords surface English content first, resolving cross-language ambiguity
    - 📊 **Multi-Format Output**: Grouped terminal table + CSV export + interactive HTML report (card/table views)
    - ⚡ **Flexible Sorting**: Sort by views/likes/comments/publish time, with independent Top N per platform
    
    **Target Users**
    
    - ✍️ Content Creators — Daily trend tracking, topic inspiration and material collection
    - 📈 Marketing Operators — Cross-platform content comparison, understanding content style differences across platforms
    - 🏢 Brand Owners — Competitive sentiment monitoring, tracking real overseas user discussions
    - 📊 Data Analysts — Topic/event tracking, quantifying cross-platform reach
    
    ---
    
    ## Features
    
    ### Core Capabilities
    
    - 🔍 **Three-Platform Search**: Keyword search across X, TikTok, and YouTube
    - 🌐 **Smart Language Prioritization**: Chinese keywords prioritize Chinese content, English keywords prioritize English content, other languages as fallback
    - 📋 **Unified Field List**: Platform, title, author, views, likes, comments, publish time, link — consistent across platforms
    - 📊 **Multi-Dimensional Sorting**: Sort by views/likes/comments/publish time, with independent Top N per platform
    - 📄 **Multi-Format Output**: Grouped terminal table, CSV data export, interactive HTML report (card/table views)
    - 🔗 **Multi-Keyword Support**: Comma-separated keywords, search multiple terms simultaneously
    - 🔄 **Fault Tolerance**: Gracefully skip platforms with API errors, no impact on other platform results
    
    ---
    
    ## Usage Guide
    
    Describe your needs in natural language — no commands to memorize.
    
    ### Quick Reference
    
    | Intent | Example | Result |
    |--------|---------|--------|
    | Trend search | "Search AI trending content on overseas platforms" | 3-platform search + terminal table + CSV + HTML |
    | Specific platform | "Show me trending AI videos on TikTok" | Search selected platform only |
    | Cross-platform comparison | "Compare iPhone 16 buzz on X and YouTube" | Targeted platform comparison |
    | Multiple keywords | "Search AI, AI agent overseas trending" | Search multiple keywords simultaneously |
    | Competitor monitoring | "Search Tesla discussions overseas" | Brand competitive sentiment tracking |
    | Content collection | "Export AI overseas trending CSV" | Export CSV for content planning |
    
    ### Output Example
    
    After the search completes, you will receive:
    
    > **Terminal Grouped Table** → Top N results grouped by platform, with title, author, views/likes/comments/publish time, and links
    > **CSV Export** → Structured data export for analysis and archival
    > **HTML Report** → Interactive report with card/table view toggle, savable offline
    
    ---
    
    ## Use Cases
    
    | Scenario | Role | Example Query | Benefit |
    |----------|------|---------------|---------|
    | Daily Trend Tracking | Content Creator | "Search AI trending today" | Master today's hottest content across 3 platforms |
    | Cross-Platform Comparison | Marketing Operator | "Compare Vision Pro buzz on X and YouTube" | Understand content style differences across platforms |
    | Topic Tracking | Brand Owner | "Search EV, new energy trends for 7 days" | Track cross-platform topic propagation |
    | Competitor Monitoring | Data Analyst | "Search real Tesla discussions overseas" | See authentic overseas user feedback |
    | Content Collection | Social Media Operator | "Export AI overseas trending CSV" | Export data for content planning |
    
  • README.md 3.7 KB
    # 海外跨平台热文搜索 / overseas-trending-search
    
    ---
    
    ## 简介
    
    海外跨平台热文搜索是一款跨平台热点洞察工具,输入任意关键词(中文/英文),即可一键搜索 X、TikTok、YouTube 三大海外平台的热门内容,智能识别关键词语言并优先展示同语言结果,输出统一字段列表与交互式 HTML 报告。
    
    **核心价值**
    
    - 🔍 **三平台覆盖**:一键搜索 X / TikTok / YouTube 三大海外平台的热门内容
    - 🌐 **智能语言优先**:中文关键词优先中文内容,英文关键词优先英文内容,解决跨语言歧义
    - 📊 **多格式输出**:终端分组表格 + CSV 数据导出 + 交互式 HTML 报告(卡片/表格双视图)
    - ⚡ **灵活排序**:支持按播放数/点赞数/评论数/发布时间自由排序,每平台独立取 Top N
    
    **适用对象**
    
    - ✍️ 内容创作者 — 每日热点追踪,获取选题灵感与素材
    - 📈 市场运营 — 跨平台内容对比,了解不同平台内容形态差异
    - 🏢 品牌方 — 竞品舆情监控,查看海外用户真实讨论
    - 📊 数据分析师 — 话题事件追踪,量化跨平台传播效果
    
    ---
    
    ## 功能特性
    
    ### 核心功能
    
    - 🔍 **三平台搜索**:覆盖 X、TikTok、YouTube 三大海外平台的关键词搜索
    - 🌐 **语言智能优先**:中文关键词各平台优先展示中文内容,英文关键词优先英文内容,其余语言兜底
    - 📋 **统一字段列表**:平台、标题、作者、播放数、点赞数、评论数、发布时间、链接,跨平台一致
    - 📊 **多维度排序**:支持按播放数/点赞数/评论数/发布时间排序,每平台独立取 Top N
    - 📄 **多格式输出**:终端分组表格展示、CSV 数据导出、交互式 HTML 报告(卡片/表格双视图)
    - 🔗 **多关键词支持**:英文逗号分隔多个关键词,一次搜索同时命中
    - 🔄 **错误容忍**:某平台异常时自动跳过,不影响其他平台结果
    
    ---
    
    ## 使用指南
    
    直接用自然语言描述需求,无需记忆命令。
    
    ### 常用说法速查
    
    | 意图 | 示例话术 | 效果 |
    |------|---------|------|
    | 热点搜索 | 「搜索 AI 在海外平台的热门内容」 | 三平台搜索 + 终端表格 + CSV + HTML |
    | 指定平台 | 「帮我看 TikTok 上关于 AI 的热门视频」 | 仅搜索指定平台 |
    | 跨平台对比 | 「对比 X 和 YouTube 上 iPhone 16 的热度」 | 指定平台对比分析 |
    | 多关键词 | 「搜索 人工智能,AI agent 的海外热文」 | 同时搜索多个关键词 |
    | 竞品监控 | 「搜一下 Tesla 在海外的讨论」 | 品牌竞品舆情追踪 |
    | 素材采集 | 「导出 AI 相关的海外热文 CSV」 | 导出 CSV 做选题库 |
    
    ### 输出示例
    
    搜索完成后,你将依次收到:
    
    > **终端分组表格** → 按平台分组展示 Top N 结果,含标题、作者、播放/点赞/评论/发布时间、链接
    > **CSV 导出** → 结构化数据导出,便于数据分析与存档
    > **HTML 报告** → 交互式报告,支持卡片/表格双视图切换,可离线保存
    
    ---
    
    ## 使用场景
    
    | 场景 | 角色 | 示例问法 | 收益 |
    |------|------|---------|------|
    | 每日热点追踪 | 内容创作者 | 「搜索 AI 今天的热门内容」 | 掌握三平台当日最热内容 |
    | 跨平台对比 | 市场运营 | 「对比 X 和 YouTube 上 Vision Pro 的热度」 | 了解不同平台内容形态差异 |
    | 话题事件追踪 | 品牌方 | 「搜 新能源,EV 近 7 天传播情况」 | 追踪特定话题跨平台传播 |
    | 竞品舆情监控 | 数据分析师 | 「搜一下 Tesla 在海外的真实讨论」 | 看海外用户真实反馈 |
    | 素材灵感采集 | 自媒体运营 | 「导出 AI 相关海外热文 CSV」 | 导出数据做选题库 |
    
  • SKILL.md 9.4 KB
    ---
    name: overseas-trending-search
    display_name: 海外跨平台热文搜索
    display_name_en: Overseas Trending Search
    description: 海外跨平台热文搜索 — 输入任意关键词(中文/英文),一次搜索 X / TikTok / YouTube 三平台热门内容,每平台 Top N(默认5)统一列表(平台/标题/作者/播放/点赞/评论/发布时间/链接),终端分组表格 + CSV 导出 + 交互式 HTML 报告(卡片/表格双视图)。按关键词语言智能优先同语言内容(中文词→中文优先,英文词→英文优先,其余语言兜底)。当用户需要搜索海外平台热点、跨平台内容对比、话题/竞品舆情监控、选题素材采集时使用。
    description_zh: 输入关键词(中英文均可),一次搜索 X / TikTok / YouTube 三平台热门内容,每平台 Top N 统一字段列表(平台/标题/作者/播放/点赞/评论/发布时间),中文关键词优先中文内容、英文关键词优先英文内容,终端分组表格 + CSV + 交互式 HTML 报告(卡片/表格双视图),适合热点追踪、跨平台对比与素材采集。
    description_en: "Search any keyword (Chinese or English) across X / TikTok / YouTube and get each platform's Top N trending posts in one unified list (platform/title/author/views/likes/comments/date). Language-aware ranking: a Chinese keyword surfaces Chinese content first, an English keyword surfaces English content first, other languages as fallback. Grouped terminal table + CSV export + interactive HTML report (card/table views) — ideal for trend tracking, cross-platform comparison and content research."
    category: data-analysis
    version: 1.3.0
    author: 红狐数据
    permissions:
      - network
      - filesystem-write
    ---
    
    # 海外跨平台热文搜索
    
    输入任意关键词(中文/英文),一键搜索 X / TikTok / YouTube 三平台热门内容,每平台按点赞/播放/评论/发布时间取 Top N(默认 5),输出统一字段列表:平台、标题、作者、播放数、点赞数、评论数、发布时间、链接。
    
    > API 请求均携带 `ChinaTrendingDigest-RedSkill` 标识。需先配置 API Key,通过环境变量 REDFOX_API_KEY 或 --api-key 参数传入。
    > 架构为平台适配器模式:新平台(如 YouTube)只需新增适配器文件,主流程零改动。
    
    ---
    
    ## 使用场景
    
    | 场景 | 示例 |
    |------|------|
    | **每日热点追踪** | 每天跑一次 "AI",掌握三平台当日最热内容 |
    | **跨平台对比** | 同一关键词对比 X / TikTok / YouTube 的热度与内容形态差异 |
    | **话题事件追踪** | 搜 "新能源,EV" 追踪特定话题跨平台传播 |
    | **竞品/舆情监控** | 搜品牌或产品名,看海外用户的真实讨论与爆款反馈 |
    | **素材灵感采集** | 导出 CSV 做选题库与数据分析 |
    | **账号内容挖掘** | TikTok 侧支持按作者作品列表下钻(userAwemeList) |
    
    ---
    
    ## 使用方法
    
    ```bash
    # 基础用法:单关键词(默认最近 24 小时,按播放数降序)
    python3 "$SKILL_PATH/scripts/digest.py" "AI"
    
    # 中文关键词 / 多关键词(英文逗号分隔)
    python3 "$SKILL_PATH/scripts/digest.py" "人工智能,AI agent"
    
    # 放宽时间窗口到最近 3 天(TikTok 热门多为历史内容,建议放宽)
    python3 "$SKILL_PATH/scripts/digest.py" "AI" --days 3
    
    # 只跑指定平台
    python3 "$SKILL_PATH/scripts/digest.py" "AI" --platforms tiktok
    
    # 按发布时间排序
    python3 "$SKILL_PATH/scripts/digest.py" "AI" --sort time
    
    # 按点赞数排序 / 调整每平台返回条数(默认 5)
    python3 "$SKILL_PATH/scripts/digest.py" "AI" --sort likes --top 10
    
    # 仅导出 CSV / 不自动打开浏览器
    python3 "$SKILL_PATH/scripts/digest.py" "AI" --csv-only --no-open
    ```
    
    CSV / HTML 默认保存在 `~/Downloads/QoderOverseasTrending/`。
    
    ---
    
    ## 返回结果展示规范
    
    向用户展示结果时,**必须**包含以下字段:
    
    | 字段 | 说明 |
    |------|------|
    | 平台 | X / TikTok / YouTube(带平台标识色) |
    | 标题 | 推文/视频文案摘要,仅展示前 20 字(HTML 表格悬停看全文) |
    | 作者 | 作者昵称 |
    | 播放数 | 千/万简写(如 47.3w),展示在作者之后 |
    | 点赞数 | 千/万简写(如 62.9w) |
    | 评论数 | 同上 |
    | 发布时间 | `YYYY-MM-DD HH:mm` |
    | 链接 | 可点击跳转原文 |
    
    平台状态需在结果前明示:某平台上游故障时优雅降级并在报告中标注,不影响其他平台结果。
    
    - 条数约束:每个平台最多返回 `--top` N 条(默认 5),按 `--sort` 指标在平台组内降序;终端与 HTML 均按平台分组展示。
    - 语言优先分层:中文关键词时各平台组内优先展示中文内容、英文关键词优先英文内容,组内中/英文内容不足时以其他语言兜底;层内仍按排序指标降序。
    - HTML 报告支持「卡片 / 表格」双视图切换,表格视图含点赞/评论/播放/分享全字段。
    - YouTube 点赞/评论数由 videoDetail 详情接口逐条补全(默认全量覆盖搜索结果 20 条,本地化计数串已解析为整数)。
    
    ---
    
    ## 参数说明
    
    | 参数 | 说明 | 默认值 |
    |------|------|--------|
    | `keywords` | 关键词,中英文均可,多词英文逗号分隔(位置参数) | — |
    | `--days` | 时间窗口:最近 N 天(0=不限) | `1` |
    | `--platforms` | 平台列表:`x,tiktok,youtube` 任意组合 | `x,tiktok,youtube` |
    | `--sort` | 排序:`views` / `likes` / `comments` / `time` | `views` |
    | `--top` | 每平台最多返回条数(0=不限) | `5` |
    | `--output-dir` | 输出目录 | `~/Downloads/QoderOverseasTrending` |
    | `--api-key` | 指定 RedFox API Key | — |
    | `--csv-only` | 仅生成 CSV,不生成 HTML | — |
    | `--no-open` | 不自动打开浏览器 | — |
    
    ---
    
    ## API Key 配置
    
    任选一种方式配置个人 Key:
    
    | 方式 | 命令 |
    |------|------|
    | 环境变量(推荐) | `export REDFOX_API_KEY=ak_你的密钥` |
    | 命令行参数 | `--api-key ak_你的密钥` |
    | 配置文件 | `echo '{"api_key":"ak_你的密钥"}' > ~/.qoder/apis/redfox.json` |
    
    注册地址:[redfox.hk](https://redfox.hk/settings/api-keys?source=redskill)
    
    ---
    
    ## 架构说明
    
    ```
    scripts/
    ├── digest.py            # 主编排器:关键词×平台采集 → 时间过滤 → 排序 → 输出
    ├── config.py            # RedFox 网关地址、密钥加载(三级优先级)
    └── sources/             # 平台适配器(新增平台只需加一个文件并在 __init__ 登记)
        ├── base.py          # BaseSource:统一 schema + 递增延迟重试
        ├── x_source.py      # X:search/tweetDetail/tweetComments
        ├── tiktok_source.py # TikTok:searchVideo(一条请求自带点赞/评论数)
        └── youtube_source.py# YouTube:searchVideo 列表 + videoDetail 补点赞/评论
    ```
    
    统一记录 schema:`platform / title / url / author / likes / comments / views / publish_ts / publish_time / keyword / shares`。
    
    ### 平台接口状态(2026-07 实测)
    
    | 平台 | 接口 | 状态 |
    |------|------|------|
    | X | search / tweetDetail / tweetComments | ✅ 全部可用;search 必填 `searchType`(Top/Latest),缺失会误报 3203 |
    | TikTok | searchVideo / awemeDetail / userAwemeList | ✅ 全部可用,searchVideo 自带全量互动数据 |
    | YouTube | searchVideo / videoDetail / videoComments | ✅ 全部可用;searchVideo 返回播放数与相对发布时间,点赞/评论由 videoDetail 补全(Top 10) |
    
    ---
    
    ## 依赖
    
    ```bash
    pip3 install requests
    ```
    
    ---
    
    ## 常见问题
    
    **Q:X 搜索返回 3203 报错?**
    A:X search 接口的 `searchType` 为必填参数(`Top` 热门 / `Latest` 最新),缺失时 RedFox 会返回误导性的 3203「X能力调用失败」。本技能已内置该参数;另外过期/无效的 tweetId 调 tweetDetail 也会报 3203。
    
    **Q:TikTok 搜索需要调详情接口补数据吗?**
    A:不需要。searchVideo 一次请求即返回点赞/评论/播放/分享数和发布时间,日常概要足够。
    
    **Q:YouTube 的点赞/评论数据从哪里来的?**
    A:searchVideo 列表接口只返回播放数,点赞/评论需调 videoDetail 逐条补全。技能默认对搜索结果 20 条全量补详情(`youtube_source.py` 中 `detail_top` 可调小以省积分),因此所有视频都有真实点赞/评论数;仍为 0 代表该视频确实无点赞/评论。YouTube 平台本身不公开分享数,故分享列恒为 0。
    
    **Q:每平台返回多少条?**
    A:默认每平台按排序指标(点赞/播放/评论/发布时间)取 Top 5,`--top N` 可调(0=不限)。各平台独立分组、组内降序,互不影响。
    
    **Q:为什么默认参数下看不到 TikTok 内容?**
    A:TikTok searchVideo 返回的是热门视频,发布时间多为数天乃至数月前,默认 `--days 1`(最近 24 小时)会将其整体过滤。想看 TikTok 热门请加 `--days 0` 或 `--days 7`;过滤时终端会按平台提示被排除的条数。
    
    **Q:搜中文关键词,为什么 X 上的日文内容被排到后面了?**
    A:技能内置语言优先分层:关键词为中文时,各平台组内优先展示中文内容、其次英文,日文等其他语言兜底(解决「南海」命中日文「南海電鉄」这类歧义问题);英文关键词同理优先英文内容。语言识别基于标题字符集(汉字/假名/谚文/拉丁字母),无第三方依赖。
    
    **Q:想做每日定时运行?**
    A:可以让 AI 注册定时任务,每天自动执行 `digest.py` 并生成报告。
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related