window.__ModuleLoader__.load({ id: "dsh-plugin-douyin-video-detail", factory: (require) => { var module = { exports: {} }; var exports = module.exports; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); let react = require("react"); let react_dom = require("react-dom"); //#region plugin css(暗色适配:嵌套变量 + 无浅色 fallback,见踩坑手册 3.4) const css = ".dvv_page{position:relative;background:var(--dsw-alias-bg-base);width:100%;min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex:1;display:grid;grid-template-rows:minmax(0,1fr);overflow:hidden}.dvv_body{display:flex;flex-direction:column;height:100%;min-height:0;padding:16px;box-sizing:border-box;gap:12px}.dvv_toolbar{display:flex;align-items:center;gap:10px;min-width:0}.dvv_btn{border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:6px 14px;font:var(--dsw-font-s-14);cursor:pointer;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));color:var(--dsw-alias-label-primary);transition:background var(--ds-transition-duration-fast) var(--ds-ease-in-out)}.dvv_btn:hover{background:var(--dsw-alias-interactive-bg-hover)}.dvv_title{font:var(--dsw-font-l-16);font-weight:600;color:var(--dsw-alias-label-primary);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.dvv_link{color:var(--dsw-alias-accent-fg);cursor:pointer;text-decoration:none;border-bottom:1px dashed var(--dsw-alias-accent-fg,transparent)}.dvv_link:hover{text-decoration:underline}.dvv_infoCard{flex:0 0 auto;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:14px 16px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))}.dvv_infoGrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px 18px}.dvv_infoItem{display:flex;flex-direction:column;gap:4px;min-width:0}.dvv_infoItemWide{grid-column:1/-1}.dvv_infoLabel{font:var(--dsw-font-s-12);color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))}.dvv_infoValue{font:var(--dsw-font-s-14);color:var(--dsw-alias-label-primary);word-break:break-word;white-space:pre-wrap;line-height:1.6}.dvv_analysisHead{flex:0 0 auto;font:var(--dsw-font-l-15);font-weight:600;color:var(--dsw-alias-label-primary)}.dvv_analysisWrap{flex:1;min-height:0;overflow:auto;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:14px 16px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));box-sizing:border-box}.dvv_state{padding:28px 16px;text-align:center;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font:var(--dsw-font-s-14)}.dvv_loading{display:flex;align-items:center;justify-content:center;height:100%;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))}.dvv_error{display:flex;align-items:center;justify-content:center;height:100%;color:var(--dsw-alias-danger-fg,var(--dsw-alias-error-fg))}.dvv_empty{display:flex;align-items:center;justify-content:center;height:100%;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))}.dvv_md{font:var(--dsw-font-s-14);line-height:1.75;color:var(--dsw-alias-label-primary);word-break:break-word}.dvv_md h1,.dvv_md h2,.dvv_md h3,.dvv_md h4{margin:18px 0 8px;font-weight:600;line-height:1.4}.dvv_md h1{font-size:20px;margin-top:26px}.dvv_md h2{font-size:17px;margin-top:24px}.dvv_md h3{font-size:15px;margin-top:20px}.dvv_md h4{font-size:14px;margin-top:18px}.dvv_md p{margin:8px 0}.dvv_md ul,.dvv_md ol{margin:8px 0;padding-left:22px}.dvv_md li{margin:2px 0}.dvv_md a{color:var(--dsw-alias-accent-fg);text-decoration:none;border-bottom:1px dashed var(--dsw-alias-accent-fg,transparent)}.dvv_md a:hover{text-decoration:underline}.dvv_md strong{font-weight:600}.dvv_md em{font-style:italic}.dvv_md code{font-family:var(--dsw-font-mono,monospace);font-size:.9em;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:4px;padding:1px 5px}.dvv_md pre{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:12px;overflow:auto;margin:10px 0;line-height:1.55}.dvv_md pre code{background:none;border:none;padding:0;display:block;white-space:pre;font-size:13px}.dvv_md blockquote{margin:10px 0;padding:8px 14px;border-left:3px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-base);border-radius:0 8px 8px 0}.dvv_md blockquote p{margin:4px 0}.dvv_md table{border-collapse:collapse;margin:10px 0;width:100%;font:var(--dsw-font-s-13)}.dvv_md th,.dvv_md td{border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));padding:7px 10px;text-align:left;vertical-align:top;line-height:1.6}.dvv_md th{background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));font-weight:600;white-space:nowrap}.dvv_md th:first-child,.dvv_md td:first-child{width:18%;min-width:96px;max-width:220px;word-break:break-word}.dvv_md hr{border:none;border-top:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));margin:14px 0}.dvv_tabs{display:flex;gap:4px;flex:0 0 auto;border-bottom:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))}.dvv_tab{border:1px solid transparent;border-bottom:none;border-radius:8px 8px 0 0;padding:7px 16px;font:var(--dsw-font-s-14);cursor:pointer;background:transparent;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));transition:color var(--ds-transition-duration-fast) var(--ds-ease-in-out),background var(--ds-transition-duration-fast) var(--ds-ease-in-out)}.dvv_tab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.dvv_tabActive{border-color:var(--dsw-alias-border-l2,var(--dsw-alias-border));background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));color:var(--dsw-alias-label-primary);font-weight:600}.dvv_tabBody{flex:1;min-height:0;overflow:auto;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-top:none;border-radius:0 0 8px 8px;padding:14px 16px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))}"; const key = "dsh-plugin-douyin-video-detail/VideoDetail.module.css"; if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(key) + "]")) { const el = document.createElement("style"); el.dataset.plugin = "dsh-plugin-douyin-video-detail"; el.dataset.pluginCss = key; el.textContent = css; document.head.appendChild(el); } const cssMap = { "page": "dvv_page", "body": "dvv_body", "toolbar": "dvv_toolbar", "btn": "dvv_btn", "title": "dvv_title", "link": "dvv_link", "infoCard": "dvv_infoCard", "infoGrid": "dvv_infoGrid", "infoItem": "dvv_infoItem", "infoItemWide": "dvv_infoItemWide", "infoLabel": "dvv_infoLabel", "infoValue": "dvv_infoValue", "analysisHead": "dvv_analysisHead", "analysisWrap": "dvv_analysisWrap", "tabs": "dvv_tabs", "tab": "dvv_tab", "tabActive": "dvv_tabActive", "tabBody": "dvv_tabBody", "state": "dvv_state", "loading": "dvv_loading", "error": "dvv_error", "empty": "dvv_empty", "md": "dvv_md" }; //#endregion //#region 极简 markdown 渲染(视频分析内容:无第三方依赖,渲染为 React 元素避免 XSS) // 行内语法:**粗体** / *斜体* / `行内代码` / [链接](url)(仅允许 http/https/mailto) const INLINE_RE = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`|\[[^\]]+\]\([^)]+\))/g; // 文本段:含 \n 时拆为文本 +
(段落内换行在 HTML 中默认折叠为空格,需显式换行) function textWithBreaks(seg, keyBase) { if (!seg.includes("\n")) return [seg]; const parts = seg.split("\n"); const out = []; parts.forEach((p, i) => { if (p) out.push(p); if (i < parts.length - 1) out.push(react.createElement("br", { key: keyBase + "-br" + i })); }); return out; } function parseInline(text, keyBase) { if (!text) return [text]; const nodes = []; let last = 0, k = 0; for (const m of text.matchAll(INLINE_RE)) { if (m.index > last) nodes.push(...textWithBreaks(text.slice(last, m.index), keyBase + "-t" + k)); const tok = m[0]; if (tok.startsWith("**") && tok.endsWith("**") && tok.length > 4) { nodes.push(react.createElement("strong", { key: keyBase + "-b" + k }, tok.slice(2, -2))); } else if (tok.startsWith("*") && tok.endsWith("*") && tok.length > 2) { nodes.push(react.createElement("em", { key: keyBase + "-i" + k }, tok.slice(1, -1))); } else if (tok.startsWith("`") && tok.endsWith("`") && tok.length > 2) { nodes.push(react.createElement("code", { key: keyBase + "-c" + k }, tok.slice(1, -1))); } else if (tok.startsWith("[")) { const mm = tok.match(/^\[([^\]]+)\]\(([^)]+)\)$/); const href = mm && /^(https?:|mailto:)/i.test(mm[2]) ? mm[2] : null; nodes.push(href ? react.createElement("a", { key: keyBase + "-a" + k, href: href, target: "_blank", rel: "noopener noreferrer" }, mm[1]) : (mm ? mm[1] : tok)); } else { nodes.push(tok); } last = m.index + tok.length; k++; } if (last < text.length) nodes.push(...textWithBreaks(text.slice(last), keyBase + "-t" + k)); return nodes; } // 表格块:rows 为已去掉分隔行的字符串数组 function mdTable(rows, keyBase) { const split = (r) => r.replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim()); const head = split(rows[0]); const body = rows.slice(1).map(split); return react.createElement("table", { key: keyBase }, react.createElement("thead", null, react.createElement("tr", null, head.map((h, i) => react.createElement("th", { key: "h" + i }, parseInline(h, keyBase + "-h" + i))))), react.createElement("tbody", null, body.map((row, ri) => react.createElement("tr", { key: "r" + ri }, row.map((c, ci) => react.createElement("td", { key: "c" + ci }, parseInline(c, keyBase + "-c" + ri + "-" + ci))))))); } // 块级解析:标题/代码块/引用/表格/列表/分隔线/段落 function mdToReact(md) { if (typeof md !== "string" || !md.trim()) return null; const lines = md.replace(/\r\n/g, "\n").split("\n"); const out = []; let i = 0, k = 0; while (i < lines.length) { const t = lines[i].trim(); // 围栏代码块 const fence = t.match(/^```(\S*)\s*$/); if (fence) { const buf = []; i++; while (i < lines.length && !/^```\s*$/.test(lines[i].trim())) { buf.push(lines[i]); i++; } i++; out.push(react.createElement("pre", { key: "md" + k++ }, react.createElement("code", null, buf.join("\n")))); continue; } // 标题 const h = t.match(/^(#{1,4})\s+(.*)$/); if (h) { out.push(react.createElement("h" + h[1].length, { key: "md" + k++ }, parseInline(h[2], "h" + k))); i++; continue; } // 分隔线 if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(t)) { out.push(react.createElement("hr", { key: "md" + k++ })); i++; continue; } // 引用 if (t.startsWith(">")) { const buf = []; while (i < lines.length && lines[i].trim().startsWith(">")) { buf.push(lines[i].trim().replace(/^>\s?/, "")); i++; } out.push(react.createElement("blockquote", { key: "md" + k++ }, mdToReact(buf.join("\n")))); continue; } // 表格:当前行含 | 且下一行为 |---| 分隔行 if (t.includes("|") && i + 1 < lines.length && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i + 1]) && lines[i + 1].includes("-")) { const sep = lines[i + 1].replace(/\s/g, ""); if (/^\|?(:?-+:?\|)+:?-+:?\|?$/.test(sep)) { const buf = [t]; i += 2; while (i < lines.length && lines[i].trim().includes("|")) { buf.push(lines[i].trim()); i++; } out.push(mdTable(buf, "md" + k++)); continue; } } // 无序列表 const ul = t.match(/^[-*+]\s+(.*)$/); if (ul) { const items = []; while (i < lines.length) { const mt = lines[i].trim().match(/^[-*+]\s+(.*)$/); if (mt) { items.push(mt[1]); i++; continue; } break; } out.push(react.createElement("ul", { key: "md" + k++ }, items.map((it, ii) => react.createElement("li", { key: ii }, parseInline(it, "li" + k + "-" + ii))))); continue; } // 有序列表 const ol = t.match(/^\d+[.)]\s+(.*)$/); if (ol) { const items = []; while (i < lines.length) { const mt = lines[i].trim().match(/^\d+[.)]\s+(.*)$/); if (mt) { items.push(mt[1]); i++; continue; } break; } out.push(react.createElement("ol", { key: "md" + k++ }, items.map((it, ii) => react.createElement("li", { key: ii }, parseInline(it, "li" + k + "-" + ii))))); continue; } // 空行跳过 if (t === "") { i++; continue; } // 段落:合并连续普通行(不跨空行) const para = [t]; i++; while (i < lines.length) { const nt = lines[i].trim(); if (nt === "" || /^(#{1,4}\s|```|>|[-*+]\s|\d+[.)]\s|.*\|)/.test(nt) || /^(-{3,}|\*{3,}|_{3,})\s*$/.test(nt)) break; para.push(nt); i++; } out.push(react.createElement("p", { key: "md" + k++ }, parseInline(para.join("\n"), "p" + k))); } return out; } //#endregion //#region 视频详情页 const fmt = (v) => (v == null || v === "") ? "-" : String(v); // 时长格式化:秒 → 「X分Y秒」(不足 1 分钟显示「X秒」) const fmtDuration = (v) => { const s = Number(v); if (!Number.isFinite(s) || s <= 0) return "-"; const m = Math.floor(s / 60); const sec = Math.round(s % 60); return m > 0 ? `${m}分${sec}秒` : `${sec}秒`; }; //#region 视频解析任务跟踪(V1.0 同步:解析/刷新 + 断点恢复;localStorage 持久化,页面重建后继续盯任务) // 记录:{ ts, force };ts=提交时刻。轮询 /mcn/api/task/status 的 analyze[](videoId→running)。 // 结束判定:提交后 90s 宽限窗(服务端 60s 防抖)+ 超时上限 35 分钟。 const PARSE_DEADLINE_MS = 35 * 60 * 1000; const PARSE_START_WINDOW_MS = 90 * 1000; const taskKeyOf = (id) => "dvv_parse_task_" + id; const readTaskRec = (id) => { try { const raw = localStorage.getItem(taskKeyOf(id)); if (!raw) return null; const o = JSON.parse(raw); return o && o.ts ? o : null; } catch (e) { return null; } }; const writeTaskRec = (id, force) => { try { localStorage.setItem(taskKeyOf(id), JSON.stringify({ ts: Date.now(), force: !!force })); } catch (e) {} }; const clearTaskRec = (id) => { try { localStorage.removeItem(taskKeyOf(id)); } catch (e) {} }; //#endregion // 视频拆解(analysis.json dict,12-13 维度)→ markdown 文本(每个维度一节,供 mdToReact 渲染) const analysisToMarkdown = (obj) => Object.entries(obj || {}) .map(([k, v]) => `## ${k}\n\n${v}`) .join("\n\n---\n\n"); // 详情页 tab:视频脚本(content)→ 视频拆解(analysis) const DETAIL_TABS = [ { id: "script", label: "视频脚本" }, { id: "deconstruct", label: "视频拆解" }, ]; function VideoDetailPage({ params }) { const videoId = params && params.videoId != null ? Number(params.videoId) : null; const awemeId = params && params.awemeId != null ? String(params.awemeId) : null; const accountId = params && params.accountId != null ? Number(params.accountId) : null; const [video, setVideo] = react.useState(null); const [analysis, setAnalysis] = react.useState(null); const [parse, setParse] = react.useState(null); const [activeTab, setActiveTab] = react.useState("script"); const [loading, setLoading] = react.useState(videoId != null || awemeId != null); const [error, setError] = react.useState(null); // V1.0 同步:解析任务状态 idle|submitting|running + 提示条 + 手动重载计数 const [parseState, setParseState] = react.useState("idle"); const [info, setInfo] = react.useState(null); const [reloadTick, setReloadTick] = react.useState(0); const parseVideoId = videoId; // 解析任务只能按库内视频发起(aweme 直链无行不提供解析) react.useEffect(() => { if (videoId == null && awemeId == null) return; let alive = true; setLoading(true); setError(null); setVideo(null); setAnalysis(null); setParse(null); setActiveTab("script"); const qs = new URLSearchParams(); if (awemeId) qs.set("awemeId", awemeId); else qs.set("videoId", String(videoId)); fetch("/mcn/api/video/detail?" + qs.toString()) .then((r) => r.json()) .then((d) => { if (!d.ok) throw new Error(d.error || "加载失败"); if (!alive) return; setVideo(d.video || null); setAnalysis(d.analysis || null); setParse(d.parse || null); }) .catch((e) => { if (alive) setError(e.message); }) .finally(() => { if (alive) setLoading(false); }); return () => { alive = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [videoId, awemeId, reloadTick]); // V1.0 同步:解析任务轮询(5s,断点恢复:localStorage 有记录 → 页面重建后继续盯)。 // 初始态恢复:挂载后立即检查一次(record 存在且未超时 → running)。 react.useEffect(() => { if (parseVideoId == null) return; let finishedOnce = false; const check = () => { const rec = readTaskRec(parseVideoId); fetch("/mcn/api/task/status") .then((r) => r.json()) .then((d) => { if (!d.ok) return; const arr = Array.isArray(d.analyze) ? d.analyze : []; const item = arr.find((a) => Number(a.videoId) === parseVideoId); const serverRunning = !!(item && item.status === "running"); if (rec) { const age = Date.now() - rec.ts; if (age > PARSE_DEADLINE_MS) { clearTaskRec(parseVideoId); setParseState("idle"); setInfo("解析任务超过 35 分钟未完成(可能已中断),可重新发起"); return; } if (serverRunning || age < PARSE_START_WINDOW_MS) { setParseState((prev) => (prev === "submitting" ? "running" : prev === "running" ? "running" : "running")); return; } // 记录存在、服务端空闲、宽限窗已过 → 任务结束 clearTaskRec(parseVideoId); setParseState("idle"); if (!finishedOnce) { finishedOnce = true; setInfo("解析任务已结束,正在刷新数据…"); setTimeout(() => setReloadTick((x) => x + 1), 300); } } else if (serverRunning) { setParseState("running"); } else if (parseState === "submitting") { // 提交后宽限期内服务端尚无条目:保持 waiting,交给 rec 分支处理 } }) .catch(() => {}); }; // 挂载恢复 + 周期轮询 check(); const timer = setInterval(check, 5000); return () => clearInterval(timer); // eslint-disable-next-line react-hooks/exhaustive-deps }, [parseVideoId]); // 提示条自动消隐 react.useEffect(() => { if (!info) return; const timer = setTimeout(() => setInfo(null), 6000); return () => clearTimeout(timer); }, [info]); // 提交解析/刷新(V1.0 同步:force=覆盖重解析;服务端 60s 防抖 + 8 并发上限) const submitParse = (force) => { if (parseVideoId == null) return; const tip = force ? "该视频已有解析结果,「刷新」将重新发起 MCP 解析并覆盖(约 3-5 分钟)。确认继续?" : "对该视频发起 AI 解析(MCP 内容解析,约 3-5 分钟,产出「视频脚本」+「视频拆解」)。确认继续?"; if (!window.confirm(tip)) return; setParseState("submitting"); fetch("/mcn/api/video/analyze", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ videoId: parseVideoId }), }) .then((r) => r.json()) .then((d) => { if (d.ok) { writeTaskRec(parseVideoId, force); setParseState("running"); setInfo("已提交解析任务(约 3-5 分钟),完成后自动刷新。可在「MCN任务会话」查看执行过程"); } else { setParseState("idle"); setInfo(d.error || "提交失败"); } }) .catch((e) => { setParseState("idle"); setInfo("提交失败: " + e.message); }); }; // 无参数(从工作台直接打开):提示从账号详情进入 if (videoId == null && awemeId == null) { return react.createElement("main", { className: cssMap.page, "aria-label": "视频详情" }, react.createElement("div", { className: cssMap.body }, react.createElement("div", { className: cssMap.toolbar }, react.createElement("button", { type: "button", className: cssMap.btn, title: "返回账号列表", onClick: () => window.__mcnNav?.open("douyin-accounts") }, "←")), react.createElement("div", { className: cssMap.empty }, "请从「账号详情」的视频列表点击视频ID进入"))); } const infoItem = (label, value, wide) => { const cls = cssMap.infoItem + (wide ? " " + cssMap.infoItemWide : ""); return react.createElement("div", { className: cls, key: label }, react.createElement("div", { className: cssMap.infoLabel }, label), react.createElement("div", { className: cssMap.infoValue }, value)); }; const backTitle = video ? (video.video_title || "视频详情") : "视频详情"; // V1.0 同步:解析/刷新按钮(有解析结果=「刷新」强制覆盖;无=「解析」发起) const hasParsed = !!(parse && (parse.content || parse.analysis)); const parseBusy = parseState === "submitting" || parseState === "running"; let parseBtn = null; if (video && parseVideoId != null) { parseBtn = react.createElement("button", { type: "button", className: cssMap.btn, disabled: parseBusy, title: parseBusy ? "解析任务执行中(可在 MCN任务会话 查看)" : (hasParsed ? "重新发起 MCP 解析并覆盖现有脚本/拆解(约 3-5 分钟)" : "对该视频发起 MCP 内容解析(约 3-5 分钟)"), onClick: () => submitParse(hasParsed), }, parseBusy ? (parseState === "submitting" ? "提交中…" : "解析中…") : (hasParsed ? "刷新" : "解析")); } // V1.0 对齐:查看原视频(外链) let viewVideoBtn = null; if (video && video.video_url) { viewVideoBtn = react.createElement("a", { className: cssMap.btn, href: video.video_url, target: "_blank", rel: "noopener noreferrer", title: "在抖音打开原视频", style: { marginLeft: "auto" } }, "查看视频 ↗"); } // 视频信息卡 let infoCard = null; let analysisArea = null; if (loading) { analysisArea = react.createElement("div", { className: cssMap.loading }, "加载中…"); } else if (error) { analysisArea = react.createElement("div", { className: cssMap.error }, error); } else if (video) { const titleVal = video.video_url ? react.createElement("a", { className: cssMap.link, href: video.video_url, target: "_blank", rel: "noopener noreferrer", title: "在新页面打开视频", }, fmt(video.video_title)) : fmt(video.video_title); infoCard = react.createElement("div", { className: cssMap.infoCard }, react.createElement("div", { className: cssMap.infoGrid }, infoItem("账号", video.account_id ? react.createElement("a", { className: cssMap.link, style: { cursor: "pointer" }, title: "查看账号详情", onClick: () => { try { window.__mcnNav && window.__mcnNav.open("douyin-account-detail", { accountId: video.account_id, accountName: video.account_name }); } catch (e) {} } }, fmt(video.account_name)) : fmt(video.account_name)), infoItem("视频ID", fmt(video.aweme_id)), infoItem("时长", fmtDuration(video.duration)), infoItem("发布时间", fmt(video.publish_time)), infoItem("点赞", fmt(video.like_display || video.like_count)), infoItem("评论", fmt(video.comment_count)), infoItem("分享", fmt(video.share_count)), infoItem("收藏", fmt(video.collect_count)), infoItem("标签", fmt(video.tags), true))); // 三个 tab 内容:视频脚本(MCP content)→ 视频拆解(MCP analysis)→ 视频分析(拆解分析) const scriptMd = parse && parse.content; const deconstructMd = parse && parse.analysis ? analysisToMarkdown(parse.analysis) : null; const analysisMd = analysis && analysis.content; let tabBody = null; if (activeTab === "script") { tabBody = scriptMd ? react.createElement("div", { className: cssMap.md }, mdToReact(scriptMd)) : react.createElement("div", { className: cssMap.state }, "该视频暂无视频脚本(未做原视频解析,可点右上角「解析」发起)"); } else if (activeTab === "deconstruct") { tabBody = deconstructMd ? react.createElement("div", { className: cssMap.md }, mdToReact(deconstructMd)) : react.createElement("div", { className: cssMap.state }, "该视频暂无视频拆解(MCP 结构化分析,可点右上角「解析」发起)"); } else { tabBody = analysisMd ? react.createElement("div", { className: cssMap.md }, mdToReact(analysisMd)) : react.createElement("div", { className: cssMap.state }, "该视频暂无视频分析(完成视频分析后展示)"); } analysisArea = react.createElement(react.Fragment, null, react.createElement("div", { className: cssMap.tabs }, DETAIL_TABS.map((t) => react.createElement("button", { key: t.id, type: "button", className: cssMap.tab + (activeTab === t.id ? " " + cssMap.tabActive : ""), onClick: () => setActiveTab(t.id), }, t.label))), react.createElement("div", { className: cssMap.tabBody }, tabBody)); } return react.createElement("main", { className: cssMap.page, "aria-label": "视频详情" }, react.createElement("div", { className: cssMap.body }, react.createElement("div", { className: cssMap.toolbar }, react.createElement("button", { type: "button", className: cssMap.btn, title: "返回账号详情", onClick: () => window.__mcnNav?.back(), }, "←"), react.createElement("span", { className: cssMap.title }, backTitle), viewVideoBtn, parseBtn), info ? react.createElement("div", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)" } }, info) : null, infoCard, analysisArea)); } //#endregion //#region plugin entry:注册到 MCN 工作台入口注册表(hidden:不显示在工作台网格,仅程序化导航打开) // 统一线性 SVG 图标(Lucide 风格) const IconVideo = ({ size = 16 }) => react.createElement("svg", { viewBox: "0 0 24 24", width: size, height: size, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", }, [react.createElement("path", { key: "p1", d: "m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5" }), react.createElement("rect", { key: "p2", x: "2", y: "6", width: "14", height: "12", rx: "2" })]); const inject = []; function apply() { window.__mcnEntries = window.__mcnEntries || []; // 幂等:避免插件热重载重复注册 if (!window.__mcnEntries.some((e) => e.id === "douyin-video-detail")) { window.__mcnEntries.push({ id: "douyin-video-detail", icon: IconVideo, title: "视频详情", desc: "视频信息与拆解分析(从账号详情视频列表进入)", hidden: true, component: VideoDetailPage, }); } } //#endregion exports.apply = apply; exports.inject = inject; return module.exports; } });