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 本地已不存在,本提交记为删除
This commit is contained in:
admin committed 2026-09-10 22:31:45 +08:00
commit 856e632807
880 files changed
+225426

No files matched your search

@@ -0,0 +1,878 @@
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(--dLine truncated
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;
}
});