Skill: mzoob/jike-data-service (version 1.0.0)

--- name: jike-data-service version: 1.0.0 license: MIT description: "极客数据服务 — 抖音内容营销数据平台。为荣耀手机提供竞品分析、内容对标、热点追踪等数据能力。覆盖场景包括：对标账号搜索、热点关键词搜索、平台热点搜索、选题内容建议、对标内容寻找。支持按品牌（荣耀/华为/小米/OPPO/vivo/苹果/三星）、粉丝量、内容标签（拍摄/电池/屏幕/AI/外观/性能/游戏/评测/开箱/折叠屏/性价比）等多维筛选KOS/KOL账号，搜索关键词排名视频，查看38分类热榜，发现热门话题趋势，基于卖点找对标内容供内容架构师分析。适用场景包括：抖音账号搜索、竞品KOS分析、KOL筛选、关键词搜索量、搜索排名视频、热榜视频、热门话题、内容选题、对标内容、手机卖点内容分析、品牌内容营销数据。" homepage: https://ry-api.dso100.com metadata: {"openclaw": {"emoji": "📊", "requires": {"bins": ["python3"]}}} --- # 极客数据服务 Skill Script: `python3 {baseDir}/scripts/ry-data.py` ## Persona 你是 **内容营销数据助手** — 一位专业的抖音内容营销数据分析顾问，服务于荣耀手机的内容团队。所有回复遵循： - 说中文，专业但亲切："查到了～"、"分析完成"、"已为你筛选"。 - 展示关键数据：粉丝量、互动量、搜索指数、热度分。 - 完成操作后主动建议下一步（"要不要看这些账号的视频内容？"、"需要查看关键词排名视频吗？"）。 - 搜索结果中重点标注荣耀和竞品的数据对比。 - 数据量大时分页展示，每次不超过 50 条。 ## CRITICAL RULES 1. **ALWAYS use the script** — 不要直接 curl API，所有操作通过 `python3 {baseDir}/scripts/ry-data.py` 执行。 2. **Secret Key 认证** — 所有接口通过 `RY_DATA_SECRET_KEY` 环境变量或 config.json 中的 `secret_key` 认证。请求头 `X-API-Key` 自动携带。 3. **分页查询** — ES 承受不了大量数据一次查询，每次最多查 50-100 条，需要更多数据时循环多页。 4. **品牌标签严格匹配** — 只支持 7 个品牌：荣耀, 华为, 小米, OPPO, vivo, 苹果, 三星。不要使用其他品牌名。 5. **内容标签严格匹配** — 只支持 11 个标签：拍摄, 电池, 屏幕, AI, 外观, 性能, 游戏, 评测, 开箱, 折叠屏, 性价比。 6. **命令参数速查表** — 见 `{baseDir}/references/cli-reference.md`。 7. **API 失败最多重试 1 次** — 如果同一个接口连续失败 2 次，立即停止重试，告知用户原因并给出替代方案。 ## Data Dependencies（核心工作流） 5 个场景之间有明确的数据流向： ``` ① 对标账号搜索 ──┐ │ ② 热点关键词搜索 ──┼──→ ④ 选题内容建议 ──→ ⑤ 对标内容寻找 │ ③ 平台热点搜索 ──┘ ``` - **主动路径**：卖点 → 找账号(①) → 找内容(④) → 对标分析(⑤) - **被动路径**：看热点(②③) → 找可复制的内容(⑤) → 对标分析 **场景间的数据传递：** - ① 输出 `sec_uid` 列表 → 传入 ④⑤ 的 `--sec-uids` 参数 - ② 输出关键词列表 → 传入 ④⑤ 的 `keyword-videos search --keyword` - ③ 输出热门话题 → 传入 ⑤ 的 `hot-videos search --keyword` ## Error Handling（错误处理） | 错误场景 | 处理方式 | |---------|---------| | 401 认证失败 | 检查 config.json 中的 secret_key 是否正确 | | API 返回 500 / 服务不可用 | 最多重试 1 次，失败后告知用户"服务暂时不可用，建议稍后再试" | | 搜索结果为空 | 放宽筛选条件（降低粉丝门槛、去掉部分标签），告知用户调整建议 | | 连续 2 次相同错误 | 停止重试，告知用户原因 | | 分页超出范围 | 告知用户已到最后一页 | | 连接超时 | 检查 API 地址是否正确：`python3 {baseDir}/scripts/ry-data.py check` | ## API Setup 编辑 `{baseDir}/scripts/config.json`，填写 `secret_key`。或设置环境变量 `RY_DATA_SECRET_KEY`。 快速检查：`python3 {baseDir}/scripts/ry-data.py check` **⚠️ 重要：** 没有正确的 secret_key 将无法访问 API（返回 401）。 ## Routing Table | 用户意图 | 命令 | 说明 | |---------|------|------| | **场景①** 对标账号搜索 | `accounts search` | 按品牌/粉丝/标签等多维筛选账号 | | 查看账号详情 | `accounts get` | 获取单个账号完整信息 | | **场景②** 关键词搜索 | `keywords search` | 搜索关键词及其指标 | | 关键词排名视频 | `keyword-videos search` | 查看关键词在抖音搜索中的排名视频 | | 添加监控关键词 | `keywords add` | 添加新关键词到监控列表 | | 删除关键词 | `keywords delete` | 从监控列表删除关键词 | | **场景③** 热榜视频 | `hot-videos search` | 查看 38 分类热榜视频 | | 热榜分类列表 | `hot-videos categories` | 查看所有可用分类 | | 热门话题 | `hashtags trending` | 查看近期热门话题趋势 | | 搜索话题 | `hashtags search` | 按关键词搜索话题标签 | | **场景④** 选题内容建议 | `accounts search` + `videos search` | 先找账号，再搜其视频内容 | | **场景⑤** 对标内容寻找 | `videos search` + `keyword-videos search` + `hot-videos search` | 汇总多来源视频 | | 视频搜索 | `videos search` | 按作者/关键词/互动量搜索视频 | | 视频趋势 | `videos trend` | 查看视频近期互动趋势 | ## Workflow Guide 完整工作流和使用示例 → 阅读 `{baseDir}/references/workflow-guide.md` ## Script Usage ```bash # 检查 API 连接 python3 {baseDir}/scripts/ry-data.py check # ── 场景① 对标账号搜索 ── # 搜索荣耀品牌 KOS 账号 python3 {baseDir}/scripts/ry-data.py accounts search \ --brand-tags "荣耀" --follower-min 1000 --user-type "1,2" --exclude-star # 搜索竞品拍摄类账号 python3 {baseDir}/scripts/ry-data.py accounts search \ --brand-tags "华为,小米,OPPO,vivo" --content-tags "拍摄" \ --follower-min 1000 --exclude-star --page-size 50 # 查看账号详情 python3 {baseDir}/scripts/ry-data.py accounts get --sec-uid "MS4wLjABAAAA..." # ── 场景② 热点关键词搜索 ── # 搜索荣耀相关关键词 python3 {baseDir}/scripts/ry-data.py keywords search --keyword "荣耀" --page-size 50 # 查看关键词排名视频 python3 {baseDir}/scripts/ry-data.py keyword-videos search --keyword "荣耀Magic7" # 添加新关键词监控 python3 {baseDir}/scripts/ry-data.py keywords add --word "荣耀GT7 Pro拍照" # ── 场景③ 平台热点搜索 ── # 查看科技分类热榜 python3 {baseDir}/scripts/ry-data.py hot-videos search --category "科技" --page-size 50 # 查看所有分类 python3 {baseDir}/scripts/ry-data.py hot-videos categories # 查看近7天热门话题 python3 {baseDir}/scripts/ry-data.py hashtags trending --days 7 --size 50 # ── 场景④⑤ 内容搜索 ── # 搜索指定账号的视频 python3 {baseDir}/scripts/ry-data.py videos search \ --sec-uids "uid1,uid2" --keyword "拍照" \ --sort-by interaction_count --page-size 50 # 搜索高互动视频 python3 {baseDir}/scripts/ry-data.py videos search \ --keyword "手机拍照" --interaction-min 1000 --page-size 50 # 查看视频趋势 python3 {baseDir}/scripts/ry-data.py videos trend --aweme-id "7321234567890" --days 7 ``` ## Output - 所有输出默认为格式化文本，加 `--json` 获取原始 JSON。 - 大数字自动格式化（如 12345 → 1.2万）。 - 错误信息包含 HTTP 状态码和 API 错误描述。 ## Data Coverage | 数据 | ES 索引 | 更新频率 | |------|---------|---------| | 用户账号 | ry_douyin_user | 每2分钟 (Scrapy crawler) | | 品牌/内容标签 | ry_douyin_user | 每分钟 (auto-tagging) | | 关键词视频 | ry_douyin_keyword_video | 每5分钟 (keyword crawler) | | 热榜视频 | ry_douyin_hot_video | 每5分钟 (douhot crawler) | | 关键词指标 | ry_douyin_keyword | 每24小时 (Go service) | ## Supported Brands 荣耀, 华为, 小米, OPPO, vivo, 苹果, 三星 ## Supported Content Tags 拍摄, 电池, 屏幕, AI, 外观, 性能, 游戏, 评测, 开箱, 折叠屏, 性价比

path: scripts/config.json
content: base_url: https://ry-api.dso100.com
page_size: 20
max_page_size: 100

path: scripts/ry-data.py
content: #!/usr/bin/env python3
"""
极客数据 CLI — OpenClaw skill script for 抖音内容营销数据平台.

Usage:
  python3 ry-data.py check
  python3 ry-data.py accounts search [options]
  python3 ry-data.py accounts get --sec-uid <sec_uid>
  python3 ry-data.py videos search [options]
  python3 ry-data.py videos trend --aweme-id <id> [--days 7]
  python3 ry-data.py keywords search [options]
  python3 ry-data.py keywords add --word <keyword>
  python3 ry-data.py keywords delete --word <keyword>
  python3 ry-data.py keyword-videos search [options]
  python3 ry-data.py hot-videos search [options]
  python3 ry-data.py hot-videos categories
  python3 ry-data.py hashtags search [options]
  python3 ry-data.py hashtags trending [--days 7] [--size 50]

Configuration:
  Edit scripts/config.json to set base_url.
  Environment variable RY_DATA_BASE_URL overrides config.json.
"""

from __future__ import annotations

import argparse
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_PATH = os.path.join(SCRIPT_DIR, "config.json")
DEFAULT_BASE_URL = "https://ry-api.dso100.com"

_config_cache = None


def load_config() -> dict:
    global _config_cache
    if _config_cache is not None:
        return _config_cache
    if not os.path.exists(CONFIG_PATH):
        _config_cache = {}
        return _config_cache
    try:
        with open(CONFIG_PATH, "r", encoding="utf-8") as f:
            _config_cache = json.load(f)
    except (json.JSONDecodeError, OSError) as e:
        print(f"Warning: Failed to read config.json: {e}", file=sys.stderr)
        _config_cache = {}
    return _config_cache


def get_base_url() -> str:
    url = os.environ.get("RY_DATA_BASE_URL", "").strip()
    if not url:
        cfg = load_config()
        url = (cfg.get("base_url") or "").strip()
    return url or DEFAULT_BASE_URL


def get_secret_key() -> str:
    """Get API key. Environment variable overrides config.json."""
    key = os.environ.get("RY_DATA_SECRET_KEY", "").strip()
    if not key:
        cfg = load_config()
        key = (cfg.get("secret_key") or "").strip()
    return key


def get_page_size() -> int:
    cfg = load_config()
    return cfg.get("page_size", 20)


# ---------------------------------------------------------------------------
# HTTP helpers
# ---------------------------------------------------------------------------


def api_request(method: str, path: str, body=None, query: dict | None = None) -> dict:
    """Send HTTP request to API. Returns parsed JSON response."""
    base = get_base_url()
    url = f"{base}{path}"
    if query:
        url += "?" + urllib.parse.urlencode({k: v for k, v in query.items() if v is not None})

    headers = {
        "Content-Type": "application/json",
        "Accept": "application/json",
    }

    secret_key = get_secret_key()
    if secret_key:
        headers["X-API-Key"] = secret_key

    data = json.dumps(body).encode("utf-8") if body else None
    req = urllib.request.Request(url, data=data, headers=headers, method=method)

    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            result = json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        err_body = e.read().decode("utf-8", errors="replace")
        try:
            err_json = json.loads(err_body)
            msg = err_json.get("message", err_body)
        except Exception:
            msg = err_body
        print(f"Error: HTTP {e.code} — {msg}", file=sys.stderr)
        sys.exit(1)
    except urllib.error.URLError as e:
        print(f"Error: Connection failed — {e.reason}", file=sys.stderr)
        sys.exit(1)

    if isinstance(result, dict) and result.get("code", 0) not in (0, 200):
        print(f"Error: {result.get('message', 'Unknown error')}", file=sys.stderr)
        sys.exit(1)
    return result.get("data") if isinstance(result, dict) else result


def fmt_json(data, use_json: bool = False):
    if use_json:
        print(json.dumps(data, ensure_ascii=False, indent=2))
        return True
    return False


def fmt_number(n) -> str:
    """Format large numbers: 12345 -> 1.2万"""
    if n is None:
        return "0"
    n = int(n)
    if n >= 10000:
        return f"{n/10000:.1f}万"
    return str(n)


# ---------------------------------------------------------------------------
# Commands: check
# ---------------------------------------------------------------------------


def cmd_check(_args):
    base = get_base_url()
    key = get_secret_key()
    print(f"Config:   {CONFIG_PATH}")
    print(f"Base URL: {base}")
    if key:
        print(f"Key:      {key[:8]}...{key[-4:]}" if len(key) > 12 else f"Key: {key}")
    else:
        print(f"Key:      未配置 (set secret_key in config.json or RY_DATA_SECRET_KEY env)")
    try:
        data = api_request("GET", "/api/v1/douyin/hot-videos/categories")
        count = len(data) if isinstance(data, list) else 0
        print(f"Status:   OK ({count} categories available)")
    except SystemExit:
        print("Status:   FAILED — check your API key and network")
        sys.exit(1)


# ---------------------------------------------------------------------------
# Commands: accounts
# ---------------------------------------------------------------------------


def cmd_accounts(args):
    if args.action == "search":
        body = {}
        if args.keyword: body["keyword"] = args.keyword
        if args.follower_min is not None: body["follower_min"] = args.follower_min
        if args.follower_max is not None: body["follower_max"] = args.follower_max
        if args.brand_tags: body["brand_tags"] = [t.strip() for t in args.brand_tags.split(",")]
        if args.content_tags: body["content_tags"] = [t.strip() for t in args.content_tags.split(",")]
        if args.user_type: body["user_type"] = [int(t.strip()) for t in args.user_type.split(",")]
        if args.exclude_star: body["exclude_star"] = True
        if args.exclude_shop: body["exclude_shop"] = True
        body["sort_by"] = args.sort_by or "follower_count"
        body["sort_order"] = args.sort_order or "desc"
        body["page"] = args.page or 1
        body["page_size"] = args.page_size or get_page_size()

        data = api_request("POST", "/api/v1/douyin/users/search", body)
        if fmt_json(data, args.json):
            return

        total = data.get("total", 0)
        users = data.get("list", [])
        print(f"共 {total} 个账号，当前显示 {len(users)} 个:\n")
        for i, u in enumerate(users, 1):
            nickname = u.get("nickname", "未知")
            followers = fmt_number(u.get("follower_count", 0))
            favorited = fmt_number(u.get("total_favorited", 0))
            aweme = u.get("aweme_count", 0)
            utype = {1: "个人", 2: "黄V", 3: "蓝V"}.get(u.get("user_type"), "?")
            brands = ",".join(u.get("brand_tags", []))
            tags = ",".join(u.get("content_tags", []))
            print(f"  {i}. {nickname} [{utype}]  粉丝:{followers}  获赞:{favorited}  作品:{aweme}")
            if brands:
                print(f"     品牌: {brands}")
            if tags:
                print(f"     标签: {tags}")
            print(f"     sec_uid: {u.get('sec_uid', '')}")
            print()

    elif args.action == "get":
        if not args.sec_uid:
            print("Error: --sec-uid required", file=sys.stderr); sys.exit(1)
        data = api_request("GET", f"/api/v1/douyin/users/{args.sec_uid}")
        if fmt_json(data, args.json):
            return
        print(json.dumps(data, ensure_ascii=False, indent=2))


# ---------------------------------------------------------------------------
# Commands: videos
# ---------------------------------------------------------------------------


def cmd_videos(args):
    if args.action == "search":
        body = {}
        if args.keyword: body["keyword"] = args.keyword
        if args.sec_uids: body["sec_uids"] = [s.strip() for s in args.sec_uids.split(",")]
        if args.interaction_min is not None: body["interaction_count_min"] = args.interaction_min
        if args.interaction_max is not None: body["interaction_count_max"] = args.interaction_max
        if args.publish_start: body["publish_time_start"] = args.publish_start
        if args.publish_end: body["publish_time_end"] = args.publish_end
        body["sort_by"] = args.sort_by or "interaction_count"
        body["sort_order"] = args.sort_order or "desc"
        body["page"] = args.page or 1
        body["page_size"] = args.page_size or get_page_size()

        data = api_request("POST", "/api/v1/douyin/videos/search", body)
        if fmt_json(data, args.json):
            return

        total = data.get("total", 0)
        videos = data.get("list", [])
        print(f"共 {total} 个视频，当前显示 {len(videos)} 个:\n")
        for i, v in enumerate(videos, 1):
            title = (v.get("desc") or v.get("item_title") or "无标题")[:60]
            author = v.get("nickname") or v.get("nick_name") or "未知"
            interaction = fmt_number(v.get("interaction_count", 0))
            play = fmt_number(v.get("play_count") or v.get("play_cnt", 0))
            like = fmt_number(v.get("digg_count") or v.get("like_cnt", 0))
            print(f"  {i}. {title}")
            print(f"     作者: {author}  播放:{play}  点赞:{like}  互动:{interaction}")
            aweme_id = v.get("aweme_id") or v.get("item_id", "")
            if aweme_id:
                print(f"     aweme_id: {aweme_id}")
            print()

    elif args.action == "trend":
        if not args.aweme_id:
            print("Error: --aweme-id required", file=sys.stderr); sys.exit(1)
        days = args.days or 7
        data = api_request("GET", f"/api/v1/douyin/videos/{args.aweme_id}/trend",
                           query={"days": str(days)})
        if fmt_json(data, args.json):
            return
        items = data.get("list", []) if isinstance(data, dict) else data
        total = data.get("total", len(items)) if isinstance(data, dict) else len(items)
        print(f"视频趋势 (近{days}天, {total}条记录):\n")
        if isinstance(items, list):
            for item in items:
                print(f"  {json.dumps(item, ensure_ascii=False)}")
        else:
            print(json.dumps(data, ensure_ascii=False, indent=2))


# ---------------------------------------------------------------------------
# Commands: keywords
# ---------------------------------------------------------------------------


def cmd_keywords(args):
    if args.action == "search":
        body = {}
        if args.keyword: body["keyword"] = args.keyword
        if args.brand: body["brand"] = args.brand
        if args.category: body["category"] = args.category
        body["sort_by"] = args.sort_by or "mounth_search_index"
        body["sort_order"] = args.sort_order or "desc"
        body["page"] = args.page or 1
        body["page_size"] = args.page_size or get_page_size()

        data = api_request("POST", "/api/v1/douyin/keywords/search", body)
        if fmt_json(data, args.json):
            return

        total = data.get("total", 0)
        kws = data.get("list", [])
        print(f"共 {total} 个关键词，当前显示 {len(kws)} 个:\n")
        for i, kw in enumerate(kws, 1):
            word = kw.get("keyword", "")
            search_idx = kw.get("mounth_search_index", 0)
            competition = kw.get("competition", "")
            video_count = kw.get("video_count", 0)
            print(f"  {i}. {word}")
            print(f"     月搜索指数:{search_idx}  竞争:{competition}  视频数:{video_count}")
            print()

    elif args.action == "add":
        if not args.word:
            print("Error: --word required", file=sys.stderr); sys.exit(1)
        data = api_request("POST", "/api/v1/douyin/keywords", {"keyword": args.word})
        if fmt_json(data, args.json):
            return
        print(f"关键词添加成功: {args.word}")

    elif args.action == "delete":
        if not args.word:
            print("Error: --word required", file=sys.stderr); sys.exit(1)
        api_request("DELETE", f"/api/v1/douyin/keywords/{urllib.parse.quote(args.word)}")
        print(f"关键词删除成功: {args.word}")


# ---------------------------------------------------------------------------
# Commands: keyword-videos
# ---------------------------------------------------------------------------


def cmd_keyword_videos(args):
    if args.action == "search":
        body = {}
        if args.keyword: body["keyword"] = args.keyword
        if args.search_date: body["search_date"] = args.search_date
        body["sort_by"] = args.sort_by or "rank"
        body["sort_order"] = args.sort_order or "asc"
        body["page"] = args.page or 1
        body["page_size"] = args.page_size or get_page_size()

        data = api_request("POST", "/api/v1/douyin/keyword-videos/search", body)
        if fmt_json(data, args.json):
            return

        total = data.get("total", 0)
        videos = data.get("list", [])
        print(f"关键词「{args.keyword or ''}」搜索排名，共 {total} 条:\n")
        for i, v in enumerate(videos, 1):
            title = (v.get("desc") or v.get("item_title") or "无标题")[:60]
            rank = v.get("rank", i)
            author = v.get("nickname") or v.get("nick_name") or "未知"
            print(f"  #{rank} {title}")
            print(f"      作者: {author}")
            print()


# ---------------------------------------------------------------------------
# Commands: hot-videos
# ---------------------------------------------------------------------------


def cmd_hot_videos(args):
    if args.action == "search":
        body = {}
        if args.category: body["category"] = args.category
        if args.snapshot_date: body["snapshot_date"] = args.snapshot_date
        if args.keyword: body["keyword"] = args.keyword
        body["sort_by"] = args.sort_by or "score"
        body["sort_order"] = args.sort_order or "desc"
        body["page"] = args.page or 1
        body["page_size"] = args.page_size or get_page_size()

        data = api_request("POST", "/api/v1/douyin/hot-videos/search", body)
        if fmt_json(data, args.json):
            return

        total = data.get("total", 0)
        videos = data.get("list", [])
        print(f"热榜视频，共 {total} 条:\n")
        for i, v in enumerate(videos, 1):
            title = (v.get("item_title") or "无标题")[:60]
            author = v.get("nick_name") or "未知"
            score = v.get("score", 0)
            play = fmt_number(v.get("play_cnt", 0))
            like = fmt_number(v.get("like_cnt", 0))
            follow_rate = v.get("follow_rate", 0)
            cat = v.get("category", "")
            rank = v.get("rank", i)
            print(f"  #{rank} [{cat}] {title}")
            print(f"      作者:{author}  热度:{score}  播放:{play}  点赞:{like}  涨粉率:{follow_rate}")
            print()

    elif args.action == "categories":
        data = api_request("GET", "/api/v1/douyin/hot-videos/categories")
        if fmt_json(data, args.json):
            return
        if isinstance(data, list):
            print(f"共 {len(data)} 个分类:\n")
            for c in data:
                print(f"  [{c.get('category_id', '?')}] {c.get('category', '?')}")
        else:
            print(json.dumps(data, ensure_ascii=False, indent=2))


# ---------------------------------------------------------------------------
# Commands: hashtags
# ---------------------------------------------------------------------------


def cmd_hashtags(args):
    if args.action == "search":
        body = {}
        if args.keyword: body["keyword"] = args.keyword
        if args.is_trending: body["is_trending"] = True
        body["sort_by"] = args.sort_by or "video_count"
        body["sort_order"] = args.sort_order or "desc"
        body["page"] = args.page or 1
        body["page_size"] = args.page_size or get_page_size()

        data = api_request("POST", "/api/v1/douyin/hashtags/search", body)
        if fmt_json(data, args.json):
            return

        total = data.get("total", 0)
        tags = data.get("list", [])
        print(f"话题标签，共 {total} 条:\n")
        for i, t in enumerate(tags, 1):
            name = t.get("hashtag", "")
            vcount = t.get("video_count", 0)
            interaction = fmt_number(t.get("total_interaction", 0))
            print(f"  {i}. #{name}  视频数:{vcount}  总互动:{interaction}")

    elif args.action == "trending":
        body = {
            "days": args.days or 7,
            "size": args.size or 50,
        }
        data = api_request("POST", "/api/v1/douyin/trending-hashtags", body)
        if fmt_json(data, args.json):
            return

        items = data.get("list", []) if isinstance(data, dict) else data
        total = data.get("total", len(items)) if isinstance(data, dict) else len(items)
        print(f"热门话题 (近{body['days']}天, {total}条):\n")
        for i, t in enumerate(items, 1):
            name = t.get("hashtag", "")
            vcount = t.get("video_count", 0)
            interaction = fmt_number(t.get("total_interaction", 0))
            print(f"  {i}. #{name}  视频数:{vcount}  总互动:{interaction}")


# ---------------------------------------------------------------------------
# Argument parser
# ---------------------------------------------------------------------------


def build_parser():
    parser = argparse.ArgumentParser(
        prog="ry-data.py",
        description="极客数据 CLI — 抖音内容营销数据平台",
    )
    sub = parser.add_subparsers(dest="command", required=True)

    # check
    sub.add_parser("check", help="检查 API 连接状态")

    # --- accounts ---
    p = sub.add_parser("accounts", help="账号搜索与查询")
    p.add_argument("action", choices=["search", "get"])
    p.add_argument("--keyword", help="搜索昵称/简介")
    p.add_argument("--follower-min", type=int, help="最低粉丝数")
    p.add_argument("--follower-max", type=int, help="最高粉丝数")
    p.add_argument("--brand-tags", help="品牌标签，逗号分隔 (荣耀,华为,小米,OPPO,vivo,苹果,三星)")
    p.add_argument("--content-tags", help="内容标签，逗号分隔 (拍摄,电池,屏幕,AI,外观,性能,游戏,评测,开箱,折叠屏,性价比)")
    p.add_argument("--user-type", help="账号类型，逗号分隔 (1=个人,2=黄V,3=蓝V)")
    p.add_argument("--exclude-star", action="store_true", help="排除明星账号")
    p.add_argument("--exclude-shop", action="store_true", help="排除带货账号")
    p.add_argument("--sec-uid", help="用户 sec_uid (get 用)")
    p.add_argument("--sort-by", default="follower_count", help="排序字段")
    p.add_argument("--sort-order", default="desc", choices=["asc", "desc"])
    p.add_argument("--page", type=int, default=1)
    p.add_argument("--page-size", type=int)
    p.add_argument("--json", action="store_true", help="输出原始 JSON")

    # --- videos ---
    p = sub.add_parser("videos", help="视频搜索与趋势")
    p.add_argument("action", choices=["search", "trend"])
    p.add_argument("--keyword", help="搜索视频标题/描述")
    p.add_argument("--sec-uids", help="作者 sec_uid 列表，逗号分隔")
    p.add_argument("--interaction-min", type=int, help="最低互动量")
    p.add_argument("--interaction-max", type=int, help="最高互动量")
    p.add_argument("--publish-start", type=int, help="发布时间起始 (Unix timestamp)")
    p.add_argument("--publish-end", type=int, help="发布时间结束 (Unix timestamp)")
    p.add_argument("--aweme-id", help="视频 ID (trend 用)")
    p.add_argument("--days", type=int, default=7, help="趋势天数 (默认7)")
    p.add_argument("--sort-by", default="interaction_count", help="排序字段")
    p.add_argument("--sort-order", default="desc", choices=["asc", "desc"])
    p.add_argument("--page", type=int, default=1)
    p.add_argument("--page-size", type=int)
    p.add_argument("--json", action="store_true")

    # --- keywords ---
    p = sub.add_parser("keywords", help="关键词搜索与管理")
    p.add_argument("action", choices=["search", "add", "delete"])
    p.add_argument("--keyword", help="搜索关键词")
    p.add_argument("--word", help="要添加/删除的关键词")
    p.add_argument("--brand", help="品牌筛选")
    p.add_argument("--category", help="分类筛选")
    p.add_argument("--sort-by", default="mounth_search_index", help="排序字段")
    p.add_argument("--sort-order", default="desc", choices=["asc", "desc"])
    p.add_argument("--page", type=int, default=1)
    p.add_argument("--page-size", type=int)
    p.add_argument("--json", action="store_true")

    # --- keyword-videos ---
    p = sub.add_parser("keyword-videos", help="关键词搜索排名视频")
    p.add_argument("action", choices=["search"])
    p.add_argument("--keyword", help="关键词 (精确匹配)")
    p.add_argument("--search-date", help="搜索日期 YYYY-MM-DD")
    p.add_argument("--sort-by", default="rank")
    p.add_argument("--sort-order", default="asc", choices=["asc", "desc"])
    p.add_argument("--page", type=int, default=1)
    p.add_argument("--page-size", type=int)
    p.add_argument("--json", action="store_true")

    # --- hot-videos ---
    p = sub.add_parser("hot-videos", help="热榜视频")
    p.add_argument("action", choices=["search", "categories"])
    p.add_argument("--category", help="分类名称 (如 科技, 游戏)")
    p.add_argument("--snapshot-date", help="日期 YYYY-MM-DD")
    p.add_argument("--keyword", help="搜索视频标题")
    p.add_argument("--sort-by", default="score")
    p.add_argument("--sort-order", default="desc", choices=["asc", "desc"])
    p.add_argument("--page", type=int, default=1)
    p.add_argument("--page-size", type=int)
    p.add_argument("--json", action="store_true")

    # --- hashtags ---
    p = sub.add_parser("hashtags", help="话题标签搜索与热门趋势")
    p.add_argument("action", choices=["search", "trending"])
    p.add_argument("--keyword", help="搜索话题")
    p.add_argument("--is-trending", action="store_true", help="仅热门话题")
    p.add_argument("--days", type=int, default=7, help="热门话题天数范围")
    p.add_argument("--size", type=int, default=50, help="热门话题数量")
    p.add_argument("--sort-by", default="video_count")
    p.add_argument("--sort-order", default="desc", choices=["asc", "desc"])
    p.add_argument("--page", type=int, default=1)
    p.add_argument("--page-size", type=int)
    p.add_argument("--json", action="store_true")

    return parser


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

COMMAND_MAP = {
    "check": cmd_check,
    "accounts": cmd_accounts,
    "videos": cmd_videos,
    "keywords": cmd_keywords,
    "keyword-videos": cmd_keyword_videos,
    "hot-videos": cmd_hot_videos,
    "hashtags": cmd_hashtags,
}


def main():
    parser = build_parser()
    args = parser.parse_args()
    handler = COMMAND_MAP.get(args.command)
    if handler:
        handler(args)
    else:
        parser.print_help()
        sys.exit(1)


if __name__ == "__main__":
    main()


path: references/workflow-guide.md
content: # 极客数据服务 工作流指南

## 核心概念

### 平台定位
极客数据服务是抖音内容营销数据平台，为荣耀手机内容团队提供竞品分析、内容对标、热点追踪等数据能力。核心用户是**内容架构师**，通过数据找到可复制的优质内容。

### 5 个场景

| # | 场景 | 优先级 | 核心能力 |
|---|------|--------|---------|
| ① | 对标账号搜索 | 中 | 按品牌/粉丝/标签多维筛选 KOS/KOL 账号 |
| ② | 热点关键词搜索 | 中 | 搜索词发现、竞争分析、关键词-视频关联 |
| ③ | 平台热点搜索 | 中 | 38 分类热榜视频、热门话题趋势 |
| ④ | 选题内容建议 | 高 | 基于卖点找对标内容，供内容架构师分析 |
| ⑤ | 对标内容寻找 | 高 | 汇总账号+关键词+热点，输出对标视频列表 |

### 工作流（数据流向）

```
① 对标账号搜索 ──┐
                  │
② 热点关键词搜索 ──┼──→ ④ 选题内容建议 ──→ ⑤ 对标内容寻找
                  │
③ 平台热点搜索 ──┘
```

- **主动路径**：卖点 → 找账号 → 找内容 → 选题建议
- **被动路径**：看热点 → 找可复制的内容 → 对标分析

### 数据覆盖

| 数据 | 更新频率 | 说明 |
|------|---------|------|
| 用户账号 | 每2分钟 | Scrapy 爬虫持续采集 |
| 品牌/内容标签 | 每分钟 | 自动打标脚本 |
| 关键词视频 | 每5分钟 | 关键词搜索排名爬虫 |
| 热榜视频 | 每5分钟 | 热点宝榜单爬虫（每次1个分类，全量约3小时） |
| 关键词指标 | 每24小时 | Go 服务内置同步 |

---

## 典型工作流

### 流程一：主动路径 — 卖点驱动找内容（最常用）

适用场景：荣耀新品上市，内容架构师拿到产品卖点（如"人像拍摄"），需要找对标内容。

```
Step 1: 找竞品账号
  → accounts search --brand-tags "华为,小米,OPPO,vivo" --content-tags "拍摄" --follower-min 1000 --exclude-star

Step 2: 看这些账号的拍摄内容
  → videos search --sec-uids "uid1,uid2,..." --keyword "拍照" --interaction-min 1000 --sort-by interaction_count --page-size 50

Step 3: 同时看关键词排名内容
  → keyword-videos search --keyword "手机拍照"

Step 4: 汇总输出 top 100 视频，供内容架构师人肉分析
```

**关键筛选标准：**
- KOS 账号内容：互动量 > 1000
- KOL 账号内容：互动量 > 10000
- 优先最近 30 天的内容

### 流程二：被动路径 — 热点驱动找内容

适用场景：内容架构师日常监控，看什么内容火可以复制。

```
Step 1: 查看科技分类热榜
  → hot-videos search --category "科技" --page-size 50

Step 2: 查看近期热门话题
  → hashtags trending --days 7 --size 50

Step 3: 搜索手机相关热榜内容
  → hot-videos search --keyword "手机" --page-size 50

Step 4: 筛选中等互动量（可复制）的内容
  → 关注互动量在 1000-10000 之间的视频，这类内容更容易被 AIGC 复刻
```

### 流程三：关键词竞争分析

适用场景：了解荣耀和竞品在抖音搜索中的表现。

```
Step 1: 搜索荣耀相关关键词
  → keywords search --keyword "荣耀" --page-size 50

Step 2: 对比竞品关键词
  → keywords search --keyword "华为" --page-size 50
  → keywords search --keyword "小米" --page-size 50

Step 3: 查看高搜索量关键词的排名视频
  → keyword-videos search --keyword "荣耀Magic7"

Step 4: 添加新关键词到监控
  → keywords add --word "荣耀GT7 Pro拍照"
```

### 流程四：账号对标分析

适用场景：深入分析特定竞品账号的内容策略。

```
Step 1: 找到目标账号
  → accounts search --brand-tags "华为" --follower-min 5000 --user-type "1,2" --page-size 20

Step 2: 查看账号详情
  → accounts get --sec-uid "MS4wLjABAAAA..."

Step 3: 搜索该账号的所有视频
  → videos search --sec-uids "MS4wLjABAAAA..." --sort-by interaction_count --page-size 50

Step 4: 查看其爆款视频的趋势
  → videos trend --aweme-id "7321234567890" --days 7
```

---

## 卖点与标签映射

内容架构师提出的卖点需要映射到系统标签：

| 卖点 | content_tags | 相关搜索关键词 |
|------|-------------|---------------|
| 人像拍摄 | 拍摄 | 拍照, 人像, 摄影, 夜景 |
| 电池续航 | 电池 | 续航, 充电, 快充, 电量 |
| 屏幕显示 | 屏幕 | 护眼, 显示, 分辨率, OLED |
| AI 功能 | AI | 智能, 大模型, 语音助手 |
| 外观设计 | 外观 | 颜值, 设计, 配色, 手感 |
| 性能体验 | 性能 | 处理器, 芯片, 骁龙, 流畅 |
| 游戏体验 | 游戏 | 电竞, 帧率, 原神, 散热 |
| 产品评测 | 评测 | 测评, 体验, 上手 |
| 新品开箱 | 开箱 | 开箱, 首发, 新品 |
| 折叠屏 | 折叠屏 | 折叠, 翻盖, 内屏, 外屏 |
| 性价比 | 性价比 | 值不值, 推荐, 千元机 |

---

## 内容筛选标准

### KOS 内容（品牌销售账号）
- 粉丝 > 1000
- 互动量 > 1000
- 发布时间：最近 30 天
- 排除明星、排除纯带货

### KOL 内容（头部达人）
- 粉丝 > 10000
- 互动量 > 10000
- 发布时间：最近 30 天

### 搜索排名内容
- 只看最近 14 天内生产的
- 只看之前没出现过的新内容

### 热榜可复制内容
- 互动量在 1000-10000 之间（中等热度，可复制）
- 和手机品类相关

---

## 错误处理

### 原则
- 同一接口最多重试 1 次，连续失败 2 次后停止
- 不要反复尝试
- 明确告知用户失败原因和替代方案

### 常见错误

| 错误场景 | 处理方式 |
|---------|---------|
| 连接失败 | 运行 `check` 确认 API 地址，检查网络 |
| 搜索无结果 | 放宽条件：降低粉丝门槛、减少标签、扩大时间范围 |
| 500 服务错误 | 重试 1 次，仍失败则建议稍后再试 |
| 数据量过大 | 分页查询，每次 50 条，循环多页 |


path: references/cli-reference.md
content: # ry-data.py 命令参数速查表

所有命令格式：`python3 {baseDir}/scripts/ry-data.py <command> [action] [options]`

全局说明：
- 所有列表命令支持 `--json` 输出原始 JSON
- 分页参数：`--page`（页码，从1开始）、`--page-size`（每页条数，默认20，最大100）
- 排序参数：`--sort-by`（排序字段）、`--sort-order`（asc/desc）

---

## check

检查 API 连接状态。

```bash
python3 ry-data.py check
```

无参数。输出配置信息、API 地址、连接状态。

---

## accounts

账号搜索与查询。

```bash
python3 ry-data.py accounts <action> [options]
```

| action | 说明 | 必填参数 | 可选参数 |
|--------|------|---------|---------|
| `search` | 多维筛选账号 | — | 见下方筛选参数 |
| `get` | 查看账号详情 | `--sec-uid` | `--json` |

### search 筛选参数

| 参数 | 说明 | 示例 |
|------|------|------|
| `--keyword` | 搜索昵称/简介 | `--keyword "荣耀"` |
| `--follower-min` | 最低粉丝数 | `--follower-min 1000` |
| `--follower-max` | 最高粉丝数 | `--follower-max 100000` |
| `--brand-tags` | 品牌标签，逗号分隔 | `--brand-tags "荣耀,华为"` |
| `--content-tags` | 内容标签，逗号分隔 | `--content-tags "拍摄,电池"` |
| `--user-type` | 账号类型，逗号分隔 | `--user-type "1,2"` (1=个人,2=黄V,3=蓝V) |
| `--exclude-star` | 排除明星账号 | `--exclude-star` |
| `--exclude-shop` | 排除带货账号 | `--exclude-shop` |
| `--sort-by` | 排序字段 | `follower_count`(默认), `total_favorited`, `aweme_count` |

可用品牌标签：荣耀, 华为, 小米, OPPO, vivo, 苹果, 三星

可用内容标签：拍摄, 电池, 屏幕, AI, 外观, 性能, 游戏, 评测, 开箱, 折叠屏, 性价比

---

## videos

视频搜索与趋势查询。

```bash
python3 ry-data.py videos <action> [options]
```

| action | 说明 | 必填参数 | 可选参数 |
|--------|------|---------|---------|
| `search` | 搜索视频 | — | 见下方参数 |
| `trend` | 视频互动趋势 | `--aweme-id` | `--days`(默认7) |

### search 参数

| 参数 | 说明 | 示例 |
|------|------|------|
| `--keyword` | 搜索视频标题/描述 | `--keyword "拍照"` |
| `--sec-uids` | 作者 sec_uid，逗号分隔 | `--sec-uids "uid1,uid2"` |
| `--interaction-min` | 最低互动量 | `--interaction-min 1000` |
| `--interaction-max` | 最高互动量 | `--interaction-max 10000` |
| `--publish-start` | 发布起始时间 (Unix timestamp) | `--publish-start 1708300800` |
| `--publish-end` | 发布结束时间 (Unix timestamp) | `--publish-end 1711000000` |
| `--sort-by` | 排序字段 | `interaction_count`(默认), `play_count`, `digg_count`, `create_time` |

---

## keywords

关键词搜索与管理。

```bash
python3 ry-data.py keywords <action> [options]
```

| action | 说明 | 必填参数 | 可选参数 |
|--------|------|---------|---------|
| `search` | 搜索关键词 | — | `--keyword`, `--brand`, `--category` |
| `add` | 添加监控关键词 | `--word` | `--json` |
| `delete` | 删除关键词 | `--word` | — |

### search 参数

| 参数 | 说明 | 示例 |
|------|------|------|
| `--keyword` | 模糊搜索关键词 | `--keyword "荣耀"` |
| `--brand` | 品牌筛选 | `--brand "荣耀"` |
| `--category` | 分类筛选 | `--category "手机"` |
| `--sort-by` | 排序字段 | `mounth_search_index`(默认), `competition`, `video_count` |

---

## keyword-videos

关键词搜索排名视频。

```bash
python3 ry-data.py keyword-videos search [options]
```

| 参数 | 说明 | 示例 |
|------|------|------|
| `--keyword` | 关键词（精确匹配） | `--keyword "荣耀Magic7"` |
| `--search-date` | 搜索日期 | `--search-date "2026-03-24"` |
| `--sort-by` | 排序字段 | `rank`(默认) |

---

## hot-videos

热榜视频查询。

```bash
python3 ry-data.py hot-videos <action> [options]
```

| action | 说明 | 必填参数 | 可选参数 |
|--------|------|---------|---------|
| `search` | 搜索热榜视频 | — | 见下方参数 |
| `categories` | 查看所有分类 | — | `--json` |

### search 参数

| 参数 | 说明 | 示例 |
|------|------|------|
| `--category` | 分类名称 | `--category "科技"` |
| `--snapshot-date` | 日期 | `--snapshot-date "2026-03-24"` |
| `--keyword` | 搜索视频标题 | `--keyword "手机"` |
| `--sort-by` | 排序字段 | `score`(默认), `play_cnt`, `like_cnt`, `follow_rate` |

可用分类（38个）：剧情, 明星, 综艺, 电影, 电视剧, 音乐, 二次元, 游戏, 公益, 随拍, 舞蹈, 动物, 三农, 科技, 财经, 母婴, 亲子, 生活家居, 法律, 医疗健康, 科普, 情感, 文化, 职场, 教育校园, 摄影摄像, 美食, 旅行, 时尚, 艺术, 体育, 休闲娱乐, 汽车, 生活记录, 人文社科, 颜值, 特效&小游戏, AI原生影像

---

## hashtags

话题标签搜索与热门趋势。

```bash
python3 ry-data.py hashtags <action> [options]
```

| action | 说明 | 必填参数 | 可选参数 |
|--------|------|---------|---------|
| `search` | 搜索话题标签 | — | `--keyword`, `--is-trending` |
| `trending` | 热门话题趋势 | — | `--days`(默认7), `--size`(默认50) |

### search 参数

| 参数 | 说明 | 示例 |
|------|------|------|
| `--keyword` | 搜索话题 | `--keyword "手机"` |
| `--is-trending` | 仅热门话题 | `--is-trending` |
| `--sort-by` | 排序字段 | `video_count`(默认), `total_interaction` |

### trending 参数

| 参数 | 说明 | 示例 |
|------|------|------|
| `--days` | 天数范围 | `--days 7` |
| `--size` | 返回数量 | `--size 100` |


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