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; } });