Files
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

667 lines
45 KiB
JavaScript
Raw Permalink 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-accounts",
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 = ".dvAcc_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;grid-template-rows:minmax(0,1fr);display:grid;overflow:hidden}.dvAcc_body{display:flex;flex-direction:column;height:100%;min-height:0;padding:16px;box-sizing:border-box;gap:12px}.dvAcc_toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.dvAcc_title{font-size:16px;font-weight:600;white-space:nowrap}.dvAcc_select{border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:6px 8px;font:var(--dsw-font-s-14);background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));color:var(--dsw-alias-label-primary)}.dvAcc_input{border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:6px 10px;font:var(--dsw-font-s-14);background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));color:var(--dsw-alias-label-primary);min-width:200px}.dvAcc_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)}.dvAcc_btn:hover{background:var(--dsw-alias-interactive-bg-hover)}.dvAcc_link{color:var(--dsw-alias-accent-fg);cursor:pointer;text-decoration:none;border-bottom:1px dashed var(--dsw-alias-accent-fg,transparent)}.dvAcc_link:hover{text-decoration:underline}.dvAcc_spacer{flex:1}.dvAcc_tableWrap{flex:1;min-height:0;overflow:auto;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px}.dvAcc_table{width:100%;border-collapse:collapse;font:var(--dsw-font-s-14);min-width:1360px}.dvAcc_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}.dvAcc_table td{padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));vertical-align:middle}.dvAcc_table tr:hover{background:var(--dsw-alias-interactive-bg-hover)}.dvAcc_cellName{font-weight:600;white-space:nowrap}.dvAcc_cellTrack{white-space:nowrap}.dvAcc_cellContent{color:var(--dsw-alias-label-secondary);max-width:420px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvAcc_cellClamp{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;white-space:normal;word-break:break-all;line-height:1.5}.dvAcc_cellM{white-space:nowrap;text-align:center}.dvAcc_badge{display:inline-block;padding:1px 8px;border-radius:10px;font-size:12px;line-height:18px;white-space:nowrap}.dvAcc_badgeSigned{background:rgba(93,125,255,.18);color:var(--dsw-alias-label-primary)}.dvAcc_badgeExternal{background:rgba(46,190,140,.18);color:var(--dsw-alias-label-primary)}.dvAcc_pager{display:flex;align-items:center;gap:12px;justify-content:flex-end}.dvAcc_pagerTotal{margin-right:auto;color:var(--dsw-alias-label-secondary);font:var(--dsw-font-s-13)}.dvAcc_pagerInfo{color:var(--dsw-alias-label-secondary);font:var(--dsw-font-s-13)}.dvAcc_loading{display:flex;align-items:center;justify-content:center;height:100%;color:var(--dsw-alias-label-secondary);font:var(--dsw-font-s-14)}.dvAcc_error{color:#ff6b6b;padding:20px;font:var(--dsw-font-s-14)}.dvAcc_empty{padding:40px 20px;text-align:center;color:var(--dsw-alias-label-secondary);font:var(--dsw-font-s-14);line-height:1.8}.dvAcc_btnPrimary{background:var(--dsw-alias-brand-primary,#4d6bfe);color:#fff;border-color:transparent}.dvAcc_btnPrimary:hover{background:var(--dsw-alias-brand-primary-hover,#3a55d6)}.dvAcc_btnIcon{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;margin-right:4px;border-radius:6px;font-size:14px}.dvAcc_mask{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:flex-start;justify-content:center;padding-top:28vh;z-index:100}.dvAcc_modal{background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));border-radius:12px;padding:20px;min-width:320px;max-width:440px;box-shadow:0 8px 30px rgba(0,0,0,.2)}.dvAcc_modalEdit{max-width:720px;width:92vw;display:flex;flex-direction:column;max-height:78vh}.dvAcc_modalBody{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:12px;padding-right:4px}.dvAcc_formGrid{display:grid;grid-template-columns:1fr 1fr;gap:10px 16px}.dvAcc_formItem{display:flex;flex-direction:column;gap:4px;font:var(--dsw-font-s-14);color:var(--dsw-alias-label-secondary)}.dvAcc_formItemWide{grid-column:1/-1}.dvAcc_textarea{border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:6px 10px;font:var(--dsw-font-s-14);background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));color:var(--dsw-alias-label-primary);resize:vertical;box-sizing:border-box;width:100%}.dvAcc_modalTitle{font-size:16px;font-weight:600;margin-bottom:12px}.dvAcc_modalText{font:var(--dsw-font-s-14);color:var(--dsw-alias-label-secondary);margin-bottom:16px;line-height:1.6}.dvAcc_modalBtns{display:flex;justify-content:flex-end;gap:8px}";
const key = "dsh-plugin-douyin-accounts/DouyinAccounts.module.css";
if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(key) + "]")) {
const el = document.createElement("style");
el.dataset.plugin = "dsh-plugin-douyin-accounts";
el.dataset.pluginCss = key;
el.textContent = css;
if (!document.querySelector('style[data-plugin-css="dvAcc-align"]')) {
const el2 = document.createElement("style");
el2.dataset.pluginCss = "dvAcc-align";
el2.textContent = ".dvAcc_table thead th:last-child{text-align:center!important}.dvAcc_table tbody td:last-child{text-align:center!important}.tblAcc thead th:nth-child(7),.tblAcc thead th:nth-child(8),.tblAcc thead th:nth-child(9),.tblAcc thead th:nth-child(10),.tblAcc tbody td:nth-child(7),.tblAcc tbody td:nth-child(8),.tblAcc tbody td:nth-child(9),.tblAcc tbody td:nth-child(10){text-align:right}.tblAcc thead th:nth-child(1),.tblAcc thead th:nth-child(11),.tblAcc thead th:nth-child(12),.tblAcc tbody td:nth-child(1),.tblAcc tbody td:nth-child(11),.tblAcc tbody td:nth-child(12){text-align:center}.dvAcc_tableWrap{overflow:auto!important;min-width:0!important}.dvAcc_table{min-width:1180px}.tblAcc thead th:nth-child(5),.tblAcc tbody td:nth-child(5){max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvAcc_table{width:auto!important;min-width:0!important;table-layout:auto}.dvAcc_page{width:100%!important;min-width:0!important;max-width:100%!important}.dvAcc_body{width:100%!important;min-width:0!important;max-width:100%!important}.dvAcc_tableWrap{width:100%!important;min-width:0!important;max-width:100%!important;overflow:auto}";
document.head.appendChild(el2);
}
document.head.appendChild(el);
}
const cssMap = {
"page": "dvAcc_page",
"body": "dvAcc_body",
"toolbar": "dvAcc_toolbar",
"title": "dvAcc_title",
"select": "dvAcc_select",
"input": "dvAcc_input",
"btn": "dvAcc_btn",
"link": "dvAcc_link",
"spacer": "dvAcc_spacer",
"tableWrap": "dvAcc_tableWrap",
"table": "dvAcc_table",
"cellName": "dvAcc_cellName",
"cellTrack": "dvAcc_cellTrack",
"cellContent": "dvAcc_cellContent",
"cellClamp": "dvAcc_cellClamp",
"cellM": "dvAcc_cellM",
"badge": "dvAcc_badge",
"badgeSigned": "dvAcc_badgeSigned",
"badgeExternal": "dvAcc_badgeExternal",
"pager": "dvAcc_pager",
"pagerTotal": "dvAcc_pagerTotal",
"pagerInfo": "dvAcc_pagerInfo",
"loading": "dvAcc_loading",
"error": "dvAcc_error",
"empty": "dvAcc_empty",
"mask": "dvAcc_mask",
"modal": "dvAcc_modal",
"modalTitle": "dvAcc_modalTitle",
"modalText": "dvAcc_modalText",
"modalBtns": "dvAcc_modalBtns",
"btnPrimary": "dvAcc_btnPrimary",
"btnIcon": "dvAcc_btnIcon",
"modalEdit": "dvAcc_modalEdit",
"modalBody": "dvAcc_modalBody",
"formGrid": "dvAcc_formGrid",
"formItem": "dvAcc_formItem",
"formItemWide": "dvAcc_formItemWide",
"textarea": "dvAcc_textarea"
};
//#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", marginRight: "4px" },
}, 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 IconRefresh = ({ size }) => react.createElement(SvgBase, { size, d: [react.createElement("path", { key: "p1", d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }), react.createElement("path", { key: "p2", d: "M21 3v5h-5" }), react.createElement("path", { key: "p3", d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }), react.createElement("path", { key: "p4", d: "M8 16H3v5" })] });
const IconEdit = ({ size }) => react.createElement(SvgBase, { size, d: [react.createElement("path", { key: "p1", d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" })] });
const IconGlobe = ({ size }) => react.createElement(SvgBase, { size, d: [react.createElement("circle", { key: "p1", cx: "12", cy: "12", r: "10" }), react.createElement("path", { key: "p2", d: "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" }), react.createElement("path", { key: "p3", d: "M2 12h20" })] });
const IconCheckSquare = ({ size }) => react.createElement(SvgBase, { size, d: [react.createElement("rect", { key: "p1", x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }), react.createElement("path", { key: "p2", d: "m9 12 2 2 4-4" })] });
const IconUsers = ({ size }) => react.createElement(SvgBase, { size, d: [react.createElement("path", { key: "p1", d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }), react.createElement("circle", { key: "p2", cx: "9", cy: "7", r: "4" }), react.createElement("path", { key: "p3", d: "M22 21v-2a4 4 0 0 0-3-3.87" }), react.createElement("path", { key: "p4", d: "M16 3.13a4 4 0 0 1 0 7.75" })] });
// 账号类型:all 全部 / signed 签约(MCP 数据)/ external 外部(skill 数据,结构待确认)
const TYPE_OPTIONS = [
{ value: "all", label: "全部" },
{ value: "signed", label: "内部" },
{ value: "external", label: "外部" }
];
const TYPE_LABEL = { signed: "内部", external: "外部" };
// 长文本缩短为约 1/3(上限 150 字),单元格保持单行省略展示;悬浮 title 仍为全文
const shortText = (s) => {
if (!s) return "-";
const full = String(s);
const n = Math.min(Math.ceil(full.length / 3), 150);
return full.length > n ? full.slice(0, n) + "…" : full;
};
function AccountsPage() {
const [type, setType] = react.useState("all"); // 默认全部
const [search, setSearch] = react.useState("");
const [track, setTrack] = react.useState("");
const [tracks, setTracks] = react.useState([]);
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(true);
const [error, setError] = react.useState(null);
const [toast, setToast] = react.useState(null);
const [importDirOpen, setImportDirOpen] = react.useState(false);
const [importDirInput, setImportDirInput] = react.useState("");
// 网页获取账号信息(抖音账号分析技能):账号名称 / 抖音号 / 抖音账号地址
const [importAccName, setImportAccName] = react.useState("");
const [importAccDid, setImportAccDid] = react.useState("");
const [importAccUrl, setImportAccUrl] = react.useState("");
const [importing, setImporting] = react.useState(false);
// 编辑账号信息弹窗状态
const [editRow, setEditRow] = react.useState(null);
const [editForm, setEditForm] = react.useState(null);
const [saving, setSaving] = react.useState(false);
// 任务状态轮询(creative/import 执行中 → 按钮置灰;导入/刷新完成后自动刷新列表)
const [taskStatus, setTaskStatus] = react.useState({ creative: "idle", import: "idle" });
// 等待 AI 会话处理完成的标记:处理后自动刷新列表数据
const [pendingRefresh, setPendingRefresh] = react.useState(false);
// V1.0 同步:批量栏 + 行内 AI创作(原创选题)状态
const [batchOn, setBatchOn] = react.useState(false);
const [selected, setSelected] = react.useState({});
const [createRow, setCreateRow] = react.useState(null);
const [createText, setCreateText] = react.useState("");
const [creating, setCreating] = react.useState(false);
// V1.0 对齐:更新关注账号(批量串行)状态
const [updBusy, setUpdBusy] = react.useState(false);
const [updLabel, setUpdLabel] = react.useState("");
const prevImportRef = react.useRef("idle");
// 刷新确认弹窗:待刷新账号
const [refreshRow, setRefreshRow] = react.useState(null);
react.useEffect(() => {
let alive = true;
const poll = () => {
fetch("/mcn/api/task/status")
.then((r) => r.json())
.then((d) => {
if (!alive) return;
const st = { creative: d.creative || "idle", import: d.import || "idle" };
setTaskStatus(st);
// 导入/刷新会话从 running → idle:AI 处理完成,刷新列表数据
if (pendingRefresh && prevImportRef.current === "running" && st.import === "idle") {
setPendingRefresh(false);
setToast("数据处理完成,列表已刷新");
load(1, type, search, track);
}
prevImportRef.current = st.import;
})
.catch(() => {});
};
poll();
const timer = setInterval(poll, 5000);
return () => { alive = false; clearInterval(timer); };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pendingRefresh]);
const creativeBusy = taskStatus.creative === "running";
const importBusy = taskStatus.import === "running";
const loadTracks = () => {
fetch("/mcn/api/tracks").then((r) => r.json()).then((d) => {
if (d.ok) setTracks(d.tracks || []);
}).catch(() => {});
};
const load = (p = page, t = type, s = search, tr = track) => {
setLoading(true);
const params = new URLSearchParams({ page: String(p), pageSize: String(pageSize), type: t, sortBy: "id", order: "asc" });
if (s) params.set("search", s);
if (tr) params.set("track", tr);
fetch("/mcn/api/accounts?" + params.toString())
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "加载失败");
setItems(d.items || []);
setTotal(d.total || 0);
setPage(d.page || 1);
setError(null);
})
.catch((e) => setError(e.message))
.finally(() => setLoading(false));
};
// 收藏/取消收藏:成功后刷新列表(收藏的账号置顶,按粉丝数降序)
const toggleFavorite = (row) => {
fetch("/mcn/api/account/favorite", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ accountId: row.id, favorite: row.favorite ? 0 : 1 }),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "操作失败");
setToast(row.favorite ? `已取消收藏「${row.account_name}」` : `已收藏「${row.account_name}」`);
load(1, type, search, track);
})
.catch((e) => setToast(e.message || "操作失败"));
};
react.useEffect(() => {
loadTracks();
load(1, "all", "", "");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
react.useEffect(() => {
if (!toast) return;
const timer = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(timer);
}, [toast]);
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const onTypeChange = (v) => { setType(v); load(1, v, search, track); };
const onSearch = () => { load(1, type, search, track); };
const onReset = () => { setSearch(""); setTrack(""); setType("all"); load(1, "all", "", ""); };
const createFor = (row) => {
const info = [
`账号名:${row.account_name}`,
row.track ? `赛道:${row.track}` : "",
row.content ? `账号定位:${row.content}` : "",
].filter(Boolean).join(";");
const prompt = `请为达人「${row.account_name}」创作一条短视频脚本。\n达人信息:${info}\n请先进行账号设定分析,再生成完整脚本。`;
fetch("/mcn/api/creative", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "触发失败");
setToast(d.message || "已开始创作,请切换到对应会话查看");
})
.catch((e) => setToast("创作触发失败: " + e.message));
};
const typeBadge = (t) => {
const label = TYPE_LABEL[t] || "签约";
const cls = t === "external" ? cssMap.badge + " " + cssMap.badgeExternal : cssMap.badge + " " + cssMap.badgeSigned;
return react.createElement("span", { className: cls }, label);
};
// 编辑账号信息:打开弹窗(账号名称不可改)
const openEdit = (row) => {
setEditRow(row);
setEditForm({
track: row.track ?? "",
is_ai: row.is_ai === "是" ? "是" : "否",
content: row.content ?? "",
tags: row.tags ?? "",
masterpiece: row.masterpiece ?? "",
douyin_id: row.douyin_id ?? "",
followers: row.followers ?? "",
total_likes: row.total_likes ?? "",
works_count: row.works_count ?? "",
ip_location: row.ip_location ?? "",
location: row.location ?? "",
age: row.age ?? "",
bio: row.bio ?? "",
});
};
const saveEdit = () => {
if (!editRow) return;
setSaving(true);
const payload = { id: editRow.id };
for (const k of Object.keys(editForm)) {
const val = String(editForm[k] ?? "").trim();
if (k === "works_count" || k === "age") {
payload[k] = val === "" ? null : (Number.isFinite(Number(val)) ? Number(val) : null);
} else {
payload[k] = val === "" ? null : val;
}
}
fetch("/mcn/api/account/update", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "保存失败");
setToast(d.message || "已保存");
setEditRow(null);
setEditForm(null);
load(1, type, search, track);
})
.catch((e) => setToast("保存失败: " + e.message))
.finally(() => setSaving(false));
};
// 编辑表单字段控件(label + input/textarea)
const editField = (label, key, opts) => {
const o = opts || {};
const cls = cssMap.formItem + (o.wide ? " " + cssMap.formItemWide : "");
const control = o.textarea
? react.createElement("textarea", {
className: cssMap.textarea,
rows: o.rows || 3,
value: editForm[key] ?? "",
onChange: (e) => setEditForm({ ...editForm, [key]: e.target.value }),
})
: react.createElement("input", {
className: cssMap.input,
type: o.type || "text",
style: { boxSizing: "border-box", width: "100%" },
value: editForm[key] ?? "",
onChange: (e) => setEditForm({ ...editForm, [key]: e.target.value }),
});
return react.createElement("label", { className: cls, key }, react.createElement("span", null, label), control);
};
// 导入:支持两种方式(AI 判断处理)
// 1) 本地文件夹路径 → 提交给宿主端 → AI 读取分析确认后导入
// 2) 抖音账号信息(名称/抖音号/地址)→ AI 调用抖音账号分析技能网页获取账号信息与视频列表并写入数据库
const submitImport = () => {
const dir = importDirInput.trim();
const accName = importAccName.trim();
const accDid = importAccDid.trim();
const accUrl = importAccUrl.trim();
if (!dir && !accName && !accDid && !accUrl) { setToast("请输入文件夹路径或抖音账号信息"); return; }
setImporting(true);
const body = dir
? { dir }
: { account: { name: accName, douyinId: accDid, url: accUrl } };
fetch("/mcn/api/external/stage", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "提交失败");
setToast(d.message || (dir ? "已提交,AI 正在分析文件结构,请到对话中确认导入" : "已提交,AI 正在获取账号信息与视频列表,请到对话中查看"));
setImportDirOpen(false);
setImportDirInput("");
setImportAccName("");
setImportAccDid("");
setImportAccUrl("");
// AI 处理完成后自动刷新列表
setPendingRefresh(true);
})
.catch((err) => setToast("提交失败: " + err.message))
.finally(() => setImporting(false));
};
// 刷新账号数据:确认弹窗 → AI 会话(mcn-dou-analysis 技能)重新获取并更新数据库
const submitRefresh = () => {
const row = refreshRow;
if (!row) return;
setRefreshRow(null);
fetch("/mcn/api/account/refresh-web", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: row.id }),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "提交失败");
setToast(d.message || "已提交刷新,AI 正在重新获取数据");
// AI 处理完成后自动刷新列表
setPendingRefresh(true);
})
.catch((err) => setToast("刷新提交失败: " + err.message));
};
// V1.0 对齐:更新所有已关注账号(⭐)——逐账号串行 refresh-web,等待各自完成;进度与汇总提示
const updateFollowedAccounts = () => {
if (updBusy) return;
fetch("/mcn/api/accounts?type=all&page=1&pageSize=100")
.then((r) => r.json())
.then((d) => {
const favs = ((d.items || [])).filter((a) => a.favorite);
if (!favs.length) { setToast("⭐ 暂无已关注账号(先点击列表中的 ☆ 关注)"); return; }
const names = favs.map((a) => String(a.account_name || "").trim()).filter(Boolean);
if (!names.length) { setToast("已关注账号缺少账号名,无法更新"); return; }
if (!window.confirm("将重新抓取 " + names.length + " 个已关注账号(⭐)的最新账号数据与视频列表(后台逐个执行,每个需数分钟;账号身份标识以本次抓取为准,强制覆盖刷新)。\n账号:" + names.slice(0, 8).join("、") + (names.length > 8 ? " 等" : ""))) return;
setUpdBusy(true);
const done = []; const failed = [];
const loop = (i) => {
if (i >= favs.length) {
setUpdBusy(false); setUpdLabel("");
setToast(done.length ? "✅ 已更新 " + done.length + " 个关注账号" + (failed.length ? ";" + failed.length + " 个未完成:" + failed.join("、") : "") : "❌ 更新未完成:" + failed.join("、"));
load(1, type, search, track);
return;
}
const id = favs[i].id;
setUpdLabel("更新中 " + (i + 1) + "/" + favs.length + "…");
fetch("/mcn/api/account/refresh-web", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: id }) })
.then((r) => r.json())
.then((d2) => {
if (!d2.ok) { failed.push(names[i] + "(" + (d2.error || "提交失败") + ")"); loop(i + 1); return; }
const deadline = Date.now() + 20 * 60 * 1000;
const wait = () => {
fetch("/mcn/api/task/status")
.then((r2) => r2.json())
.then((st) => {
const ilist = Array.isArray(st.imports) ? st.imports : [];
const t = ilist.find((x) => x.id === "refresh:" + id);
const running = !!(t && t.status === "running");
if (!running || Date.now() > deadline) { done.push(names[i]); loop(i + 1); return; }
setTimeout(wait, 5000);
})
.catch(() => { done.push(names[i]); loop(i + 1); });
};
wait();
})
.catch((e) => { failed.push(names[i] + "(" + e.message + ")"); loop(i + 1); });
};
loop(0);
})
.catch((e) => setToast("获取账号列表失败: " + e.message));
};
// ---- V1.0 同步:批量操作(勾选→连坐删除)/ AI脚本列跳转 / 行内 AI创作(原创选题) ----
const openScriptsOf = (row) => {
try {
if (window.__mcnNav && typeof window.__mcnNav.openView === "function") {
window.__mcnNav.openView("ai-videos", { accountId: row.id, accountName: row.account_name });
} else { setToast("请先打开工作台「AI写脚本」后重试"); }
} catch (e) { /* 导航失败忽略 */ }
};
const toggleSelect = (id) => setSelected((prev) => { const n = Object.assign({}, prev); if (n[id]) delete n[id]; else n[id] = true; return n; });
const pageAllSelected = items.length > 0 && items.every((r) => !!selected[r.id]);
const toggleSelectAll = () => {
const n = Object.assign({}, selected);
if (pageAllSelected) items.forEach((r) => { delete n[r.id]; }); else items.forEach((r) => { n[r.id] = true; });
setSelected(n);
};
const selCount = Object.keys(selected).filter((k) => selected[k]).length;
const exitBatch = () => { setBatchOn(false); setSelected({}); };
const submitBatchDelete = () => {
const ids = Object.keys(selected).filter((k) => selected[k]).map(Number);
if (!ids.length) { setToast("请先勾选要删除的账号"); return; }
if (!window.confirm("将删除所选 " + ids.length + " 个账号及其视频数据、视频解析、账号设定、数据分析、AI脚本与复盘数据,不可恢复。确认删除?")) return;
fetch("/mcn/api/batch/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ kind: "accounts", ids: ids }),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "删除失败");
setToast(d.message || ("已删除 " + d.deleted + " 个账号(含关联解析/复盘/分镜)"));
exitBatch();
load(1, type, search, track);
})
.catch((e) => setToast("删除失败: " + e.message));
};
const openCreate = (row) => { setCreateRow(row); setCreateText(""); };
const submitCreate = () => {
const row = createRow;
if (!row || creating) return;
setCreating(true);
fetch("/mcn/api/rewrite/original", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ accountId: row.id, reqText: createText.trim() }),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "提交失败");
setToast(d.message || "已提交原创选题创作,完成后自动入库到「AI写脚本 → 原创选题创作」");
setCreateRow(null); setCreateText("");
})
.catch((e) => setToast("创作提交失败: " + e.message))
.finally(() => setCreating(false));
};
const toolbar = react.createElement("div", { className: cssMap.toolbar },
react.createElement("button", {
type: "button",
className: cssMap.btn,
title: "返回工作台",
onClick: () => { try { window.__mcnNav && window.__mcnNav.goHome(); } catch (e) {} },
}, "←"),
react.createElement("span", { className: cssMap.title }, "账号列表"),
);
const filterRow = react.createElement("div", { className: cssMap.toolbar, style: { flexWrap: "wrap" } }, react.createElement("select", {
className: cssMap.select,
value: type,
onChange: (e) => onTypeChange(e.target.value),
title: "账号类型",
}, TYPE_OPTIONS.map((o) => react.createElement("option", { value: o.value, key: o.value }, o.label))),
react.createElement("div", { style: { marginLeft: "auto", flex: "1 1 0%", minWidth: 0, display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" } },
react.createElement("input", {
className: cssMap.input,
placeholder: "搜索账号名 / 内容 / 赛道",
value: search,
onChange: (e) => setSearch(e.target.value),
onKeyDown: (e) => { if (e.key === "Enter") onSearch(); },
}),
react.createElement("select", {
className: cssMap.select,
value: track,
onChange: (e) => { setTrack(e.target.value); load(1, type, search, e.target.value); },
}, [react.createElement("option", { value: "", key: "" }, "全部赛道")].concat(tracks.map((t) => react.createElement("option", { value: t, key: t }, t)))),
type !== "signed" && react.createElement("button", {
type: "button",
className: cssMap.btn,
title: importBusy ? "导入分析任务执行中,请等待完成" : "导入外部账号数据(输入文件夹路径)",
onClick: () => setImportDirOpen(true),
disabled: importing || importBusy,
}, importing || importBusy ? "导入执行中…" : react.createElement(react.Fragment, null, react.createElement(IconDownload, {}), "添加")),
react.createElement("button", { type: "button", className: cssMap.btn, disabled: updBusy, title: updBusy ? "正在更新关注账号…" : "重新抓取所有已关注账号(⭐)的最新账号数据与视频列表(后台任务逐个执行)", onClick: updateFollowedAccounts }, updBusy ? updLabel : react.createElement(IconGlobe, { size: 15 })),
toast ? react.createElement("span", { className: cssMap.pagerInfo }, toast) : null));let tableBody;
if (loading) {
tableBody = react.createElement("div", { className: cssMap.loading }, "加载中…");
} else if (error) {
tableBody = react.createElement("div", { className: cssMap.error }, error);
} else if (items.length === 0) {
tableBody = type === "external"
? react.createElement("div", { className: cssMap.empty }, "暂无外部账号数据")
: react.createElement("div", { className: cssMap.empty }, "暂无数据");
} else {
// 首页排行榜同款“弹性行”排版:左组紧凑靠左、中间弹性空隙、右组紧凑靠右(列宽不摊开)
const C = (al, clip) => ({ padding: "9px 10px", borderBottom: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", textAlign: al || "left", minWidth: 0, whiteSpace: "nowrap", overflow: clip ? "hidden" : undefined, textOverflow: clip ? "ellipsis" : undefined, boxSizing: "border-box" });
const HST = { padding: "9px 10px", textAlign: "center", borderBottom: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", color: "var(--dsw-alias-label-secondary,#6b7280)", fontWeight: 600, fontSize: 13, whiteSpace: "nowrap" };
const template = batchOn ? "36px 50px 66px 120px 120px 48px minmax(56px,1fr) 68px 70px 62px 52px 66px 110px" : "50px 66px 120px 120px 48px minmax(56px,1fr) 68px 70px 62px 52px 66px 110px";
const headCells = [];
const rowCells = (row) => {
const cells = [];
if (batchOn) cells.push(react.createElement("div", { key: "b", style: C("center") }, react.createElement("input", { type: "checkbox", checked: !!selected[row.id], onChange: () => toggleSelect(row.id) })));
cells.push(
react.createElement("div", { key: "f", style: C("center") }, react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnSm, title: row.favorite ? "取消收藏(点击后从置顶移除)" : "收藏该账号(收藏的账号排在列表最前,按粉丝数降序)", onClick: () => toggleFavorite(row), style: row.favorite ? { color: "#f5b301", borderColor: "#f5b30166" } : { color: "var(--dsw-alias-label-secondary,#8a8f98)" } }, row.favorite ? "★" : "☆")),
react.createElement("div", { key: "ty", style: C("left") }, typeBadge(row.account_type)),
react.createElement("div", { key: "nm", style: Object.assign(C("left"), { whiteSpace: "normal", wordBreak: "break-all" }) }, react.createElement("span", { className: cssMap.link, title: "查看账号详情(账号信息 + 视频列表)", style: { cursor: "pointer", color: "var(--mcn-brand,#4176e6)", fontWeight: 600 }, onClick: () => window.__mcnNav?.open("douyin-account-detail", { accountId: row.id, accountName: row.account_name }) }, row.account_name)),
react.createElement("div", { key: "tr", style: Object.assign(C("left"), { whiteSpace: "normal", wordBreak: "break-all" }) }, row.track || "-"),
react.createElement("div", { key: "ai", style: C("left") }, row.is_ai === "是" ? "是" : "否"),
react.createElement("div", { key: "bj", style: C("left", true) }, row.bio || "-"),
react.createElement("div", { key: "f1", style: C("right") }, row.followers ?? "-"),
react.createElement("div", { key: "f2", style: C("right") }, row.total_likes ?? "-"),
react.createElement("div", { key: "f3", style: C("right") }, row.works_count ?? "-"),
react.createElement("div", { key: "f4", style: C("right") }, row.video_count ?? 0),
react.createElement("div", { key: "as", style: C("center", true) }, row.rewrite_count > 0 ? react.createElement("span", { className: cssMap.link, style: { cursor: "pointer", color: "var(--mcn-brand,#4176e6)" }, title: "该账号 AI 脚本数(参考选题+原创选题),点击进入 AI写脚本 页按账号查看", onClick: () => openScriptsOf(row) }, String(row.rewrite_count) + " 条") : "-"),
react.createElement("div", { key: "op", style: C("center") }, react.createElement("div", { style: { display: "inline-flex", gap: 6, alignItems: "center" } },
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnIcon, title: "编辑账号信息(账号名称不可修改)", onClick: () => openEdit(row) }, react.createElement(IconEdit, { size: 14 })),
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnIcon, title: "AI创作(原创选题:基于该账号设定创作全新脚本,完成后入库「AI写脚本 → 原创选题创作」)", onClick: () => openCreate(row) }, "创作"))));
return cells;
};
const cols = [
{ t: "关注", a: "center", w: 58 }, { t: "类型", a: "left", w: 64 }, { t: "账号名", a: "left", w: 150 }, { t: "赛道", a: "left", w: 92 }, { t: "AI制作", a: "left", w: 74 }, { t: "简介", a: "left", w: 220 }, { t: "粉丝", a: "right", w: 92 }, { t: "获赞", a: "right", w: 96 }, { t: "作品数", a: "right", w: 84 }, { t: "视频数", a: "right", w: 84 }, { t: "AI脚本", a: "center", w: 92 }, { t: "操作", a: "center", w: 150 }
];
if (batchOn) headCells.push(react.createElement("div", { key: "hb", style: HST }, react.createElement("input", { type: "checkbox", checked: pageAllSelected, title: "全选本页", onChange: () => toggleSelectAll() })));
cols.forEach((cc, ci) => { if (!cc.gap) headCells.push(react.createElement("div", { key: "h" + ci, style: { ...HST, textAlign: cc.a } }, cc.t)); else headCells.push(react.createElement("div", { key: "hg" + ci })); });
tableBody = react.createElement("div", { style: { display: "flex", flexDirection: "column", minWidth: 0 } },
react.createElement("div", { style: { display: "grid", gridTemplateColumns: template, alignItems: "center", background: "var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))", position: "sticky", top: 0, zIndex: 2 } }, headCells),
items.map((row) => react.createElement("div", { key: row.id, style: { display: "grid", gridTemplateColumns: template, alignItems: "center" } }, rowCells(row))));
}
return react.createElement("main", { className: cssMap.page, "aria-label": "抖音账号" },
react.createElement("div", { className: cssMap.body },
toolbar,
filterRow,
react.createElement("div", { className: cssMap.tableWrap }, tableBody),
react.createElement("div", { className: cssMap.pager },
react.createElement("div", { style: { display: "inline-flex", alignItems: "center", gap: 8, marginRight: 12 } },
!batchOn
? react.createElement("button", { type: "button", className: cssMap.btn, title: "批量操作:勾选多个账号后删除(连坐清理其视频/解析/设定/AI脚本)", onClick: () => { setBatchOn(true); setSelected({}); } }, react.createElement(IconCheckSquare, { size: 16 }))
: react.createElement(react.Fragment, null,
react.createElement("span", { className: cssMap.pagerInfo }, "已选 " + selCount),
react.createElement("button", { type: "button", className: cssMap.btn, disabled: selCount === 0, style: selCount > 0 ? { borderColor: "#e5484d", color: "#e5484d" } : {}, onClick: submitBatchDelete }, "删除所选"),
react.createElement("button", { type: "button", className: cssMap.btn, onClick: exitBatch }, "取消")),
),
react.createElement("span", { className: cssMap.pagerTotal }, `共 ${total} 条`),
react.createElement("span", { className: cssMap.pagerInfo }, `第 ${page} / ${totalPages} 页`),
react.createElement("button", { type: "button", className: cssMap.btn, disabled: page <= 1, onClick: () => load(page - 1) }, "上一页"),
react.createElement("button", { type: "button", className: cssMap.btn, disabled: page >= totalPages, onClick: () => load(page + 1) }, "下一页"))),
createRow && react.createElement("div", { className: cssMap.mask, onClick: () => setCreateRow(null) },
react.createElement("div", { className: cssMap.modal, onClick: (e) => e.stopPropagation() },
react.createElement("div", { className: cssMap.modalTitle }, "AI创作 · " + createRow.account_name),
react.createElement("div", { className: cssMap.modalText }, "原创选题创作(无原视频):AI 将按「短视频工作台」技能完整流程(S1-S11)基于账号设定与下列创作需求创作全新脚本并自动入库。"),
react.createElement("textarea", { className: cssMap.textarea, rows: 6, style: { width: "100%", boxSizing: "border-box", marginBottom: "12px" }, placeholder: "创作需求(可选):创意 / 创新爆点 / 痛点爽点 / 叙事结构 / 时长等,回车换行。空着则交由 AI 按人设自主决策", value: createText, onChange: (e) => setCreateText(e.target.value) }),
react.createElement("div", { className: cssMap.modalBtns },
react.createElement("button", { type: "button", className: cssMap.btn, onClick: () => setCreateRow(null) }, "取消"),
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnPrimary, disabled: creating, onClick: submitCreate }, creating ? "提交中…" : "提交创作")),
)),
refreshRow && react.createElement("div", { className: cssMap.mask, onClick: () => setRefreshRow(null) },
react.createElement("div", { className: cssMap.modal, onClick: (e) => e.stopPropagation() },
react.createElement("div", { className: cssMap.modalTitle }, "确认刷新账号数据"),
react.createElement("div", { className: cssMap.modalText }, `将调用 AI 会话(抖音账号分析技能)通过抖音网页重新获取「${refreshRow.account_name}」的账号信息与视频列表,并更新到数据库。确认刷新?`),
react.createElement("div", { className: cssMap.modalBtns },
react.createElement("button", { type: "button", className: cssMap.btn, onClick: () => setRefreshRow(null) }, "取消"),
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnPrimary, onClick: submitRefresh }, "确认刷新")))),
importDirOpen && react.createElement("div", { className: cssMap.mask, onClick: () => setImportDirOpen(false) },
react.createElement("div", { className: cssMap.modal, onClick: (e) => e.stopPropagation() },
react.createElement("div", { className: cssMap.modalTitle }, "导入外部账号数据"),
react.createElement("div", { className: cssMap.modalText }, "方式一:填写抖音账号信息,AI 通过抖音网页获取账号信息与视频列表(写入数据库并保存到本地)。"),
react.createElement("input", {
className: cssMap.input,
style: { width: "100%", boxSizing: "border-box", marginBottom: "8px" },
placeholder: "抖音账号名称(如:王微斯)",
value: importAccName,
onChange: (e) => setImportAccName(e.target.value),
}),
react.createElement("input", {
className: cssMap.input,
style: { width: "100%", boxSizing: "border-box", marginBottom: "8px" },
placeholder: "抖音号(如:1919714646)",
value: importAccDid,
onChange: (e) => setImportAccDid(e.target.value),
}),
react.createElement("input", {
className: cssMap.input,
style: { width: "100%", boxSizing: "border-box", marginBottom: "12px" },
placeholder: "抖音账号地址(如:https://www.douyin.com/user/xxx)",
value: importAccUrl,
onChange: (e) => setImportAccUrl(e.target.value),
}),
react.createElement("div", { className: cssMap.modalText, style: { margin: "10px 0 8px" } }, "方式二:输入本地数据文件夹绝对路径,AI 直接读取目录分析文件结构,确认后导入数据库。"),
react.createElement("input", {
className: cssMap.input,
style: { width: "100%", boxSizing: "border-box", marginBottom: "12px" },
placeholder: "本地文件夹路径(如 C:\\Users\\Administrator\\Desktop\\账号对标)",
value: importDirInput,
onChange: (e) => setImportDirInput(e.target.value),
onKeyDown: (e) => { if (e.key === "Enter") submitImport(); },
}),
react.createElement("div", { className: cssMap.modalBtns },
react.createElement("button", { type: "button", className: cssMap.btn, onClick: () => setImportDirOpen(false) }, "取消"),
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnPrimary, onClick: submitImport, disabled: importing }, importing ? "提交中…" : "提交")))),
editRow && editForm && react.createElement("div", { className: cssMap.mask, onClick: () => setEditRow(null) },
react.createElement("div", { className: cssMap.modal + " " + cssMap.modalEdit, onClick: (e) => e.stopPropagation() },
react.createElement("div", { className: cssMap.modalTitle }, "编辑账号:" + editRow.account_name),
react.createElement("div", { className: cssMap.modalBody },
react.createElement("div", { className: cssMap.formGrid },
editField("赛道", "track"),
react.createElement("label", { className: cssMap.formItem, key: "is_ai" },
react.createElement("span", null, "AI制作"),
react.createElement("select", {
className: cssMap.input,
style: { boxSizing: "border-box", width: "100%" },
value: editForm.is_ai ?? "否",
onChange: (e) => setEditForm({ ...editForm, is_ai: e.target.value }),
},
react.createElement("option", { value: "否", key: "n" }, "否"),
react.createElement("option", { value: "是", key: "y" }, "是"))),
editField("定位/内容", "content", { textarea: true, rows: 4, wide: true }),
editField("抖音号", "douyin_id"),
editField("粉丝", "followers"),
editField("获赞", "total_likes"),
editField("作品数", "works_count", { type: "number" }),
editField("IP属地", "ip_location"),
editField("地区", "location"),
editField("年龄", "age", { type: "number" }),
editField("标签", "tags"),
editField("代表作", "masterpiece"),
editField("简介", "bio", { textarea: true, rows: 4, wide: true }),
)),
react.createElement("div", { className: cssMap.modalBtns },
react.createElement("button", { type: "button", className: cssMap.btn, onClick: () => setEditRow(null) }, "取消"),
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnPrimary, onClick: saveEdit, disabled: saving }, saving ? "保存中…" : "保存")))));
}
//#endregion
//#region plugin entry:注册到 MCN 工作台入口注册表
const inject = [];
function apply() {
window.__mcnEntries = window.__mcnEntries || [];
// 幂等:避免插件热重载重复注册
if (!window.__mcnEntries.some((e) => e.id === "douyin-accounts")) {
window.__mcnEntries.push({
id: "douyin-accounts",
icon: IconUsers,
title: "热门账号",
desc: "签约/外部账号分析",
component: AccountsPage,
});
}
}
//#endregion
exports.apply = apply;
exports.inject = inject;
return module.exports;
}
});