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

+666
View File
@@ -0,0 +1,666 @@
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))Line truncated
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;
}
});
+9
View File
@@ -0,0 +1,9 @@
// dsh-plugin-douyin-accounts 宿主端入口
// 本插件是纯客户端 UI 插件:账号管理页面通过 ./client 导出加载,
// 数据复用 dsh-plugin-mcn 服务端注册的 /mcn/api/* 路由。
// 服务端无需注册路由,仅需一个空 apply 以满足 cordis 插件契约。
function apply() {}
const inject = [];
export { apply, inject };