{
  "format": "mosael-workflow",
  "version": 1,
  "workflow_revision": 1,
  "name": "自媒体视频爆款拆解",
  "description": "贴一条抖音、小红书或 B站视频的链接,取视频数据和赞最多的评论,下载视频、转写带时间码的口播,拆解它为什么火(选题、前 3 秒钩子、结构节奏、情绪点、标题封面、评论区反馈、可复用的套路),并给一份「照这个套路做一条」的脚本提纲,存成笔记。数据来源在开始节点的下拉里二选一:内嵌浏览器(不用配置、不花钱,要登录的平台换成已登录的浏览器档案)或 TikHub(要装插件、填密钥,每次运行 2 次请求、每次约 0.001 美元起;字段全、更稳、不用登录)。下载用的是素材库的「从链接导入」(抖音一般要在「下载视频进素材库」上选已登录的浏览器档案),下不到时照样按数据和评论拆;转写在本机跑;另有 1–2 次 AI 对话。暂不看画面(没有抽关键帧)。",
  "graph_hash": "25268c35c0698d5f6ee542875e4f477651cfbc7981b9a03ef78cee413f489adf",
  "graph": {
    "meta": {
      "template_id": "viral_video_breakdown",
      "template_version": 5,
      "source": "official"
    },
    "nodes": [
      {
        "id": "start",
        "type": "start",
        "name": "填视频与数据来源",
        "position": {
          "x": 40,
          "y": 535
        },
        "config": {
          "params": {
            "video_link": "",
            "data_source": "",
            "platform": "",
            "comment_count": 30,
            "download_video": "yes",
            "my_topic": "",
            "report_language": "中文"
          },
          "required_params": [
            "video_link",
            "data_source"
          ],
          "param_options": {
            "data_source": [
              {
                "value": "browser",
                "label": "内嵌浏览器",
                "description": "不用配置、不花钱;要登录才看得到的页面(小红书一定要、抖音多半要),在「用内嵌浏览器打开」里换成浏览器池里已登录的档案。B 站评论不登录只给前 3 条一级评论(每条下前 20 条回复),要取全也换成已登录 B 站的档案。页面上的数字是约数,发布时间常常缺"
              },
              {
                "value": "tikhub",
                "label": "TikHub",
                "description": "需安装 TikHub 插件、接好连接并填上 API 密钥(按次计费);数据更全更稳、不用登录",
                "requires": "tikhub_video"
              }
            ]
          }
        }
      },
      {
        "id": "link",
        "type": "social_link",
        "name": "认出平台和编号",
        "position": {
          "x": 360,
          "y": 535
        },
        "config": {
          "link": "{{start.video_link}}",
          "platform": "{{start.platform}}",
          "expect": "video"
        }
      },
      {
        "id": "use_tikhub",
        "type": "condition",
        "name": "用 TikHub 取数吗",
        "position": {
          "x": 1000,
          "y": 535
        },
        "config": {
          "left": "{{start.data_source}}",
          "op": "equals",
          "right": "tikhub"
        }
      },
      {
        "id": "tk_is_dy",
        "type": "condition",
        "name": "是抖音吗",
        "position": {
          "x": 1320,
          "y": 60
        },
        "config": {
          "left": "",
          "op": "equals",
          "right": "douyin"
        },
        "inputs": [
          "left"
        ]
      },
      {
        "id": "dy_video",
        "type": "plugin_tool",
        "name": "TikHub 取抖音视频详情",
        "position": {
          "x": 1640,
          "y": 60
        },
        "config": {
          "plugin_id": "dev.mosael.tikhub",
          "tool_name": "douyin_web_fetch_one_video",
          "instance_id": "",
          "input": {
            "aweme_id": "{{link.id}}"
          }
        }
      },
      {
        "id": "dy_comments",
        "type": "plugin_tool",
        "name": "TikHub 取抖音评论",
        "position": {
          "x": 1640,
          "y": 145
        },
        "config": {
          "plugin_id": "dev.mosael.tikhub",
          "tool_name": "douyin_web_fetch_video_comments",
          "instance_id": "",
          "input": {
            "aweme_id": "{{link.id}}",
            "count": "{{start.comment_count}}"
          }
        }
      },
      {
        "id": "dy_video_m",
        "type": "social_metrics",
        "name": "整理视频数据",
        "position": {
          "x": 1960,
          "y": 60
        },
        "config": {
          "data": "",
          "kind": "posts",
          "limit": 1,
          "duration_unit": "milliseconds"
        },
        "inputs": [
          "data"
        ]
      },
      {
        "id": "dy_comments_m",
        "type": "social_metrics",
        "name": "按赞排好评论",
        "position": {
          "x": 1960,
          "y": 145
        },
        "config": {
          "data": "",
          "kind": "comments",
          "limit": "{{start.comment_count}}"
        },
        "inputs": [
          "data"
        ]
      },
      {
        "id": "tk_is_xhs",
        "type": "condition",
        "name": "是小红书吗",
        "position": {
          "x": 1320,
          "y": 250
        },
        "config": {
          "left": "",
          "op": "equals",
          "right": "xiaohongshu"
        },
        "inputs": [
          "left"
        ]
      },
      {
        "id": "xhs_video",
        "type": "plugin_tool",
        "name": "TikHub 取小红书视频详情",
        "position": {
          "x": 1640,
          "y": 250
        },
        "config": {
          "plugin_id": "dev.mosael.tikhub",
          "tool_name": "xiaohongshu_app_v2_get_video_note_detail",
          "instance_id": "",
          "input": {
            "note_id": "{{link.id}}",
            "share_text": "{{link.url}}"
          }
        }
      },
      {
        "id": "xhs_comments",
        "type": "plugin_tool",
        "name": "TikHub 取小红书评论",
        "position": {
          "x": 1640,
          "y": 335
        },
        "config": {
          "plugin_id": "dev.mosael.tikhub",
          "tool_name": "xiaohongshu_app_v2_get_note_comments",
          "instance_id": "",
          "input": {
            "note_id": "{{link.id}}",
            "share_text": "{{link.url}}"
          }
        }
      },
      {
        "id": "xhs_video_m",
        "type": "social_metrics",
        "name": "整理视频数据",
        "position": {
          "x": 1960,
          "y": 250
        },
        "config": {
          "data": "",
          "kind": "posts",
          "limit": 1,
          "duration_unit": "auto"
        },
        "inputs": [
          "data"
        ]
      },
      {
        "id": "xhs_comments_m",
        "type": "social_metrics",
        "name": "按赞排好评论",
        "position": {
          "x": 1960,
          "y": 335
        },
        "config": {
          "data": "",
          "kind": "comments",
          "limit": "{{start.comment_count}}"
        },
        "inputs": [
          "data"
        ]
      },
      {
        "id": "tk_is_bili",
        "type": "condition",
        "name": "是B站吗",
        "position": {
          "x": 1320,
          "y": 440
        },
        "config": {
          "left": "",
          "op": "equals",
          "right": "bilibili"
        },
        "inputs": [
          "left"
        ]
      },
      {
        "id": "bili_video",
        "type": "plugin_tool",
        "name": "TikHub 取B站视频详情",
        "position": {
          "x": 1640,
          "y": 440
        },
        "config": {
          "plugin_id": "dev.mosael.tikhub",
          "tool_name": "bilibili_web_fetch_one_video",
          "instance_id": "",
          "input": {
            "bv_id": "{{link.id}}"
          }
        }
      },
      {
        "id": "bili_comments",
        "type": "plugin_tool",
        "name": "TikHub 取B站评论",
        "position": {
          "x": 1640,
          "y": 525
        },
        "config": {
          "plugin_id": "dev.mosael.tikhub",
          "tool_name": "bilibili_web_fetch_video_comments",
          "instance_id": "",
          "input": {
            "bv_id": "{{link.id}}",
            "pn": "1"
          }
        }
      },
      {
        "id": "bili_video_m",
        "type": "social_metrics",
        "name": "整理视频数据",
        "position": {
          "x": 1960,
          "y": 440
        },
        "config": {
          "data": "",
          "kind": "posts",
          "limit": 1,
          "duration_unit": "auto"
        },
        "inputs": [
          "data"
        ]
      },
      {
        "id": "bili_comments_m",
        "type": "social_metrics",
        "name": "按赞排好评论",
        "position": {
          "x": 1960,
          "y": 525
        },
        "config": {
          "data": "",
          "kind": "comments",
          "limit": "{{start.comment_count}}",
          "unescape_html": "yes"
        },
        "inputs": [
          "data"
        ]
      },
      {
        "id": "tk_unsupported",
        "type": "notify",
        "name": "TikHub 这一路不认这个平台",
        "position": {
          "x": 1640,
          "y": 630
        },
        "config": {
          "title": "没有开始分析:TikHub 这一路不支持这个平台",
          "body": "认出的平台是「{{link.platform}}」。TikHub 这一路接了抖音、小红书、B站;别的平台在开始节点的数据来源(data_source)里改选「内嵌浏览器」再运行。认错了的话,在开始节点的 platform 里写明平台。"
        }
      },
      {
        "id": "web_open",
        "type": "browser_open",
        "name": "用内嵌浏览器打开",
        "position": {
          "x": 1320,
          "y": 858
        },
        "config": {
          "url": "",
          "session_mode": "named",
          "session_name": "自媒体分析"
        },
        "inputs": [
          "url"
        ]
      },
      {
        "id": "web_read",
        "type": "browser_evaluate",
        "name": "往下滚几屏,读出页面文字",
        "position": {
          "x": 1640,
          "y": 858
        },
        "config": {
          "session": "",
          "expression": "(async () => {\n  const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n  const BILI = /bilibili\\.com\\/video\\/(BV[\\w]+)/;\n  const bili = location.href.match(BILI) || String(input.video_url || \"\").match(BILI);\n  if (bili) {\n    //: B 站:页面上下文直接调它自己的接口 —— 带登录态、结构化 JSON、分页器和楼中楼都在里面,\n    //: 评论区组件渲染失败(风控「玩命加载」)也不受影响。接口变了就落空,落回下面读 DOM 那条路。\n    try {\n      const getJson = async (url) => (await fetch(url, { credentials: \"include\" })).json();\n      const started = Date.now();\n      //: 时间预算要落在节点的脚本上限(timeout_ms)之内;页与页之间隔一下,不踩频控。\n      const budget = Number(input.budget_ms) || 15000;\n      const pause = input.page_pause_ms == null ? 400 : Number(input.page_pause_ms);\n      const outOfTime = () => Date.now() - started > budget;\n      const view = await getJson(\"https://api.bilibili.com/x/web-interface/view?bvid=\" + bili[1]);\n      const v = view && view.data;\n      if (v && v.aid) {\n        const total = v.stat ? Number(v.stat.reply) || 0 : 0;\n        const nav = await getJson(\"https://api.bilibili.com/x/web-interface/nav\").catch(() => null);\n        const loggedIn = Boolean(nav && nav.data && nav.data.isLogin);\n        //: 评论有多少抓多少,上限 comment_max(最多 2000 条)。\n        const wanted = Math.min(Number(input.comment_max) || 1000, 2000);\n        const seen = new Set();\n        const picked = [];\n        let stoppedBy = \"\";\n        //: 同一条评论只算一次(一级里自带的楼中楼预览、置顶,和翻出来的是同一条)。楼中楼拍平、带 ↳ 前缀。\n        const take = (c, isReply) => {\n          const id = c && (c.rpid_str || (c.rpid ? String(c.rpid) : \"\"));\n          if (!id || seen.has(id)) return;\n          seen.add(id);\n          picked.push({ isReply, comment: {\n            author: c.member ? c.member.uname : \"\",\n            //: 原文照搬:换行是原样的一部分;接口交回的 HTML 转义(&#39;)留给整理节点解一次(unescape_html)。\n            text: (isReply ? \"↳ \" : \"\") + ((c.content ? c.content.message : \"\") || \"\").trim(),\n            likes: typeof c.like === \"number\" ? c.like : \"\",\n            published_at: c.ctime ? new Date(c.ctime * 1000).toISOString().slice(0, 10) : \"\",\n          } });\n        };\n        const full = () => {\n          if (picked.length >= wanted) { stoppedBy = \"limit\"; return true; }\n          if (outOfTime()) { stoppedBy = \"time\"; return true; }\n          return false;\n        };\n        //: 一级评论逐页翻到平台不再给(第 1 页另带置顶)。未登录时 B 站只给 1 页 3 条 —— 照实停下,下面说清。\n        const threads = [];\n        for (let pn = 1; !(pn > 1 && full()); pn += 1) {\n          const r = await getJson(\"https://api.bilibili.com/x/v2/reply?type=1&oid=\" + v.aid + \"&sort=2&ps=20&pn=\" + pn);\n          const d = (r && r.data) || {};\n          const replies = d.replies || [];\n          const pinned = pn === 1 ? [...(d.top_replies || []), ...(d.upper && d.upper.top ? [d.upper.top] : [])] : [];\n          for (const one of [...pinned, ...replies]) {\n            take(one, false);\n            for (const sub of one.replies || []) take(sub, true);\n            threads.push(one);\n          }\n          if (replies.length < 20) break;\n          await wait(pause);\n        }\n        //: 楼中楼:一级里只自带前几条预览,回复更多的楼逐页翻 reply/reply,翻到平台不再给。\n        for (const one of threads) {\n          if (stoppedBy || full()) break;\n          if ((one.rcount || 0) <= (one.replies || []).length) continue;\n          for (let pn = 1; !full(); pn += 1) {\n            const r = await getJson(\"https://api.bilibili.com/x/v2/reply/reply?type=1&oid=\" + v.aid + \"&root=\" + (one.rpid_str || one.rpid) + \"&ps=20&pn=\" + pn);\n            const subs = (r && r.data && r.data.replies) || [];\n            for (const sub of subs) take(sub, true);\n            if (subs.length < 20) break;\n            await wait(pause);\n          }\n        }\n        const kept = picked.slice(0, wanted);\n        if (kept.length) {\n          const fetchedReplies = kept.filter((one) => one.isReply).length;\n          //: 取到的比平台说的少,说清是哪一种:没登录(平台只给前几条)、到了条数 / 时间上限、或者平台就是没给\n          //: (被删除、折叠、仅自己可见)。这几句话由模板按界面语言交进来(input.notes)。\n          const notes = input.notes || {};\n          const gap = kept.length >= total ? \"\"\n            : stoppedBy === \"limit\" ? notes.limit\n            : stoppedBy === \"time\" ? notes.time\n            : !loggedIn ? notes.login\n            : notes.platform;\n          //: 视频本身的数据也在 view 接口里(播放、点赞、时长、发布时间……)—— 一并交回,爆款拆解直接整理它。\n          //: 只带整理要用的那几格,原样的 view 返回很大,会白占运行记录。\n          const video = { bvid: v.bvid, aid: v.aid, title: v.title, pubdate: v.pubdate, duration: v.duration, desc: v.desc,\n                          owner: v.owner ? { name: v.owner.name, mid: v.owner.mid } : null, stat: v.stat || {} };\n          return { url: location.href, title: document.title, now: new Date().toISOString(), mode: \"api\", total,\n                   fetched: kept.length, fetched_roots: kept.length - fetchedReplies, fetched_replies: fetchedReplies,\n                   logged_in: loggedIn, gap_note: gap || \"\", expanded: -1, video,\n                   text: JSON.stringify({ total, comments: kept.map((one) => one.comment) }) };\n        }\n      }\n    } catch (e) { /* 接口这条路不通就落回读 DOM */ }\n  }\n  await wait(Number(input.settle_ms) || 2500);\n  const rounds = Math.min(Number(input.scrolls) || 0, 12);\n  let last = -1, stable = 0;\n  for (let i = 0; i < rounds; i += 1) {\n    window.scrollBy(0, Math.max(window.innerHeight, 800));\n    await wait(Number(input.pause_ms) || 1300);\n    const h = document.body.scrollHeight;\n    const atBottom = window.scrollY + window.innerHeight >= h - 4;\n    if (atBottom && h === last) { stable += 1; if (stable >= 2) break; } else { stable = 0; }\n    last = h;\n  }\n  const expandMax = Math.min(Number(input.expand_max) || 0, 20);\n  const expanders = [];\n  if (expandMax > 0) {\n    await wait(Number(input.expand_settle_ms) || 1200);\n    //: 目标站点说什么语言不归界面语言管 —— 两种都认。英文站点(YouTube 等)的折叠钮是\n    //: 「View 12 replies」「Show more replies」;认不出就这轮不展开,读到的仍是顶层评论。\n    const PATTERN = /^(共\\s*\\d+\\s*条回复|展开\\s*\\d*\\s*条回复|展开更多回复|查看回复|view\\s+\\d+\\s+(more\\s+)?repl|show\\s+more\\s+repl)/i;\n    //: 折叠钮的文字可能是「共<em>43</em>条回复」这种带子标签的 —— 不要求叶子,\n    //: 取**最深的**那个匹配(后代里没有再匹配的),点它才对得上点击处理器。\n    const collect = (root) => {\n      root.querySelectorAll(\"*\").forEach((el) => {\n        if (el.shadowRoot) collect(el.shadowRoot);\n        if (expanders.length >= expandMax || el.tagName === \"A\") return;\n        const text = (el.textContent || \"\").trim();\n        if (text.length <= 2 || text.length >= 40 || !PATTERN.test(text)) return;\n        const nested = Array.from(el.children).some((c) => PATTERN.test((c.textContent || \"\").trim()));\n        if (!nested) expanders.push(el);\n      });\n    };\n    collect(document);\n    //: 说明文字旁边另有一个「点击查看」时点那个:B 站的折叠钮是 <span>共5条回复，</span><bili-text-button>点击查看\n    //: </bili-text-button>,点 span 什么也不发生(实测展开报了 8 次、楼中楼一条没出来)。\n    const VIEW = /^(点击查看|查看|展开|view|show)/i;\n    const clickTarget = (el) => Array.from((el.parentElement && el.parentElement.children) || [])\n      .find((one) => one !== el && VIEW.test((one.textContent || \"\").trim())) || el;\n    for (const el of expanders) {\n      const target = clickTarget(el);\n      target.scrollIntoView({ block: \"center\" });\n      target.click();\n      await wait(Number(input.expand_wait_ms) || 600);\n    }\n  }\n  await wait(Number(input.final_ms) || 1500);\n  const SKIP = new Set([\"STYLE\", \"SCRIPT\", \"NOSCRIPT\", \"LINK\", \"META\"]);\n  const parts = [(document.body && document.body.innerText) || \"\"];\n  const visit = (root) => {\n    root.querySelectorAll(\"*\").forEach((el) => {\n      if (!el.shadowRoot) return;\n      const text = Array.from(el.shadowRoot.children)\n        .filter((one) => !SKIP.has(one.tagName))\n        .map((one) => one.innerText || \"\").join(\"\\n\").trim();\n      if (text) parts.push(text);\n      visit(el.shadowRoot);\n    });\n  };\n  visit(document);\n  const limit = Number(input.max_chars) || 30000;\n  return { url: location.href, title: document.title, now: new Date().toISOString(), expanded: expanders.length, text: parts.join(\"\\n\\n\").slice(0, limit) };\n})()",
          "input": {
            "settle_ms": 2500,
            "scrolls": 8,
            "pause_ms": 1200,
            "max_chars": 40000,
            "expand_max": 6,
            "comment_max": 1000,
            "budget_ms": 90000,
            "page_pause_ms": 400,
            "notes": {
              "login": "没登录:B 站对未登录的访问只给前 3 条一级评论、每条下前 20 条回复。要取全,在「用内嵌浏览器打开」上把会话方式换成浏览器池、选一个已登录 B 站的档案。",
              "limit": "到了这次最多取的条数,后面的没有再取。",
              "time": "到了这次取数的时间上限,后面的没有再取。",
              "platform": "其余的 B 站没有给(被删除、折叠或仅自己可见的评论)。"
            }
          },
          "timeout_ms": 120000
        },
        "inputs": [
          "session"
        ]
      },
      {
        "id": "web_close",
        "type": "browser_close",
        "name": "关掉浏览器",
        "position": {
          "x": 1960,
          "y": 1010
        },
        "config": {
          "session": ""
        },
        "inputs": [
          "session"
        ]
      },
      {
        "id": "web_is_api",
        "type": "condition",
        "name": "是接口取的吗",
        "position": {
          "x": 1832,
          "y": 858
        },
        "config": {
          "left": "{{web_read.value.mode}}",
          "op": "equals",
          "right": "api"
        }
      },
      {
        "id": "web_api_video_m",
        "type": "social_metrics",
        "name": "整理接口取回的视频数据",
        "position": {
          "x": 2152,
          "y": 744
        },
        "config": {
          "data": "{{web_read.value.video}}",
          "kind": "posts",
          "limit": 1,
          "duration_unit": "seconds"
        }
      },
      {
        "id": "web_api_comments_m",
        "type": "social_metrics",
        "name": "整理接口取回的评论",
        "position": {
          "x": 2152,
          "y": 820
        },
        "config": {
          "data": "{{web_read.value.text}}",
          "kind": "comments",
          "limit": "{{start.comment_count}}",
          "unescape_html": "yes"
        }
      },
      {
        "id": "web_api_coverage",
        "type": "template",
        "name": "评论取到多少",
        "position": {
          "x": 2152,
          "y": 877
        },
        "config": {
          "template": "评论条数:平台显示 {{web_read.value.total}} 条,这次取到 {{web_read.value.fetched}} 条(一级 {{web_read.value.fetched_roots}} 条、楼中楼 {{web_read.value.fetched_replies}} 条)。{{web_read.value.gap_note}}"
        }
      },
      {
        "id": "web_struct",
        "type": "llm",
        "name": "把页面文字抄成视频数据和评论",
        "position": {
          "x": 1960,
          "y": 933
        },
        "config": {
          "profile_id": "",
          "model": "",
          "preset": "precise",
          "system": "你会收到内嵌浏览器打开一条自媒体视频 / 笔记后读到的页面文字(往下滚过,评论区可能在里面)。把这条视频的数据和页面上能看到的评论整理出来(评论原文一字不改,最多 80 条,赞多的优先)。\n规则:\n- 只抄页面上真有的,不编、不补、不估;页面上没有的字段写空字符串。\n- 数字照页面原样抄(1.2万、3.4w、10万+ 都原样写),不要自己换算。\n- 发布时间:能确定就写成 YYYY-MM-DD(有钟点就 YYYY-MM-DD HH:MM);写的是相对时间(3天前、昨天、2小时前)就按\n  「读取时间」换算成日期;只写了月-日(09-21)就补上读取时间那一年;实在看不出就空字符串。\n- 页面主要是登录框、验证码、「登录后查看更多」时 login_wall = true,并在 notes 里说清看到了什么;\n  这时宁可少抄,不要把推荐位、广告、别人的作品当成这个账号的。\n只输出符合 JSON Schema 的对象。",
          "prompt": "平台(可能为空):{{link.platform}}\n视频:{{link.url}}\n读取时间:{{web_read.value.now}}\n页面标题:{{web_read.value.title}}\n\n页面文字:\n{{web_read.value.text}}",
          "response_format": "json_schema",
          "json_schema_name": "video_page",
          "json_schema": {
            "type": "object",
            "properties": {
              "video": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "标题或文案的开头"
                  },
                  "published_at": {
                    "type": "string",
                    "description": "发布时间,写成 YYYY-MM-DD 或 YYYY-MM-DD HH:MM;看不出就空字符串"
                  },
                  "duration": {
                    "type": "string",
                    "description": "时长,写成 mm:ss 或秒数;没有就空字符串"
                  },
                  "views": {
                    "type": "string",
                    "description": "照页面原样抄(如 1.2万、3456、10万+);页面上没有就空字符串"
                  },
                  "likes": {
                    "type": "string",
                    "description": "照页面原样抄(如 1.2万、3456、10万+);页面上没有就空字符串"
                  },
                  "comments": {
                    "type": "string",
                    "description": "照页面原样抄(如 1.2万、3456、10万+);页面上没有就空字符串"
                  },
                  "collects": {
                    "type": "string",
                    "description": "照页面原样抄(如 1.2万、3456、10万+);页面上没有就空字符串"
                  },
                  "shares": {
                    "type": "string",
                    "description": "照页面原样抄(如 1.2万、3456、10万+);页面上没有就空字符串"
                  },
                  "author": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string",
                    "description": "简介 / 文案 / 话题标签"
                  }
                },
                "required": [
                  "title",
                  "published_at",
                  "duration",
                  "views",
                  "likes",
                  "comments",
                  "collects",
                  "shares",
                  "author",
                  "description"
                ],
                "additionalProperties": false
              },
              "comments": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "text": {
                      "type": "string",
                      "description": "评论原文,一字不改"
                    },
                    "likes": {
                      "type": "string",
                      "description": "照页面原样抄(如 1.2万、3456、10万+);页面上没有就空字符串"
                    },
                    "author": {
                      "type": "string"
                    },
                    "published_at": {
                      "type": "string",
                      "description": "能看出就写 YYYY-MM-DD,否则空字符串"
                    }
                  },
                  "required": [
                    "text",
                    "likes",
                    "author",
                    "published_at"
                  ],
                  "additionalProperties": false
                },
                "maxItems": 80
              },
              "login_wall": {
                "type": "boolean"
              },
              "notes": {
                "type": "string",
                "description": "页面上异常的地方;没有就空字符串"
              }
            },
            "required": [
              "video",
              "comments",
              "login_wall",
              "notes"
            ],
            "additionalProperties": false
          },
          "json_schema_strict": "true",
          "temperature": 0.1,
          "max_tokens": 12000
        }
      },
      {
        "id": "web_video_m",
        "type": "social_metrics",
        "name": "整理视频数据",
        "position": {
          "x": 2280,
          "y": 933
        },
        "config": {
          "data": "{{web_struct.json.video}}",
          "kind": "posts",
          "limit": 1
        }
      },
      {
        "id": "web_comments_m",
        "type": "social_metrics",
        "name": "按赞排好评论",
        "position": {
          "x": 2280,
          "y": 1019
        },
        "config": {
          "data": "{{web_struct.json.comments}}",
          "kind": "comments",
          "limit": "{{start.comment_count}}"
        }
      },
      {
        "id": "data_block",
        "type": "template",
        "name": "汇合这一路的数据",
        "position": {
          "x": 2600,
          "y": 535
        },
        "config": {
          "template": "{{dy_video_m.summary}}{{xhs_video_m.summary}}{{bili_video_m.summary}}{{web_api_video_m.summary}}{{web_video_m.summary}}\n\n### 视频明细\n\n{{dy_video_m.table}}{{xhs_video_m.table}}{{bili_video_m.table}}{{web_api_video_m.table}}{{web_video_m.table}}\n\n{{dy_comments_m.summary}}{{xhs_comments_m.summary}}{{bili_comments_m.summary}}{{web_api_comments_m.summary}}{{web_comments_m.summary}}\n{{web_api_coverage.text}}\n\n{{dy_comments_m.table}}{{xhs_comments_m.table}}{{bili_comments_m.table}}{{web_api_comments_m.table}}{{web_comments_m.table}}"
        }
      },
      {
        "id": "has_video",
        "type": "condition",
        "name": "取到这条视频了吗",
        "position": {
          "x": 2920,
          "y": 535
        },
        "config": {
          "left": "{{dy_video_m.count}}{{xhs_video_m.count}}{{bili_video_m.count}}{{web_api_video_m.count}}{{web_video_m.count}}",
          "op": "gt",
          "right": "0"
        }
      },
      {
        "id": "no_video_notice",
        "type": "notify",
        "name": "没取到这条视频",
        "position": {
          "x": 3240,
          "y": 858
        },
        "config": {
          "title": "爆款拆解没有完成:没取到这条视频的数据",
          "body": "{{link.url}} 没取到数据。常见原因:要登录才看得到(在「用内嵌浏览器打开」上改用浏览器池里已登录的档案);链接不是单条作品;TikHub 的工具没勾选或额度用完。{{web_struct.json.notes}}"
        }
      },
      {
        "id": "want_download",
        "type": "condition",
        "name": "要下载视频转写口播吗",
        "position": {
          "x": 3240,
          "y": 307
        },
        "config": {
          "left": "{{start.download_video}}",
          "op": "not_equals",
          "right": "no"
        }
      },
      {
        "id": "download",
        "type": "import_url",
        "name": "下载视频进素材库",
        "position": {
          "x": 3560,
          "y": 307
        },
        "config": {
          "url": "",
          "kind": "video",
          "profile_id": "",
          "max_height": 720,
          "fail_on_error": "no"
        },
        "inputs": [
          "url"
        ]
      },
      {
        "id": "got_video",
        "type": "condition",
        "name": "下到了吗",
        "position": {
          "x": 3880,
          "y": 307
        },
        "config": {
          "left": "",
          "op": "not_empty"
        },
        "inputs": [
          "left"
        ]
      },
      {
        "id": "transcript",
        "type": "transcribe_asset",
        "name": "转写口播(带时间码)",
        "position": {
          "x": 4200,
          "y": 307
        },
        "config": {
          "asset_id": ""
        },
        "inputs": [
          "asset_id"
        ]
      },
      {
        "id": "transcript_section",
        "type": "template",
        "name": "逐字稿附在笔记后",
        "position": {
          "x": 4520,
          "y": 307
        },
        "config": {
          "template": "## 口播逐字稿\n\n{{transcript.text}}"
        }
      },
      {
        "id": "breakdown",
        "type": "llm",
        "name": "拆解爆款原因并写脚本提纲",
        "position": {
          "x": 4520,
          "y": 535
        },
        "config": {
          "profile_id": "",
          "model": "",
          "preset": "precise",
          "system": "你是短视频爆款拆解专家。你会收到一条视频的数据(代码算好的)、赞最多的评论,以及带时间码的口播逐字稿\n(下载或转写不成功时为空)。拆解它为什么火,再给一份照这个套路做一条的脚本提纲。\n\n- verdict:一句话说清它火的核心原因。\n- report_markdown 用二级标题分成这几节:\n  1. 数据表现:互动率、收藏 / 转发 / 评论各占多少,说明了什么(收藏多 = 有用,转发多 = 社交货币,评论多 = 争议或共鸣);\n  2. 选题:为什么这个题有人看,击中了谁的什么需求;\n  3. 钩子(前 3 秒):引用逐字稿开头的原话分析;没有逐字稿就根据标题、简介和评论推断,并写明是推断;\n  4. 结构与节奏:按时间段拆,每段起什么作用、在哪里转折;\n  5. 情绪点:哪些地方让人想点赞、评论、转发,用评论原文佐证(「」里只放评论原文的逐字摘录(太长可以用……删节,不改字、不把两条拼成一句);归纳、转述、话题名、建议回复的台词都不要加「」);\n  6. 标题与封面文案:用了什么手法;\n  7. 评论区反馈:大家在夸什么、问什么、争什么;\n  8. 可复用的套路:3–5 条,写成可以照做的规则;\n  9. 不能照搬的部分与风险(账号势能、时效、平台规则)。\n- script_outline_markdown:照这个套路做一条新的,主题围绕「{{start.my_topic}}」(为空就挑同领域的一个题):\n  3 个标题备选、前 3 秒钩子台词、分段脚本(时间 / 画面 / 台词)、结尾引导。\n- 只根据给到的材料下结论;没给的数据不要编。数据里「评论条数」那句说了评论取到多少、平台显示多少:只取到一部分时,\n  评论区反馈那一节写明是从取到的这些里看的。\n- 用{{start.report_language}}写。\n只输出符合 JSON Schema 的对象。",
          "prompt": "视频:{{link.url}}\n平台:{{link.platform}}\n数据来源:{{start.data_source}}\n\n{{data_block.text}}\n\n口播逐字稿(带时间码 JSON,可能为空):\n{{transcript.timed_text}}\n\n没有逐字稿的原因(可能为空):{{download.error}}",
          "response_format": "json_schema",
          "json_schema_name": "viral_breakdown",
          "json_schema": {
            "type": "object",
            "properties": {
              "title": {
                "type": "string",
                "description": "笔记标题:带上视频标题和「爆款拆解」"
              },
              "verdict": {
                "type": "string",
                "description": "一句话:它火的核心原因"
              },
              "report_markdown": {
                "type": "string",
                "description": "完整拆解,Markdown"
              },
              "script_outline_markdown": {
                "type": "string",
                "description": "照这个套路做一条的脚本提纲,Markdown"
              }
            },
            "required": [
              "title",
              "verdict",
              "report_markdown",
              "script_outline_markdown"
            ],
            "additionalProperties": false
          },
          "json_schema_strict": "true",
          "temperature": 0.4,
          "max_tokens": 12000
        }
      },
      {
        "id": "quotes",
        "type": "quote_check",
        "name": "核对引用的原文",
        "position": {
          "x": 4840,
          "y": 535
        },
        "config": {
          "texts": {
            "verdict": "{{breakdown.json.verdict}}",
            "report": "{{breakdown.json.report_markdown}}"
          },
          "sources": {
            "videos": "{{dy_video_m.items}}{{xhs_video_m.items}}{{bili_video_m.items}}{{web_api_video_m.items}}{{web_video_m.items}}",
            "comments": "{{dy_comments_m.items}}{{xhs_comments_m.items}}{{bili_comments_m.items}}{{web_api_comments_m.items}}{{web_comments_m.items}}",
            "data": "{{data_block.text}}",
            "transcript": "{{transcript.text}}",
            "topic": "{{start.my_topic}}"
          }
        }
      },
      {
        "id": "save_note",
        "type": "note_create",
        "name": "存成笔记",
        "position": {
          "x": 5160,
          "y": 535
        },
        "config": {
          "title": "{{breakdown.json.title}}",
          "markdown": "> {{quotes.texts.verdict}}\n\n{{quotes.texts.report}}\n\n## 照这个套路做一条\n\n{{breakdown.json.script_outline_markdown}}\n\n---\n\n## 附:关键数据\n\n{{data_block.text}}\n\n{{quotes.summary}}\n\n{{transcript_section.text}}",
          "tags": "爆款拆解"
        }
      },
      {
        "id": "done_notice",
        "type": "notify",
        "name": "拆解完成通知",
        "position": {
          "x": 5480,
          "y": 535
        },
        "config": {
          "title": "爆款拆解已存成笔记",
          "body": "{{quotes.texts.verdict}}"
        }
      },
      {
        "id": "output",
        "type": "output",
        "name": "交付拆解与脚本提纲",
        "position": {
          "x": 5800,
          "y": 535
        },
        "config": {
          "values": {
            "note_id": "{{save_note.note_id}}",
            "verdict": "{{quotes.texts.verdict}}",
            "report": "{{quotes.texts.report}}",
            "script_outline": "{{breakdown.json.script_outline_markdown}}",
            "video_asset_id": "{{download.asset_id}}",
            "transcript": "{{transcript.text}}",
            "quote_check": "{{quotes.summary}}"
          }
        }
      }
    ],
    "edges": [
      {
        "id": "start__link",
        "source": "start",
        "target": "link"
      },
      {
        "id": "link__use_tikhub",
        "source": "link",
        "target": "use_tikhub"
      },
      {
        "id": "use_tikhub__tk_is_dy__true",
        "source": "use_tikhub",
        "target": "tk_is_dy",
        "source_handle": "true"
      },
      {
        "id": "tk_is_dy__dy_video__true",
        "source": "tk_is_dy",
        "target": "dy_video",
        "source_handle": "true"
      },
      {
        "id": "tk_is_dy__dy_comments__true",
        "source": "tk_is_dy",
        "target": "dy_comments",
        "source_handle": "true"
      },
      {
        "id": "dy_video__dy_video_m",
        "source": "dy_video",
        "target": "dy_video_m"
      },
      {
        "id": "dy_comments__dy_comments_m",
        "source": "dy_comments",
        "target": "dy_comments_m"
      },
      {
        "id": "tk_is_dy__tk_is_xhs__false",
        "source": "tk_is_dy",
        "target": "tk_is_xhs",
        "source_handle": "false"
      },
      {
        "id": "tk_is_xhs__xhs_video__true",
        "source": "tk_is_xhs",
        "target": "xhs_video",
        "source_handle": "true"
      },
      {
        "id": "tk_is_xhs__xhs_comments__true",
        "source": "tk_is_xhs",
        "target": "xhs_comments",
        "source_handle": "true"
      },
      {
        "id": "xhs_video__xhs_video_m",
        "source": "xhs_video",
        "target": "xhs_video_m"
      },
      {
        "id": "xhs_comments__xhs_comments_m",
        "source": "xhs_comments",
        "target": "xhs_comments_m"
      },
      {
        "id": "tk_is_xhs__tk_is_bili__false",
        "source": "tk_is_xhs",
        "target": "tk_is_bili",
        "source_handle": "false"
      },
      {
        "id": "tk_is_bili__bili_video__true",
        "source": "tk_is_bili",
        "target": "bili_video",
        "source_handle": "true"
      },
      {
        "id": "tk_is_bili__bili_comments__true",
        "source": "tk_is_bili",
        "target": "bili_comments",
        "source_handle": "true"
      },
      {
        "id": "bili_video__bili_video_m",
        "source": "bili_video",
        "target": "bili_video_m"
      },
      {
        "id": "bili_comments__bili_comments_m",
        "source": "bili_comments",
        "target": "bili_comments_m"
      },
      {
        "id": "tk_is_bili__tk_unsupported__false",
        "source": "tk_is_bili",
        "target": "tk_unsupported",
        "source_handle": "false"
      },
      {
        "id": "use_tikhub__web_open__false",
        "source": "use_tikhub",
        "target": "web_open",
        "source_handle": "false"
      },
      {
        "id": "web_open__web_read",
        "source": "web_open",
        "target": "web_read"
      },
      {
        "id": "web_read__web_close",
        "source": "web_read",
        "target": "web_close"
      },
      {
        "id": "web_read__web_is_api",
        "source": "web_read",
        "target": "web_is_api"
      },
      {
        "id": "web_is_api__web_api_video_m__true",
        "source": "web_is_api",
        "target": "web_api_video_m",
        "source_handle": "true"
      },
      {
        "id": "web_is_api__web_api_comments_m__true",
        "source": "web_is_api",
        "target": "web_api_comments_m",
        "source_handle": "true"
      },
      {
        "id": "web_is_api__web_api_coverage__true",
        "source": "web_is_api",
        "target": "web_api_coverage",
        "source_handle": "true"
      },
      {
        "id": "web_is_api__web_struct__false",
        "source": "web_is_api",
        "target": "web_struct",
        "source_handle": "false"
      },
      {
        "id": "web_struct__web_video_m",
        "source": "web_struct",
        "target": "web_video_m"
      },
      {
        "id": "web_struct__web_comments_m",
        "source": "web_struct",
        "target": "web_comments_m"
      },
      {
        "id": "dy_video_m__data_block",
        "source": "dy_video_m",
        "target": "data_block"
      },
      {
        "id": "xhs_video_m__data_block",
        "source": "xhs_video_m",
        "target": "data_block"
      },
      {
        "id": "bili_video_m__data_block",
        "source": "bili_video_m",
        "target": "data_block"
      },
      {
        "id": "web_api_video_m__data_block",
        "source": "web_api_video_m",
        "target": "data_block"
      },
      {
        "id": "web_video_m__data_block",
        "source": "web_video_m",
        "target": "data_block"
      },
      {
        "id": "dy_comments_m__data_block",
        "source": "dy_comments_m",
        "target": "data_block"
      },
      {
        "id": "xhs_comments_m__data_block",
        "source": "xhs_comments_m",
        "target": "data_block"
      },
      {
        "id": "bili_comments_m__data_block",
        "source": "bili_comments_m",
        "target": "data_block"
      },
      {
        "id": "web_api_comments_m__data_block",
        "source": "web_api_comments_m",
        "target": "data_block"
      },
      {
        "id": "web_comments_m__data_block",
        "source": "web_comments_m",
        "target": "data_block"
      },
      {
        "id": "web_api_coverage__data_block",
        "source": "web_api_coverage",
        "target": "data_block"
      },
      {
        "id": "data_block__has_video",
        "source": "data_block",
        "target": "has_video"
      },
      {
        "id": "has_video__no_video_notice__false",
        "source": "has_video",
        "target": "no_video_notice",
        "source_handle": "false"
      },
      {
        "id": "has_video__want_download__true",
        "source": "has_video",
        "target": "want_download",
        "source_handle": "true"
      },
      {
        "id": "want_download__download__true",
        "source": "want_download",
        "target": "download",
        "source_handle": "true"
      },
      {
        "id": "download__got_video",
        "source": "download",
        "target": "got_video"
      },
      {
        "id": "got_video__transcript__true",
        "source": "got_video",
        "target": "transcript",
        "source_handle": "true"
      },
      {
        "id": "transcript__transcript_section",
        "source": "transcript",
        "target": "transcript_section"
      },
      {
        "id": "has_video__breakdown__true",
        "source": "has_video",
        "target": "breakdown",
        "source_handle": "true"
      },
      {
        "id": "breakdown__quotes",
        "source": "breakdown",
        "target": "quotes"
      },
      {
        "id": "quotes__save_note",
        "source": "quotes",
        "target": "save_note"
      },
      {
        "id": "save_note__done_notice",
        "source": "save_note",
        "target": "done_notice"
      },
      {
        "id": "done_notice__output",
        "source": "done_notice",
        "target": "output"
      },
      {
        "id": "d-link-platform-tk_is_dy-left",
        "source": "link",
        "target": "tk_is_dy",
        "kind": "data",
        "source_output": "platform",
        "target_input": "left"
      },
      {
        "id": "d-dy_video-output-dy_video_m-data",
        "source": "dy_video",
        "target": "dy_video_m",
        "kind": "data",
        "source_output": "output",
        "target_input": "data"
      },
      {
        "id": "d-dy_comments-output-dy_comments_m-data",
        "source": "dy_comments",
        "target": "dy_comments_m",
        "kind": "data",
        "source_output": "output",
        "target_input": "data"
      },
      {
        "id": "d-link-platform-tk_is_xhs-left",
        "source": "link",
        "target": "tk_is_xhs",
        "kind": "data",
        "source_output": "platform",
        "target_input": "left"
      },
      {
        "id": "d-xhs_video-output-xhs_video_m-data",
        "source": "xhs_video",
        "target": "xhs_video_m",
        "kind": "data",
        "source_output": "output",
        "target_input": "data"
      },
      {
        "id": "d-xhs_comments-output-xhs_comments_m-data",
        "source": "xhs_comments",
        "target": "xhs_comments_m",
        "kind": "data",
        "source_output": "output",
        "target_input": "data"
      },
      {
        "id": "d-link-platform-tk_is_bili-left",
        "source": "link",
        "target": "tk_is_bili",
        "kind": "data",
        "source_output": "platform",
        "target_input": "left"
      },
      {
        "id": "d-bili_video-output-bili_video_m-data",
        "source": "bili_video",
        "target": "bili_video_m",
        "kind": "data",
        "source_output": "output",
        "target_input": "data"
      },
      {
        "id": "d-bili_comments-output-bili_comments_m-data",
        "source": "bili_comments",
        "target": "bili_comments_m",
        "kind": "data",
        "source_output": "output",
        "target_input": "data"
      },
      {
        "id": "d-link-url-web_open-url",
        "source": "link",
        "target": "web_open",
        "kind": "data",
        "source_output": "url",
        "target_input": "url"
      },
      {
        "id": "d-web_open-session-web_read-session",
        "source": "web_open",
        "target": "web_read",
        "kind": "data",
        "source_output": "session",
        "target_input": "session"
      },
      {
        "id": "d-web_read-session-web_close-session",
        "source": "web_read",
        "target": "web_close",
        "kind": "data",
        "source_output": "session",
        "target_input": "session"
      },
      {
        "id": "d-link-url-download-url",
        "source": "link",
        "target": "download",
        "kind": "data",
        "source_output": "url",
        "target_input": "url"
      },
      {
        "id": "d-download-asset_id-got_video-left",
        "source": "download",
        "target": "got_video",
        "kind": "data",
        "source_output": "asset_id",
        "target_input": "left"
      },
      {
        "id": "d-download-asset_id-transcript-asset_id",
        "source": "download",
        "target": "transcript",
        "kind": "data",
        "source_output": "asset_id",
        "target_input": "asset_id"
      }
    ]
  }
}
