Files
dsh_mcn_agent/profiles/web/node_modules/dsh-plugin-douyin-account-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

878 lines
52 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-account-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 = ".dvd_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}.dvd_body{display:flex;flex-direction:column;height:100%;min-height:0;padding:16px;box-sizing:border-box;gap:12px}.dvd_toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.dvd_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)}.dvd_btn:hover{background:var(--dsw-alias-interactive-bg-hover)}.dvd_btn:disabled{opacity:.55;cursor:not-allowed}.dvd_btnSm{padding:3px 10px;font-size:12px;border-radius:6px;white-space:nowrap;min-width:70px}.dvd_btnPrimary{background:var(--dsw-alias-brand-primary,#4d6bfe);color:#fff;border-color:transparent}.dvd_btnPrimary:hover{background:var(--dsw-alias-brand-primary-hover,#3a55d6)}.dvd_mask{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:flex-start;justify-content:center;padding-top:26vh;z-index:100}.dvd_modal{background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));border-radius:12px;padding:20px;min-width:320px;max-width:480px;width:min(480px,92vw);box-shadow:0 8px 30px rgba(0,0,0,.2)}.dvd_modalTitle{font-size:16px;font-weight:600;margin-bottom:12px;color:var(--dsw-alias-label-primary)}.dvd_modalText{font:var(--dsw-font-s-14);color:var(--dsw-alias-label-secondary);margin-bottom:16px;line-height:1.7;word-break:break-all;display:flex;flex-direction:column;gap:6px}.dvd_modalBtns{display:flex;justify-content:flex-end;gap:8px}.dvd_title{font:var(--dsw-font-l-16);font-weight:600;color:var(--dsw-alias-label-primary)}.dvd_link{color:var(--dsw-alias-accent-fg);cursor:pointer;text-decoration:none;border-bottom:1px dashed var(--dsw-alias-accent-fg,transparent)}.dvd_link:hover{text-decoration:underline}.dvd_badge{display:inline-block;padding:2px 10px;border-radius:999px;font:var(--dsw-font-s-12);line-height:20px}.dvd_badgeSigned{background:var(--dsw-alias-accent-bg-subtle,var(--dsw-alias-accent-bg-soft));color:var(--dsw-alias-accent-fg)}.dvd_badgeExternal{background:var(--dsw-alias-ok-bg-subtle,var(--dsw-alias-ok-bg-soft));color:var(--dsw-alias-ok-fg)}.dvd_tabsCard{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));overflow:hidden}.dvd_infoGrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px 18px;flex:0 0 auto;margin-bottom:12px}.dvd_infoItem{display:flex;flex-direction:column;gap:4px;min-width:0}.dvd_infoItemWide{grid-column:1/-1}.dvd_infoLabel{font:var(--dsw-font-s-12);color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))}.dvd_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;max-height:140px;overflow-y:auto}.dvd_tableWrap{flex:1;min-height:0;overflow:auto;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px}.dvd_table{width:100%;border-collapse:collapse;font:var(--dsw-font-s-14);min-width:1180px}.dvd_table th{position:sticky;top:0;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));text-align:left;padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));font-weight:600;white-space:nowrap;user-select:none}.dvd_table td{padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));vertical-align:middle;white-space:nowrap;max-width:260px;overflow:hidden;text-overflow:ellipsis;line-height:1.5}.dvd_table td.dvd_cellTitle{max-width:300px}.dvd_pager{display:flex;align-items:center;gap:12px;flex:0 0 auto;margin-top:10px}.dvd_pagerInfo{font:var(--dsw-font-s-13);color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))}.dvd_loading{display:flex;align-items:center;justify-content:center;height:100%;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))}.dvd_error{display:flex;align-items:center;justify-content:center;height:100%;color:var(--dsw-alias-danger-fg,var(--dsw-alias-error-fg))}.dvd_empty{display:flex;align-items:center;justify-content:center;height:100%;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))}.dvd_tabs{display:flex;align-items:stretch;border-bottom:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));padding:0 8px;gap:2px}.dvd_tab{background:none;border:none;padding:10px 16px;font:var(--dsw-font-s-14);color:var(--dsw-alias-label-secondary);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:color var(--ds-transition-duration-fast) var(--ds-ease-in-out)}.dvd_tab:hover{color:var(--dsw-alias-label-primary)}.dvd_tabActive{color:var(--dsw-alias-accent-fg);font-weight:600;border-bottom-color:var(--dsw-alias-accent-fg)}.dvd_tabPanel{flex:1;min-height:0;display:flex;flex-direction:column;padding:14px 16px;box-sizing:border-box;overflow:hidden}.dvd_personaPanel{overflow:auto;padding-right:6px}.dvd_tabState{padding:28px 16px;text-align:center;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font:var(--dsw-font-s-14)}.dvd_md{font:var(--dsw-font-s-14);line-height:1.75;color:var(--dsw-alias-label-primary);word-break:break-word}.dvd_md h1,.dvd_md h2,.dvd_md h3,.dvd_md h4{margin:18px 0 8px;font-weight:600;line-height:1.4}.dvd_md h1{font-size:20px;margin-top:26px}.dvd_md h2{font-size:17px;margin-top:24px}.dvd_md h3{font-size:15px;margin-top:20px}.dvd_md h4{font-size:14px;margin-top:18px}.dvd_md p{margin:8px 0}.dvd_md ul,.dvd_md ol{margin:8px 0;padding-left:22px}.dvd_md li{margin:2px 0}.dvd_md a{color:var(--dsw-alias-accent-fg);text-decoration:none;border-bottom:1px dashed var(--dsw-alias-accent-fg,transparent)}.dvd_md a:hover{text-decoration:underline}.dvd_md strong{font-weight:600}.dvd_md em{font-style:italic}.dvd_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}.dvd_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}.dvd_md pre code{background:none;border:none;padding:0;display:block;white-space:pre;font-size:13px}.dvd_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}.dvd_md blockquote p{margin:4px 0}.dvd_md table{border-collapse:collapse;margin:10px 0;width:100%;font:var(--dsw-font-s-13)}.dvd_md th,.dvd_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}.dvd_md th{background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));font-weight:600;white-space:nowrap}.dvd_md th:first-child,.dvd_md td:first-child{width:18%;min-width:96px;max-width:220px;word-break:break-word}.dvd_md hr{border:none;border-top:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));margin:14px 0}";
const key = "dsh-plugin-douyin-account-detail/Detail.module.css";
if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(key) + "]")) {
const el = document.createElement("style");
el.dataset.plugin = "dsh-plugin-douyin-account-detail";
el.dataset.pluginCss = key;
el.textContent = css;
if (!document.querySelector('style[data-plugin-css="dvd-align"]')) {
const el2 = document.createElement("style");
el2.dataset.pluginCss = "dvd-align";
el2.textContent = ".dvd_table thead th:last-child{text-align:center!important}.dvd_table tbody td:last-child{text-align:center!important}";
document.head.appendChild(el2);
}
document.head.appendChild(el);
}
const cssMap = {
"page": "dvd_page",
"body": "dvd_body",
"toolbar": "dvd_toolbar",
"btn": "dvd_btn",
"btnSm": "dvd_btnSm",
"btnPrimary": "dvd_btnPrimary",
"mask": "dvd_mask",
"modal": "dvd_modal",
"modalTitle": "dvd_modalTitle",
"modalText": "dvd_modalText",
"modalBtns": "dvd_modalBtns",
"title": "dvd_title",
"link": "dvd_link",
"badge": "dvd_badge",
"badgeSigned": "dvd_badgeSigned",
"badgeExternal": "dvd_badgeExternal",
"infoCard": "dvd_tabsCard",
"infoGrid": "dvd_infoGrid",
"infoItem": "dvd_infoItem",
"infoItemWide": "dvd_infoItemWide",
"infoLabel": "dvd_infoLabel",
"infoValue": "dvd_infoValue",
"tabs": "dvd_tabs",
"tab": "dvd_tab",
"tabActive": "dvd_tabActive",
"tabPanel": "dvd_tabPanel",
"personaPanel": "dvd_personaPanel",
"tabState": "dvd_tabState",
"md": "dvd_md",
"tableWrap": "dvd_tableWrap",
"table": "dvd_table",
"cellTitle": "dvd_cellTitle",
"pager": "dvd_pager",
"pagerInfo": "dvd_pagerInfo",
"loading": "dvd_loading",
"error": "dvd_error",
"empty": "dvd_empty"
};
//#endregion
//#region 极简 markdown 渲染(账号设定 tab:无第三方依赖,渲染为 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);
const firstColStyle = { width: "88px", maxWidth: "88px", minWidth: 0, wordBreak: "break-word" };
return react.createElement("table", { key: keyBase, style: { width: "100%" } },
react.createElement("thead", null,
react.createElement("tr", null, head.map((h, i) => react.createElement("th", { key: "h" + i, style: i === 0 ? firstColStyle : undefined }, 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, style: ci === 0 ? firstColStyle : undefined }, 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 账号详情页
// 统一线性 SVG 图标(Lucide 风格:24 viewBox、stroke currentColor、圆角端点)
const SvgBase = ({ d, 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",
style: { verticalAlign: "-2px" },
}, d);
const IconDownload = ({ size }) => react.createElement(SvgBase, { size, d: [react.createElement("path", { key: "p1", d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }), react.createElement("polyline", { key: "p2", points: "7 10 12 15 17 10" }), react.createElement("line", { key: "p3", x1: "12", y1: "15", x2: "12", y2: "3" })] });
const IconChart = ({ size }) => react.createElement(SvgBase, { size, d: [react.createElement("path", { key: "p1", d: "M3 3v18h18" }), react.createElement("path", { key: "p2", d: "M18 17V9" }), react.createElement("path", { key: "p3", d: "M13 17V5" }), react.createElement("path", { key: "p4", d: "M8 17v-3" })] });
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}秒`;
};
// 视频表格列
const COLUMNS = [
["序号", (r, i) => String(i)],
["视频ID", (r) => r.aweme_id],
["视频标题", (r) => r.video_title],
["点赞", (r) => r.like_display || fmt(r.like_count)],
["评论", (r) => fmt(r.comment_count)],
["分享", (r) => fmt(r.share_count)],
["收藏", (r) => fmt(r.collect_count)],
["播放", (r) => (r.play_count == null || Number(r.play_count) === 0) ? "-" : fmt(r.play_count)],
["时长", (r) => fmtDuration(r.duration)],
["发布时间", (r) => fmt(r.publish_time)],
["标签", (r) => fmt(r.tags)]
];
//#region V1.0 同步:视频互动趋势(lieflat-charts 风格内联 SVG 双序列折线;不引第三方图表库)
// 数据 = 该账号全部视频(分页循环拉取,独立于列表分页);窗口 = 近半年(按发布时间)
const TREND_DAYS = 180;
const TREND_COLOR_LIKE = "#e5484d";
const TREND_COLOR_COMMENT = "#2f6fed";
// 数值混合解析:数字原样;"7.8万"/"500w"/"1.2亿" → 数值
function numOfMix(v) {
const s = String(v == null ? "" : v).trim();
if (!s) return 0;
const m = s.match(/^([\d.]+)\s*(亿|w|万)?$/);
if (!m) return Number(s) || 0;
let n = parseFloat(m[1]) || 0;
if (m[2] === "亿") n *= 100000000;
else if (m[2]) n *= 10000;
return n;
}
function fmtWan(v) {
const n = Number(v);
if (!Number.isFinite(n) || n <= 0) return "0";
return n >= 10000 ? (n / 10000).toFixed(n >= 1000000 ? 0 : 1) + "万" : String(Math.round(n));
}
// videos → 升序趋势点(近半年内有发布时间的视频);不足 2 点返回 [](前端显示空态提示)
function buildTrendPoints(videos) {
const cutoff = Date.now() - TREND_DAYS * 86400000;
const pts = [];
for (const v of videos || []) {
const ds = String(v.publish_time || "").slice(0, 10);
if (!/^\d{4}-\d{2}-\d{2}$/.test(ds)) continue;
const t = new Date(ds + "T00:00:00");
if (Number.isNaN(t.getTime()) || t.getTime() < cutoff) continue;
pts.push({
d: ds,
like: numOfMix(v.like_display || v.like_count),
comment: numOfMix(v.comment_count),
title: String(v.video_title || "").slice(0, 40) || ds,
});
}
pts.sort((a, b) => (a.d < b.d ? -1 : a.d > b.d ? 1 : 0));
return pts;
}
// 内联 SVG 双序列折线(左轴=点赞 右轴=评论,各自归一化到自身量程)
function trendSvg(pts) {
const W = 640, H = 220, PL = 62, PR = 62, PT = 14, PB = 26;
const n = pts.length;
if (n < 2) return null;
const xs = (i) => PL + (W - PL - PR) * i / (n - 1);
const lmax = Math.max.apply(null, pts.map((p) => p.like));
const lmin = Math.min.apply(null, pts.map((p) => p.like));
const cmax = Math.max.apply(null, pts.map((p) => p.comment));
const cmin = Math.min.apply(null, pts.map((p) => p.comment));
const span = (lo, hi) => Math.max(hi - lo, 1);
const yl = (v) => PT + (H - PT - PB) * (1 - (v - lmin) / span(lmin, Math.max(lmax, 1)));
const yc = (v) => PT + (H - PT - PB) * (1 - (v - cmin) / span(cmin, Math.max(cmax, 1)));
const grid = [0.25, 0.5, 0.75].map((g) => {
const y = PT + (H - PT - PB) * g;
return react.createElement("line", { key: "g" + g, x1: PL, x2: W - PR, y1: y, y2: y,
style: { stroke: "var(--dsw-alias-border-l2,#e8e8ea)", strokeWidth: 1 } });
});
const likePts = pts.map((p, i) => `${xs(i).toFixed(1)},${yl(p.like).toFixed(1)}`).join(" ");
const cmtPts = pts.map((p, i) => `${xs(i).toFixed(1)},${yc(p.comment).toFixed(1)}`).join(" ");
const dots = n <= 40
? pts.map((p, i) => [
react.createElement("circle", { key: "dl" + i, cx: xs(i), cy: yl(p.like), r: 2.2, fill: TREND_COLOR_LIKE }),
react.createElement("circle", { key: "dc" + i, cx: xs(i), cy: yc(p.comment), r: 2.2, fill: TREND_COLOR_COMMENT }),
])
: [];
const last = pts[n - 1];
const labelFill = { fill: "var(--dsw-alias-label-secondary,#8a8f98)", fontSize: 10 };
return react.createElement("svg", { viewBox: "0 0 " + W + " " + H, style: { width: "100%", height: "auto", display: "block" }, "aria-label": "互动趋势图" },
grid,
react.createElement("polyline", { key: "pl", points: likePts, fill: "none",
style: { stroke: TREND_COLOR_LIKE, strokeWidth: 1.8 } }),
react.createElement("polyline", { key: "pc", points: cmtPts, fill: "none",
style: { stroke: TREND_COLOR_COMMENT, strokeWidth: 1.8 } }),
dots,
// 左轴:点赞 量程(顶部=最大)
react.createElement("text", { x: PL - 6, y: PT + 8, textAnchor: "end", style: Object.assign({ fill: TREND_COLOR_LIKE, fontSize: 10 }, {}) }, fmtWan(lmax)),
react.createElement("text", { x: PL - 6, y: H - PB, textAnchor: "end", style: labelFill }, "0"),
// 右轴:评论 量程(顶部=最大)
react.createElement("text", { x: W - PR + 6, y: PT + 8, textAnchor: "start", style: { fill: TREND_COLOR_COMMENT, fontSize: 10 } }, fmtWan(cmax)),
react.createElement("text", { x: W - PR + 6, y: H - PB, textAnchor: "start", style: labelFill }, "0"),
// 日期刻度:首/中/尾
react.createElement("text", { x: PL, y: H - 8, textAnchor: "start", style: labelFill }, pts[0].d.slice(5)),
react.createElement("text", { x: (PL + W - PR) / 2, y: H - 8, textAnchor: "middle", style: labelFill },
pts[Math.floor((n - 1) / 2)].d.slice(5)),
react.createElement("text", { x: W - PR, y: H - 8, textAnchor: "end", style: labelFill }, last.d.slice(5)));
}
//#endregion
function AccountDetailPage({ params }) {
const accountId = params && params.accountId != null ? Number(params.accountId) : null;
const [account, setAccount] = react.useState(null);
const [items, setItems] = react.useState([]);
const [total, setTotal] = react.useState(0);
const [page, setPage] = react.useState(1);
const [pageSize] = react.useState(20);
const [loading, setLoading] = react.useState(accountId != null);
const [error, setError] = react.useState(null);
// 账号信息 / 账号设定 tab 状态(设定内容懒加载,仅首次切换时请求)
const [activeTab, setActiveTab] = react.useState("info");
const [personaLoading, setPersonaLoading] = react.useState(false);
const [personaError, setPersonaError] = react.useState(null);
const [personaData, setPersonaData] = react.useState(null);
const [personaLoaded, setPersonaLoaded] = react.useState(false);
// V1.0 同步:账号数据分析 tab(读 account_analysis 最新 md;空态引导)
const [analysisData, setAnalysisData] = react.useState(null);
const [analysisLoading, setAnalysisLoading] = react.useState(false);
const [analysisError, setAnalysisError] = react.useState(null);
const [analysisLoaded, setAnalysisLoaded] = react.useState(false);
const [analysisBusy, setAnalysisBusy] = react.useState(false);
const loadAnalysisRef = react.useRef(null);
// V1.0 同步:视频互动趋势(近半年点赞/评论折线,数据=该账号全部视频,与分页无关)
const [trendPoints, setTrendPoints] = react.useState(null);
// 对标创作状态(toast 提示 + 进行中的视频行 + 确认弹窗行)
const [toast, setToast] = react.useState(null);
const [creating, setCreating] = react.useState(null);
const [benchmarkRow, setBenchmarkRow] = react.useState(null);
// 视频解析确认弹窗:待解析视频行
const [analyzeRow, setAnalyzeRow] = react.useState(null);
// 账号设定提炼:确认弹窗行 + 提炼中状态(每账号独立会话,多账号可并发)
const [personaRow, setPersonaRow] = react.useState(null);
const [personaBusy, setPersonaBusy] = react.useState(false);
const accountIdRef = react.useRef(accountId);
accountIdRef.current = accountId;
// 当前账号刷新任务状态跟踪(imports 轮询用)
const refreshPrevRef = react.useRef(false);
// 视频解析任务状态(并发):videoId -> true 表示该视频解析中(每视频独立会话,互不阻塞)
const [analyzing, setAnalyzing] = react.useState({});
const analyzingRef = react.useRef({});
analyzingRef.current = analyzing;
const pageRef = react.useRef(1);
pageRef.current = page;
const loadRef = react.useRef(null);
const load = (p) => {
if (accountId == null) return;
setLoading(true);
fetch("/mcn/api/account/videos?accountId=" + accountId + "&page=" + p + "&pageSize=" + pageSize)
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "加载失败");
setAccount(d.account || null);
setItems(d.items || []);
setTotal(d.total || 0);
setPage(d.page || 1);
setError(null);
})
.catch((e) => setError(e.message))
.finally(() => setLoading(false));
};
loadRef.current = load;
// V1.0 同步:拉取该账号全量视频(分页 pageSize=100,上限 6 页)→ 计算趋势点(近半年,独立于列表分页)
const loadTrend = () => {
if (accountId == null) return;
setTrendPoints(null);
const acc = [];
const fetchPage = (page) =>
fetch("/mcn/api/account/videos?accountId=" + accountId + "&page=" + page + "&pageSize=100")
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "加载失败");
acc.push.apply(acc, d.items || []);
if (acc.length < (d.total || 0) && page < 6) return fetchPage(page + 1);
setTrendPoints(buildTrendPoints(acc));
})
.catch(() => setTrendPoints([]));
fetchPage(1);
};
// 轮询视频解析任务(5s):某视频从 running → 非 running 即解析完成,自动刷新列表(并发多任务各自独立,完成后各自刷新)
react.useEffect(() => {
const timer = setInterval(() => {
fetch("/mcn/api/task/status")
.then((r) => r.json())
.then((d) => {
if (!d.ok || !Array.isArray(d.analyze)) return;
const running = {};
d.analyze.forEach((a) => { if (a.status === "running") running[a.videoId] = true; });
const prev = analyzingRef.current;
const next = {};
let doneAny = false;
Object.keys(prev).forEach((id) => {
if (running[id]) next[id] = true;
else doneAny = true; // 之前解析中 → 现在空闲:该视频解析完成
});
Object.keys(running).forEach((id) => { if (!prev[id]) next[id] = true; });
setAnalyzing(next);
if (doneAny) {
const p = pageRef.current;
setTimeout(() => loadRef.current(p), 300);
}
// persona:当前账号提炼状态(running→idle 即完成,刷新账号设定数据)
const plist = Array.isArray(d.persona) ? d.persona : [];
const curId = accountIdRef.current;
const pCur = plist.find((p) => p.accountId === curId);
const pRunning = !!(pCur && pCur.status === "running");
setPersonaBusy((prev) => {
if (pRunning) return true;
if (prev && !pRunning) {
setTimeout(() => {
fetch("/mcn/api/account/persona?accountId=" + accountIdRef.current)
.then((r) => r.json())
.then((dd) => {
if (dd.ok) { setPersonaData({ content: dd.content, analysisTime: dd.analysisTime }); setPersonaLoaded(true); }
})
.catch(() => {});
}, 300);
}
return false;
});
// accountAnalysis:账号数据分析任务(running→idle 即完成,自动刷新数据分析内容)
const alist2 = Array.isArray(d.accountAnalysis) ? d.accountAnalysis : [];
const aCur = alist2.find((t) => t.accountId === accountIdRef.current);
const aRunning = !!(aCur && aCur.status === "running");
setAnalysisBusy((prev) => {
if (aRunning) return true;
if (prev && !aRunning) {
setTimeout(() => { setAnalysisLoaded(false); loadAnalysisRef.current(); }, 300);
}
return false;
});
// imports:当前账号刷新任务(refresh:{accountId})running→idle 即完成,自动刷新账号信息与视频列表
const ilist = Array.isArray(d.imports) ? d.imports : [];
const curId2 = accountIdRef.current;
const refTask = ilist.find((t) => t.id === "refresh:" + curId2);
const refRunning = !!(refTask && refTask.status === "running");
if (refreshPrevRef.current && !refRunning) {
const p = pageRef.current;
setTimeout(() => loadRef.current(p), 300);
}
refreshPrevRef.current = refRunning;
})
.catch(() => {});
}, 5000);
return () => clearInterval(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
react.useEffect(() => {
if (!toast) return;
const timer = setTimeout(() => setToast(null), 4000);
return () => clearTimeout(timer);
}, [toast]);
react.useEffect(() => {
// 切换账号时重置 tab 状态,避免残留上一账号的内容
setActiveTab("info");
setPersonaLoaded(false);
setPersonaData(null);
setPersonaError(null);
setToast(null);
setCreating(null);
setBenchmarkRow(null);
setAnalyzeRow(null);
setPersonaRow(null);
setPersonaBusy(false);
// V1.0 同步:数据分析 tab 状态随账号重置;趋势数据重新拉取
setAnalysisLoaded(false);
setAnalysisData(null);
setAnalysisError(null);
setTrendPoints(null);
loadTrend();
loadPersona();
load(1);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [accountId]);
// 无参数(从工作台直接打开):提示从账号列表进入
if (accountId == 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?.back() }, "←")),
react.createElement("div", { className: cssMap.empty }, "请从「热门账号」列表点击账号的抖音号进入详情")));
}
const totalPages = Math.max(1, Math.ceil(total / pageSize));
// 加载账号设定信息(读取 account_persona 最新一条 markdown 内容)
const loadPersona = () => {
setPersonaLoading(true);
setPersonaError(null);
fetch("/mcn/api/account/persona?accountId=" + accountId)
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "加载失败");
setPersonaData({ content: d.content, analysisTime: d.analysisTime });
setPersonaLoaded(true);
})
.catch((e) => setPersonaError(e.message))
.finally(() => setPersonaLoading(false));
};
// 切换 tab;账号设定内容懒加载(仅首次);数据分析懒加载(仅首次)
const switchTab = (tab) => {
setActiveTab(tab);
if (tab === "info" && !personaLoaded) loadPersona();
if (tab === "analysis" && !analysisLoaded) loadAnalysis();
};
// 加载账号数据分析(读取 account_analysis 最新一条 markdown 内容,V1.0 同步)
const loadAnalysis = () => {
setAnalysisLoading(true);
setAnalysisError(null);
fetch("/mcn/api/account/analysis?accountId=" + accountId)
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "加载失败");
setAnalysisData({ content: d.content, analysisTime: d.analysisTime });
setAnalysisLoaded(true);
})
.catch((e) => setAnalysisError(e.message))
.finally(() => setAnalysisLoading(false));
};
loadAnalysisRef.current = loadAnalysis;
const tabBtn = (label, value) => react.createElement("button", {
type: "button",
role: "tab",
"aria-selected": activeTab === value,
className: cssMap.tab + (activeTab === value ? " " + cssMap.tabActive : ""),
onClick: () => switchTab(value),
}, label);
// 改写脚本:点击按钮 → 确认弹窗 → 账号设定 + 视频选题(只要选题)→ 创作全新故事脚本
// 复用固定会话 dsh_rewrite(无则新建),单任务串行:运行中重复点击会被宿主 409 拒绝
const REWRITE_SESSION = "dsh_rewrite";
const openBenchmark = (row) => setBenchmarkRow(row);
const confirmBenchmark = () => {
const row = benchmarkRow;
if (!row) return;
setBenchmarkRow(null);
setCreating(row.id);
// 账号设定:优先用已加载的缓存,否则按需拉取
const personaP = personaData && personaData.content
? Promise.resolve(personaData.content)
: fetch("/mcn/api/account/persona?accountId=" + accountId)
.then((r) => r.json())
.then((d) => (d.ok ? (d.content || "") : ""));
personaP
.then((persona) => {
const lines = [
`请为达人「${account.account_name}」改写一条短视频故事脚本。`,
``,
`【账号信息】`,
`赛道:${account.track || "-"}`,
`账号定位:${account.content || "-"}`,
``,
`【账号设定】`,
persona || "(该账号暂无账号设定文档)",
``,
`【视频选题】`,
row.topic
? row.topic
: `标题:${row.video_title || "-"}\n视频ID:${row.aweme_id || "-"}`,
``,
`请基于账号设定与上述视频选题,创作一个全新的故事脚本:`,
`- 新故事、新情节,不要复刻原视频;`,
`- 输出完整故事脚本(含标题、故事梗概、口播文案/台词);`,
`- 不要输出分镜脚本,无需对标分析。`,
].join("\n");
return fetch("/mcn/api/creative", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: lines, sessionId: REWRITE_SESSION }),
}).then((r) => r.json());
})
.then((d) => {
if (!d.ok) throw new Error(d.error || "触发失败");
setToast(d.message || "已开始创作,请切换到对应会话查看");
})
.catch((e) => setToast("创作触发失败: " + e.message))
.finally(() => setCreating(null));
};
// 解析视频:确认弹窗 → 每视频独立 AI 会话(mcn-dou-analysis 功能五)MCP 解析并保存/写库;多视频可并发提交
const submitAnalyze = () => {
const row = analyzeRow;
if (!row) return;
setAnalyzeRow(null);
// 乐观标记:按钮立即变「解析中…」,轮询会从 task/status 同步真实状态
setAnalyzing((prev) => { const n = Object.assign({}, prev); n[row.id] = true; return n; });
fetch("/mcn/api/video/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ videoId: row.id }),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) {
setAnalyzing((prev) => { const n = Object.assign({}, prev); delete n[row.id]; return n; });
throw new Error(d.error || "提交失败");
}
setToast(d.message || "已提交解析,AI 正在解析(约 3-5 分钟)");
})
.catch((e) => setToast("解析提交失败: " + e.message));
};
// 提炼账号设定:确认弹窗 → 每账号独立 AI 会话(mcn-dou-analysis 功能三)筛选TOP4→MCP解析→生成人设卡并写库;多账号可并发
const submitPersonaExtract = () => {
const row = personaRow;
if (!row) return;
setPersonaRow(null);
setPersonaBusy(true);
fetch("/mcn/api/account/persona/extract", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ accountId: row.id }),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) { setPersonaBusy(false); throw new Error(d.error || "提交失败"); }
setToast(d.message || "已提交提炼账号设定,AI 正在执行(约 10-20 分钟)");
})
.catch((e) => setToast("提炼提交失败: " + e.message));
};
// 发起账号数据分析(V1.0 同步:mcn-dou-analysis 功能四;每账号独立会话,运行中禁重复发起)
const submitAccountAnalysis = () => {
if (accountId == null) return;
if (!window.confirm("对该账号发起账号数据分析(功能四:近 4 个月最好/最差视频逐条拆解 → 8 章汇总报告,约 10-20 分钟)?")) return;
setAnalysisBusy(true);
fetch("/mcn/api/account/analysis/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ accountId: accountId }),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) { setAnalysisBusy(false); throw new Error(d.error || "提交失败"); }
setToast(d.message || "已提交账号数据分析,AI 正在执行(约 10-20 分钟)");
})
.catch((e) => { setAnalysisBusy(false); setToast("数据分析提交失败: " + e.message); });
};
// 账号信息卡
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));
};
let infoCard = null;
let tableArea = null;
if (loading) {
tableArea = react.createElement("div", { className: cssMap.loading }, "加载中…");
} else if (error) {
tableArea = react.createElement("div", { className: cssMap.error }, error);
} else {
// 视频表格列(追加操作列:已解析(原始解析或拆解分析)→ 改写脚本;均无 → 下载图标占位;拆解详情经视频ID进入)
const columns = COLUMNS.concat([[
"操作",
(row) => (row.has_source || row.has_analysis)
? react.createElement("button", {
type: "button",
className: cssMap.btn + " " + cssMap.btnSm,
disabled: creating === row.id,
title: "基于账号设定与视频选题,改写一条全新的故事脚本",
onClick: () => openBenchmark(row),
}, creating === row.id ? "创作中…" : "改写脚本")
: react.createElement("button", {
type: "button",
className: cssMap.btn + " " + cssMap.btnSm,
disabled: !!analyzing[row.id],
title: analyzing[row.id] ? "该视频正在解析中(约 3-5 分钟),可同时提交其他视频解析" : "解析本视频(MCP 解析,保存到本地并写入数据库)",
onClick: () => setAnalyzeRow(row),
}, analyzing[row.id] ? "解析中…" : react.createElement(IconDownload, {})),
]]);
// 视频表格(账号信息 tab 面板内:表格区滚动,分页固定在面板底部)
const videoTable = react.createElement("div", { className: cssMap.tableWrap },
items.length === 0
? react.createElement("div", { className: cssMap.empty }, "暂无视频数据")
: react.createElement("table", { className: cssMap.table },
react.createElement("thead", null,
react.createElement("tr", null,
columns.map((c) => react.createElement("th", { key: c[0] }, c[0])))),
react.createElement("tbody", null,
items.map((row, idx) => react.createElement("tr", { key: row.id },
columns.map((c) => {
const val = c[1](row, (page - 1) * pageSize + idx + 1);
const cellCls = c[0] === "视频标题" ? cssMap.cellTitle : "";
// 视频标题:有 video_url 时渲染为新页面链接
let content = val;
let tip = c[0] === "视频标题" || c[0] === "标签" ? val : undefined;
if (c[0] === "视频标题" && row.video_url) {
content = react.createElement("a", {
className: cssMap.link,
href: row.video_url,
target: "_blank",
rel: "noopener noreferrer",
title: "在新页面打开视频",
}, val);
tip = row.video_url;
}
// 视频ID:点击打开视频分析详情页
if (c[0] === "视频ID") {
content = react.createElement("span", {
className: cssMap.link,
title: "查看视频分析详情",
onClick: () => window.__mcnNav?.open("douyin-video-detail", { videoId: row.id, awemeId: row.aweme_id, accountId: accountId }),
}, val);
tip = "查看视频分析详情";
}
return react.createElement("td", { key: c[0], className: cellCls, title: tip }, content);
}))))));
const pager = react.createElement("div", { className: cssMap.pager },
react.createElement("span", { className: cssMap.pagerInfo }, `共 ${total} 条视频`),
react.createElement("span", { className: cssMap.pagerInfo }, `第 ${page} / ${totalPages} 页`),
react.createElement("button", { type: "button", className: cssMap.btn, disabled: page <= 1 || loading, onClick: () => load(page - 1) }, "上一页"),
react.createElement("button", { type: "button", className: cssMap.btn, disabled: page >= totalPages || loading, onClick: () => load(page + 1) }, "下一页"));
// V1.0 同步:数据分析 tab 面板(account_analysis 最新 md;空态引导)
const analysisPanel = react.createElement("div", { className: cssMap.tabPanel + " " + cssMap.personaPanel },
analysisLoading
? react.createElement("div", { className: cssMap.tabState }, "加载中…")
: analysisError
? react.createElement("div", { className: cssMap.tabState }, "加载失败:" + analysisError)
: analysisData && analysisData.content
? react.createElement("div", { className: cssMap.md }, mdToReact(analysisData.content))
: react.createElement("div", { className: cssMap.tabState },
react.createElement("div", null, "该账号暂无账号数据分析"),
react.createElement("div", { style: { marginTop: 8, fontSize: 12, lineHeight: 1.6, opacity: 0.85 } },
"数据分析(账号内容结构拆解与定位诊断报告)生成后在此展示。可在对话中发起「分析这个账号的视频数据」,将按 mcn-dou-analysis 功能四执行并自动落库。")),
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnPrimary, style: { marginTop: 12, alignSelf: "flex-start" }, disabled: analysisBusy || accountId == null, title: analysisBusy ? "该账号数据分析任务执行中(约 10-20 分钟),可先处理其他账号" : "调用抖音账号分析技能执行账号数据分析(功能四:样本拆解 → 8 章报告)", onClick: submitAccountAnalysis }, analysisBusy ? "分析中…" : "发起账号数据分析"),
);
// V1.0 同步:视频互动趋势区块(近半年点赞/评论,lieflat 风格内联 SVG;不足 2 点或加载中不显示)
const trendBlock = trendPoints == null ? null
: trendPoints.length >= 2
? react.createElement("div", { style: { border: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", borderRadius: 8, padding: "8px 12px 6px", marginBottom: 12, background: "var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))" } },
react.createElement("div", { style: { display: "flex", alignItems: "center", gap: 14, marginBottom: 4, fontSize: 12 } },
react.createElement("span", { style: { color: "var(--dsw-alias-label-primary)" } }, "互动趋势"),
react.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 4, color: "#e5484d" } }, "● 点赞"),
react.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 4, color: "#2f6fed" } }, "● 评论"),
react.createElement("span", { style: { marginLeft: "auto", color: "var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))" } }, "近半年 · 按发布时间")),
trendSvg(trendPoints))
: (total > 0 ? react.createElement("div", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)", marginBottom: 12 } }, "近半年互动数据不足 2 条,暂不展示趋势") : null);
if (account) {
infoCard = react.createElement("div", { className: cssMap.infoCard },
react.createElement("div", { className: cssMap.tabs, role: "tablist" },
tabBtn("账号信息", "info"),
tabBtn("视频列表", "persona"),
tabBtn("数据分析", "analysis")),
activeTab === "info"
? react.createElement("div", { className: cssMap.tabPanel },
react.createElement("div", { className: cssMap.infoGrid },
infoItem("抖音号", fmt(account.douyin_id)),
infoItem("赛道", fmt(account.track)),
infoItem("AI制作", account.is_ai === "是" ? "是" : "否"),
infoItem("粉丝", fmt(account.followers)),
infoItem("获赞", fmt(account.total_likes)),
infoItem("作品数", fmt(account.works_count)),
infoItem("地区", fmt(account.location)),
infoItem("年龄", fmt(account.age)),
infoItem("获取视频", fmt(total)),
infoItem("简介", fmt(account.bio), true),
infoItem("定位/内容", fmt(account.content), true)),
react.createElement("div", { style: { margin: "18px 0 10px", paddingTop: 12, borderTop: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", fontWeight: 600, fontSize: 15, color: "var(--dsw-alias-label-primary)" } }, "账号设定"),
personaLoading
? react.createElement("div", { className: cssMap.tabState }, "加载中…")
: personaError
? react.createElement("div", { className: cssMap.tabState }, "加载失败:" + personaError)
: personaData && personaData.content
? react.createElement("div", { className: cssMap.md }, mdToReact(personaData.content))
: react.createElement("div", { className: cssMap.tabState },
react.createElement("div", null, "该账号暂无账号设定信息"),
react.createElement("button", {
type: "button",
className: cssMap.btn + " " + cssMap.btnPrimary,
style: { marginTop: 12 },
disabled: personaBusy,
title: personaBusy ? "该账号正在提炼账号设定中(约 10-20 分钟),可先处理其他账号" : "调用抖音账号分析技能执行提炼账号设定(功能三:筛选TOP4→MCP解析→生成人设卡)",
onClick: () => setPersonaRow(account),
}, personaBusy ? "提炼中…" : "提炼账号设定")))
: activeTab === "analysis"
? analysisPanel
: react.createElement("div", { className: cssMap.tabPanel },
trendBlock,
videoTable,
pager));
}
}
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() }, "←"),
account ? react.createElement("span", { className: cssMap.title }, account.account_name || "账号详情") : react.createElement("span", { className: cssMap.title }, "账号详情"),
toast ? react.createElement("span", { className: cssMap.pagerInfo }, toast) : null),
infoCard || tableArea),
benchmarkRow && react.createElement("div", { className: cssMap.mask, onClick: () => setBenchmarkRow(null) },
react.createElement("div", { className: cssMap.modal, onClick: (e) => e.stopPropagation() },
react.createElement("div", { className: cssMap.modalTitle }, "确认改写脚本"),
react.createElement("div", { className: cssMap.modalText },
react.createElement("div", null, "视频:", benchmarkRow.video_title || "-"),
react.createElement("div", null, "视频ID:", benchmarkRow.aweme_id || "-"),
react.createElement("div", null, `将基于账号设定与该视频选题,在会话 dsh_rewrite 中创作一条全新的故事脚本(非分镜、不对标分析)。同一会话串行执行,运行中不可再次触发。`)),
react.createElement("div", { className: cssMap.modalBtns },
react.createElement("button", { type: "button", className: cssMap.btn, onClick: () => setBenchmarkRow(null) }, "取消"),
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnPrimary, onClick: confirmBenchmark }, "确认创作")))),
analyzeRow && react.createElement("div", { className: cssMap.mask, onClick: () => setAnalyzeRow(null) },
react.createElement("div", { className: cssMap.modal, onClick: (e) => e.stopPropagation() },
react.createElement("div", { className: cssMap.modalTitle }, "确认解析视频"),
react.createElement("div", { className: cssMap.modalText },
react.createElement("div", null, "视频:", analyzeRow.video_title || "-"),
react.createElement("div", null, "视频ID:", analyzeRow.aweme_id || "-"),
react.createElement("div", null, "将调用抖音账号分析技能解析本视频(MCP 解析,约 3-5 分钟),解析结果保存到本地并写入数据库。确认解析?")),
react.createElement("div", { className: cssMap.modalBtns },
react.createElement("button", { type: "button", className: cssMap.btn, onClick: () => setAnalyzeRow(null) }, "取消"),
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnPrimary, onClick: submitAnalyze }, "确认解析")))),
personaRow && react.createElement("div", { className: cssMap.mask, onClick: () => setPersonaRow(null) },
react.createElement("div", { className: cssMap.modal, onClick: (e) => e.stopPropagation() },
react.createElement("div", { className: cssMap.modalTitle }, "确认提炼账号设定"),
react.createElement("div", { className: cssMap.modalText },
react.createElement("div", null, "账号:", personaRow.account_name || "-"),
react.createElement("div", null, "将调用抖音账号分析技能执行提炼账号设定(功能三:筛选 TOP4 视频 → MCP 解析 → 生成人设卡,约 10-20 分钟),结果保存到本地并写入数据库。确认执行?")),
react.createElement("div", { className: cssMap.modalBtns },
react.createElement("button", { type: "button", className: cssMap.btn, onClick: () => setPersonaRow(null) }, "取消"),
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnPrimary, onClick: submitPersonaExtract }, "确认提炼")))));
}
//#endregion
//#region plugin entry:注册到 MCN 工作台入口注册表(hidden:不显示在工作台网格,仅程序化导航打开)
const inject = [];
function apply() {
window.__mcnEntries = window.__mcnEntries || [];
// 幂等:避免插件热重载重复注册
if (!window.__mcnEntries.some((e) => e.id === "douyin-account-detail")) {
window.__mcnEntries.push({
id: "douyin-account-detail",
icon: IconChart,
title: "账号详情",
desc: "账号信息与视频列表(从账号列表进入)",
hidden: true,
component: AccountDetailPage,
});
}
}
//#endregion
exports.apply = apply;
exports.inject = inject;
return module.exports;
}
});