Files
dsh_mcn_agent/profiles/web/node_modules/dsh-plugin-douyin-video-detail/lib/client.js
T
admin 856e632807 baseline: dsh 工作区基线快照(2026-09-10,全新历史起点)
本提交为 dsh_mcn_workshop 仓库的首个提交(旧仓 dsh_MCNProject 已停用),
完整固化 C:/Users/Administrator/.dsh 当前磁盘状态。

1) 临时数据清理:移出 sessions/(81M)、logs/、skill-backup-20260906/(14M)、
   scripts/__pycache__、storages/workspace.json.bak —— 合计 94M(.dsh 162M → 68M)
2) 凭据脱敏:.credentials.yaml(含明文 DEEPSEEK_API_KEY)移出 git 跟踪并加入
   .gitignore;mcn-data-insight/scripts/setup_redfox_key.py 中真实 key 示例改为占位符
3) .gitignore 增补 logs/、skill-backup-*/ 规则
4) 技能结构以当前磁盘状态为准:skills/ = mcn-short-video / impeccable / taste-skill /
   dsh-multi-user-migration;旧技能目录(mcn-dou-analysis、storyboard-prompt、
   short-video-script、browser-harness、mcn-data-insight)与
   profiles/web/node_modules/dsh-vision-router 本地已不存在,本提交记为删除
2026-09-10 22:31:45 +08:00

497 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 时拆为文本 + <br/>(段落内换行在 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;
}
});