#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import datetime
import html
import json
import os
import re
import sys
import time
import uuid
import urllib.parse
import urllib.request

WORKSPACE = os.path.dirname(os.path.abspath(__file__))
OUTPUT_PATH = os.path.join(WORKSPACE, "ai-learning-videos.json")
LIFE_DATA_PATH = os.path.join(WORKSPACE, "life-data.json")
MIN_DAILY_VIDEOS = 10
RECENT_WINDOW_DAYS = 183

STAGES = [
    {
        "name": "第1阶段：AI基础使用与提示词",
        "goal": "用 ChatGPT、豆包、Kimi 等工具完成清晰提问、改写、总结和模板化输出。",
        "queries": [
            "ChatGPT 提示词 入门 办公 实操 教程",
            "豆包 AI 使用 教程 新手 提示词",
            "Kimi AI 使用 教程 办公 总结",
            "AI 提示词 公式 新手 教程",
            "ChatGPT 新手 入门 实操 提示词",
        ],
        "track": "AI基础使用与提示词",
    },
    {
        "name": "第2阶段：AI办公自动化",
        "goal": "用 AI 处理周报、会议纪要、邮件、PPT 大纲等高频办公任务。",
        "queries": [
            "AI 办公自动化 实操 教程 周报",
            "ChatGPT 办公 自动化 Excel Word PPT 教程",
            "Kimi 会议纪要 周报 AI办公 教程",
            "飞书 多维表格 AI 自动化 教程",
        ],
        "track": "AI办公自动化",
    },
    {
        "name": "第3阶段：AI文案/小红书/短视频脚本",
        "goal": "用 AI 生成标题、选题、脚本和内容模板，完成可发布的内容草稿。",
        "queries": [
            "AI 小红书 文案 实操 教程",
            "AI 短视频脚本 保姆级 教程",
            "ChatGPT 爆款标题 文案 模板",
            "豆包 小红书 文案 视频脚本 教程",
        ],
        "track": "AI内容生产",
    },
    {
        "name": "第4阶段：AI图片/电商素材",
        "goal": "用 AI 图片工具生成商品主图、封面、海报和电商素材。",
        "queries": [
            "AI 商品主图 电商素材 实操 教程",
            "即梦 AI 图片 电商 教程",
            "Canva AI 海报 商品图 教程",
            "AI 绘画 电商素材 保姆级 教程",
        ],
        "track": "AI设计/电商素材",
    },
    {
        "name": "第5阶段：AI表格/数据整理",
        "goal": "用 AI 清洗表格、整理数据、生成分析摘要和固定模板。",
        "queries": [
            "AI Excel 数据整理 实操 教程",
            "ChatGPT Excel 表格 自动化 教程",
            "AI 数据清洗 表格 分析 教程",
            "飞书多维表格 AI 数据整理 教程",
        ],
        "track": "AI数据整理",
    },
    {
        "name": "第6阶段：AI低代码/自动化工作流",
        "goal": "用扣子、Coze、低代码工具搭建简单工作流，不涉及复杂部署。",
        "queries": [
            "扣子 Coze 工作流 入门 实操 教程",
            "Coze 智能体 工作流 保姆级 教程",
            "AI 自动化工作流 从0到1 教程",
            "低代码 AI 工作流 实战 教程",
        ],
        "track": "AI低代码/自动化工作流",
    },
    {
        "name": "第7阶段：AI变现案例拆解",
        "goal": "拆解可复制的小项目，把前面学过的技能组合成可交付作品。",
        "queries": [
            "AI 变现 案例拆解 实操 教程",
            "AI 副业 实操 案例 复盘",
            "AI 小项目 变现 从0到1 教程",
            "AI 接单 作品集 实操 教程",
        ],
        "track": "AI变现案例",
    },
]

MONETIZATION_QUERIES = [
    "AI 变现 案例拆解 实操 教程",
    "AI 副业 实操 案例 复盘",
    "AI 小红书 变现 实操",
]

BEGINNER_FALLBACK_VIDEOS = [
    ("吴恩达最新《面向开发者的ChatGPT提示工程》", "https://www.bilibili.com/video/BV1e8411o7NP/", "AI基础使用与提示词"),
    ("AI基础课01：ChatGPT Prompt Engineering实战，中文提示工程教学教程", "https://www.bilibili.com/video/BV1g24y1L7WX/", "AI基础使用与提示词"),
    ("新手友好：KIMI AI保姆级教程，7分钟掌握AI助手！", "https://www.bilibili.com/video/BV1MYVJeFECx/", "AI基础使用与提示词"),
    ("还能这么用？超全！豆包AI 使用指南", "https://www.bilibili.com/video/BV158kKBLErq/", "AI基础使用与提示词"),
    ("手把手教你玩转豆包AI，豆包AI全功能实测", "https://www.bilibili.com/video/BV17YQqYqEGh/", "AI基础使用与提示词"),
    ("豆包AI注册使用教程：下载豆包免费智能app的方法", "https://www.bilibili.com/video/BV11m87efESo/", "AI基础使用与提示词"),
    ("CHATGPT从小白到精通AI神器+OFFICE全家桶", "https://www.bilibili.com/video/BV1FCyGBmEBd/", "AI办公自动化"),
    ("打工人必看！AI一键生成PPT太强了", "https://www.bilibili.com/video/BV1LzGC6UENM/", "AI办公自动化"),
    ("免费、全能，打工人无法拒绝的AI工具，赶超GPT的中文大模型Kimi", "https://www.bilibili.com/video/BV1LB421z7Nt/", "AI办公自动化"),
    ("ChatGPT教程：35个我希望早点知道的技巧", "https://www.bilibili.com/video/BV1NbHRzBEdv/", "AI基础使用与提示词"),
]

RECENT_VALIDATED_SEED_VIDEOS = [
    ("还能这么用？超全！豆包AI 使用指南", "https://www.bilibili.com/video/BV158kKBLErq/", "AI基础使用与提示词", "搜索结果显示约 6 个月内发布"),
    ("不知道上哪玩AI？四个运用场景帮你推荐！", "https://www.bilibili.com/video/BV1AdPHzFEsB/", "AI基础使用与提示词", "搜索结果显示约 5 个月内发布"),
    ("2026豆包指令85+提示词合集包：分类清晰，小白也可轻松使用", "https://www.bilibili.com/video/BV1CMQEBnEeU/", "AI基础使用与提示词", "搜索结果显示约 3 个月内发布"),
    ("2026豆包AI全能实战课：基础功能解析", "https://www.bilibili.com/video/BV12gNTz4EB1/", "AI基础使用与提示词", "搜索结果显示约 4 个月内发布"),
]

PRACTICAL_KEYWORDS = (
    "教程", "实操", "案例", "工作流", "自动化", "从0到1", "从零到一", "保姆级",
    "模板", "脚本", "复盘", "教学", "实战", "演示", "手把手",
)
TOOL_KEYWORDS = (
    "ChatGPT", "豆包", "Kimi", "Coze", "扣子", "剪映", "Canva", "即梦", "可灵",
    "Excel", "飞书", "多维表格", "PPT", "Word", "Codex",
)
LOW_VALUE_KEYWORDS = (
    "焦虑", "割韭菜", "暴富", "月入百万", "躺赚", "收徒", "训练营", "私域课",
    "卖课", "加盟", "骗局", "资讯", "新闻", "发布会",
)
STAGE_BEGINNER_BLOCKLIST = (
    "Codex", "Cursor", "Coze", "扣子", "智能体", "Agent", "RAG", "LangChain",
    "部署", "开发", "编程", "代码", "工作流", "Sora", "即梦", "可灵", "短片",
    "电影", "影视", "漫剧", "漫画", "数字人", "全套", "全36集", "全30集",
    "AI视频", "视频提示词", "分镜", "故事板", "剧本", "GPT-Image", "ComfyUI",
    "AI绘画", "绘画", "生图", "图片", "图像", "海报", "Midjourney", "识图", "插件",
    "国赛", "竞赛", "数学建模", "论文", "科研", "提示词赚钱",
    "新媒体", "小红书", "抖音", "直播", "电商", "运营", "起号", "涨粉",
    "音乐", "Suno", "图片", "文生图", "图生图", "无水印", "下载",
    "系统课", "通关课", "必修课", "训练营", "马士兵",
    "邪修", "番茄", "签约", "签一篇", "过签", "结算", "战绩", "就业",
    "天花板", "最全", "出图", "参考图", "论文", "essay",
    "白嫖", "百万年薪", "终极", "小说", "完整版", "资源", "GPT5.6", "GPT 5.6", "ChatGPT 5.6",
    "今日头条", "情感赛道", "Seedance", "生成视频", "生图", "制图", "绘图",
    "广告", "设计师", "skill", "Skill", "违规", "欺骗",
)


def now_dt():
    return datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8)))


def now_str():
    return now_dt().strftime("%Y-%m-%d %H:%M:%S")


def browser_headers(referer="https://search.bilibili.com/"):
    return {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
        "Referer": referer,
        "Origin": "https://search.bilibili.com",
        "Accept": "application/json, text/plain, */*",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
        "Cookie": f"buvid3={uuid.uuid4().hex.upper()}infoc; b_nut={int(time.time())}; CURRENT_FNVAL=4048;",
    }


def fetch_json(url, timeout=15, referer="https://search.bilibili.com/"):
    req = urllib.request.Request(url, headers=browser_headers(referer))
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.loads(resp.read().decode("utf-8", "ignore"))


def clean_title(title):
    title = re.sub(r"<em[^>]*>|</em>", "", title or "", flags=re.I)
    title = re.sub(r"<[^>]+>", "", title)
    return html.unescape(re.sub(r"\s+", " ", title)).strip()


def normalize_number(value):
    if value in (None, "", "-"):
        return None
    if isinstance(value, (int, float)):
        return int(value)
    text = str(value).strip().lower()
    try:
        if text.endswith("万"):
            return int(float(text[:-1]) * 10000)
        if text.endswith("亿"):
            return int(float(text[:-1]) * 100000000)
        return int(float(text))
    except Exception:
        return None


def load_completed_count():
    try:
        with open(LIFE_DATA_PATH, "r", encoding="utf-8-sig") as f:
            data = json.load(f)
        history = ((data.get("aiLearn") or {}).get("history") or [])
        return len([h for h in history if isinstance(h, dict) and h.get("done")])
    except Exception:
        return 0


def get_completed_count_from_args():
    for arg in sys.argv[1:]:
        if arg.startswith("--completed-count="):
            try:
                return max(0, int(arg.split("=", 1)[1]))
            except Exception:
                return None
    return None


def get_refresh_offset_from_args():
    for arg in sys.argv[1:]:
        if arg.startswith("--refresh-offset="):
            try:
                return max(0, int(arg.split("=", 1)[1]))
            except Exception:
                return 0
    return 0


def stage_for_count(completed_count):
    idx = min(len(STAGES) - 1, max(0, int(completed_count or 0) // 7))
    return idx, STAGES[idx]


def get_published_dt(pubdate):
    if not pubdate:
        return None
    try:
        return datetime.datetime.fromtimestamp(int(pubdate), tz=datetime.timezone(datetime.timedelta(hours=8)))
    except Exception:
        return None


def is_recent_enough(pubdate):
    published = get_published_dt(pubdate)
    if not published:
        return False
    return 0 <= (now_dt() - published).days <= RECENT_WINDOW_DAYS


def format_published_at(pubdate):
    published = get_published_dt(pubdate)
    return published.strftime("%Y-%m-%d") if published else None


def get_freshness_label(pubdate, is_classic=False):
    if is_classic:
        return "经典教程补足"
    published = get_published_dt(pubdate)
    if not published:
        return "发布时间未知"
    days = max(0, (now_dt() - published).days)
    if days <= 7:
        return "近 7 天"
    if days <= 30:
        return "近 30 天"
    if days <= RECENT_WINDOW_DAYS:
        return "最近半年"
    return "超过半年"


def practical_score(title, tags="", pubdate=None):
    text = f"{title} {tags}"
    score = 0
    score += sum(4 for k in PRACTICAL_KEYWORDS if k.lower() in text.lower())
    score += sum(3 for k in TOOL_KEYWORDS if k.lower() in text.lower())
    score -= sum(6 for k in LOW_VALUE_KEYWORDS if k in text)
    if is_recent_enough(pubdate):
        published = get_published_dt(pubdate)
        days = (now_dt() - published).days if published else RECENT_WINDOW_DAYS
        score += max(4, 12 - min(days, RECENT_WINDOW_DAYS) // 20)
    if any(k in text for k in ("入门", "新手", "小白", "零基础", "0基础")):
        score += 4
    return score


def is_stage_appropriate(title, stage_index):
    text = title or ""
    if stage_index == 0:
        if len(re.sub(r"\s+", "", text)) < 8:
            return False
        if any(k.lower() in text.lower() for k in STAGE_BEGINNER_BLOCKLIST):
            return False
        if re.search(r"全\d+[集讲]|[1-9]\d{2,}\s*集", text):
            return False
        has_tool = any(k.lower() in text.lower() for k in ("chatgpt", "豆包", "kimi", "提示词"))
        has_simple_task = any(k in text for k in ("新手", "入门", "办公", "总结", "写作", "提问", "提示词", "效率", "小白", "使用", "指南", "功能", "基础"))
        return has_tool and has_simple_task
    if stage_index <= 1:
        if any(k.lower() in text.lower() for k in ("部署", "模型训练", "微调", "langchain", "rag")):
            return False
    return True


def infer_difficulty(title, stage_index):
    text = title or ""
    if stage_index <= 1 or any(k in text for k in ("入门", "新手", "小白", "零基础", "保姆级")):
        return "入门"
    if any(k in text for k in ("进阶", "高级", "复杂", "部署", "开发")):
        return "进阶"
    return "初级"


def infer_track(title, default_track):
    text = title or ""
    if any(k in text for k in ("办公", "周报", "会议", "PPT", "Word", "飞书")):
        return "AI办公自动化"
    if any(k in text for k in ("小红书", "文案", "脚本", "短视频", "标题")):
        return "AI内容生产"
    if any(k in text for k in ("图片", "海报", "商品图", "主图", "电商", "Canva", "即梦")):
        return "AI设计/电商素材"
    if any(k in text for k in ("Excel", "表格", "数据", "多维表格")):
        return "AI数据整理"
    if any(k in text for k in ("Coze", "扣子", "工作流", "自动化", "低代码")):
        return "AI低代码/自动化工作流"
    if any(k in text for k in ("变现", "副业", "接单", "案例拆解")):
        return "AI变现案例"
    return default_track


def build_learning_fields(title, stage, priority):
    track = infer_track(title, stage["track"])
    if track == "AI办公自动化":
        task = "跟着视频做一份可复用的办公提示词或自动化流程。"
        output = "一个可复制的周报/会议纪要/邮件处理模板"
    elif track == "AI内容生产":
        task = "用视频里的方法生成 5 个标题和 1 段短视频脚本。"
        output = "一套小红书标题模板或短视频脚本草稿"
    elif track == "AI设计/电商素材":
        task = "按视频流程做一张商品图、封面或海报。"
        output = "一张可发布的商品主图或内容封面"
    elif track == "AI数据整理":
        task = "用示例表格复现一次清洗、分类或摘要流程。"
        output = "一份整理后的表格和操作提示词"
    elif track == "AI低代码/自动化工作流":
        task = "搭一个最小可用工作流，只保留输入、处理、输出三步。"
        output = "一个可运行的简单 AI 工作流"
    elif track == "AI变现案例":
        task = "拆解视频中的交付物、客户、流程和报价方式。"
        output = "一份 AI 变现案例复盘表"
    else:
        task = "把视频中的提示词方法复现一遍，并保存可复用模板。"
        output = "一个可复用的 AI 提示词模板"
    return {
        "track": track,
        "whyWatch": "标题包含明确实操/教程/工具线索，适合按步骤跟做，不是纯概念内容。",
        "practicalValue": f"看完应能掌握：{task.replace('跟着视频', '').replace('按视频流程', '').replace('用视频里的方法', '').strip()}",
        "practiceTask": task,
        "outputExample": output,
        "priority": priority,
    }


def collect_bilibili(stage_index, stage):
    beginner_monetization = ["ChatGPT 办公 变现 案例", "AI 提示词 变现 案例", "Kimi AI 办公 效率 案例"]
    queries = list(stage["queries"]) + (beginner_monetization if stage_index == 0 else MONETIZATION_QUERIES)
    results = []
    seen = set()
    for query in queries:
        for order in ("pubdate", "totalrank"):
          for page in (1, 2, 3):
            url = "https://api.bilibili.com/x/web-interface/search/type?" + urllib.parse.urlencode(
                {"search_type": "video", "keyword": query, "page": page, "page_size": 30, "order": order}
            )
            try:
                data = fetch_json(url, referer="https://search.bilibili.com/all?keyword=" + urllib.parse.quote(query))
            except Exception:
                continue
            if data.get("code") != 0:
                continue
            for item in (data.get("data") or {}).get("result") or []:
                title = clean_title(item.get("title"))
                bvid = item.get("bvid") or ""
                video_url = f"https://www.bilibili.com/video/{bvid}/" if bvid else str(item.get("arcurl") or "").replace("http://", "https://")
                if not title or "bilibili.com/video" not in video_url or video_url in seen:
                    continue
                if not is_stage_appropriate(title, stage_index):
                    continue
                if not is_recent_enough(item.get("pubdate")):
                    continue
                tag_text = str(item.get("tag") or "")
                score = practical_score(title, tag_text, item.get("pubdate"))
                if score < 4:
                    continue
                plays = normalize_number(item.get("play"))
                likes = normalize_number(item.get("like"))
                rec = {
                    "id": bvid or re.sub(r"\W+", "", video_url)[-20:],
                    "platform": "B站",
                    "title": title,
                    "url": video_url,
                    "difficulty": infer_difficulty(title, stage_index),
                    "plays": plays,
                    "playCount": plays,
                    "likes": likes,
                    "likeCount": likes,
                    "fans": None,
                    "followerCount": None,
                    "author": item.get("author") or "",
                    "duration": item.get("duration") or None,
                    "tags": [t for t in re.split(r"[,，#\s]+", tag_text) if t][:5],
                    "capturedAt": now_str(),
                    "sourceQuery": query,
                    "pubdate": item.get("pubdate") or None,
                    "publishedAt": format_published_at(item.get("pubdate")),
                    "freshnessLabel": get_freshness_label(item.get("pubdate")),
                    "isClassic": False,
                    "score": score,
                    "completed": False,
                    "completedAt": None,
                    "notes": "B站公开搜索接口抓取；粉丝数未稳定获取时保留为 null。",
                }
                results.append(rec)
                seen.add(video_url)
    results.sort(key=lambda v: (v.get("score") or 0, v.get("plays") or 0, v.get("likes") or 0), reverse=True)
    return results


def dedupe(videos):
    out = []
    seen_urls = set()
    seen_titles = set()
    for video in videos:
        if video.get("platform") == "快手":
            continue
        url = str(video.get("url") or "")
        if "kuaishou.com" in url.lower() or "gifshow.com" in url.lower():
            continue
        title = clean_title(video.get("title"))
        key_title = re.sub(r"\W+", "", title.lower())[:34]
        if not url or not title or url in seen_urls or key_title in seen_titles:
            continue
        video["title"] = title
        out.append(video)
        seen_urls.add(url)
        seen_titles.add(key_title)
    return out


def rotate_items(items, refresh_offset, step=8):
    if not items:
        return []
    start = (max(0, int(refresh_offset or 0)) * step) % len(items)
    return items[start:] + items[:start]


def assign_priorities(videos, stage, refresh_offset=0):
    fresh_videos = [v for v in videos if not v.get("isClassic")]
    classic_videos = [v for v in videos if v.get("isClassic")]
    monetization = [v for v in fresh_videos if any(k in v.get("title", "") for k in ("变现", "副业", "接单", "案例", "复盘"))]
    normal = [v for v in fresh_videos if v not in monetization]
    normal = rotate_items(normal, refresh_offset, step=8)
    monetization = rotate_items(monetization, refresh_offset, step=2)
    classic_videos = rotate_items(classic_videos, refresh_offset, step=4)
    selected = []
    selected.extend(normal[:3])
    selected.extend(normal[3:8])
    selected.extend(monetization[:2])
    for v in fresh_videos + classic_videos:
        if len(selected) >= MIN_DAILY_VIDEOS:
            break
        if v not in selected:
            selected.append(v)
    selected = selected[:MIN_DAILY_VIDEOS]
    for idx, video in enumerate(selected):
        if video.get("isClassic"):
            priority = "经典教程"
        else:
            priority = "今日必看" if idx < 3 else ("可选拓展" if idx < 8 else "变现案例")
        video.update(build_learning_fields(video.get("title", ""), stage, priority))
    return selected


def fallback_beginner_videos(stage):
    captured = now_str()
    videos = []
    for title, url, track in BEGINNER_FALLBACK_VIDEOS:
        videos.append({
            "id": url.rstrip("/").split("/")[-1],
            "platform": "B站",
            "title": title,
            "url": url,
            "difficulty": "入门",
            "plays": None,
            "playCount": None,
            "likes": None,
            "likeCount": None,
            "fans": None,
            "followerCount": None,
            "author": None,
            "duration": None,
            "tags": ["AI学习", "提示词", "新手入门"],
            "capturedAt": captured,
            "sourceQuery": "public search fallback",
            "pubdate": None,
            "publishedAt": None,
            "freshnessLabel": "经典教程补足",
            "isClassic": True,
            "score": 1,
            "completed": False,
            "completedAt": None,
            "notes": "经典教程补足：当最近半年内可验证实操视频不足时使用；公开视频指标未稳定获取时保留为 null。",
            "track": track,
        })
    return videos


def recent_validated_seed_videos(stage):
    captured = now_str()
    videos = []
    for title, url, track, freshness_note in RECENT_VALIDATED_SEED_VIDEOS:
        if not is_stage_appropriate(title, 0):
            continue
        videos.append({
            "id": url.rstrip("/").split("/")[-1],
            "platform": "B站",
            "title": title,
            "url": url,
            "difficulty": "入门",
            "plays": None,
            "playCount": None,
            "likes": None,
            "likeCount": None,
            "fans": None,
            "followerCount": None,
            "author": None,
            "duration": None,
            "tags": ["AI学习", "提示词", "新手入门"],
            "capturedAt": captured,
            "sourceQuery": "validated recent search seed",
            "pubdate": None,
            "publishedAt": None,
            "freshnessLabel": "最近半年（搜索验证）",
            "isClassic": False,
            "validatedRecent": True,
            "score": 18,
            "completed": False,
            "completedAt": None,
            "notes": f"{freshness_note}；公开视频指标未稳定获取时保留为 null。",
            "track": track,
        })
    return videos


def main():
    arg_count = get_completed_count_from_args()
    refresh_offset = get_refresh_offset_from_args()
    completed_count = load_completed_count() if arg_count is None else arg_count
    stage_index, stage = stage_for_count(completed_count)
    notes = []
    videos = []
    try:
        videos = collect_bilibili(stage_index, stage)
    except Exception as e:
        notes.append(f"B站公开搜索抓取失败：{e}")
    videos = dedupe(videos)
    if stage_index == 0 and len(videos) < MIN_DAILY_VIDEOS:
        notes.append("B站接口结果不足时，补入最近半年内搜索结果可验证的 B站新手实操视频；发布时间只有相对时间时不伪造具体日期。")
        videos = dedupe(videos + recent_validated_seed_videos(stage))
    if stage_index == 0 and len(videos) < MIN_DAILY_VIDEOS:
        notes.append("最近半年内的 B站可验证实操视频不足，已用经典教程补足并在卡片标注“经典教程补足”。")
        videos = dedupe(videos + fallback_beginner_videos(stage))
    videos = assign_priorities(videos, stage, refresh_offset=refresh_offset)
    if len(videos) < MIN_DAILY_VIDEOS:
        notes.append(f"B站可验证实操视频不足 {MIN_DAILY_VIDEOS} 条，本次仅保留 {len(videos)} 条真实链接。")
    notes.append("小红书/抖音公开搜索结果稳定性较差，未拿到可验证真实视频链接时不写入；不推荐快手。")
    notes.append(f"常规推荐只保留最近 {RECENT_WINDOW_DAYS} 天内的视频；已过滤明显卖课、标题党、纯资讯、纯概念或无实操指向的内容；播放量/点赞/作者拿不到时保留 null。")
    data = {
        "updatedAt": now_str(),
        "source": "daily automation via public web search",
        "track": "AI新手变现实操路线",
        "stage": stage["name"],
        "stageIndex": stage_index,
        "completedCountBasis": completed_count,
        "refreshOffset": refresh_offset,
        "goal": stage["goal"],
        "minDailyVideos": MIN_DAILY_VIDEOS,
        "notes": "；".join(notes),
        "videos": videos,
    }
    with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
    print(json.dumps({"ok": len(videos) > 0, "count": len(videos), "path": OUTPUT_PATH, "stage": stage["name"]}, ensure_ascii=False))


if __name__ == "__main__":
    main()
