const STORAGE_KEY = "storyforge-web-v4-session"; function detectDefaultBackendUrl() { if (typeof window === "undefined") { return "http://127.0.0.1:8081"; } const { origin, hostname, port, pathname } = window.location; if (/^https?:/i.test(origin) && hostname === "storyforge.hyzq.net") { return origin; } if (/^https?:/i.test(origin) && pathname.startsWith("/storyforge")) { return `${origin}/storyforge`; } if ((hostname === "127.0.0.1" || hostname === "localhost") && port && port !== "8081") { return "http://127.0.0.1:8081"; } return "http://127.0.0.1:8081"; } const DEFAULT_BACKEND_URL = detectDefaultBackendUrl(); const navButtons = document.querySelectorAll("[data-screen-target]"); const screens = Array.from(document.querySelectorAll("[data-screen]")); const screenMap = Object.fromEntries(screens.map((screen) => [screen.dataset.screen, screen])); const appState = { screen: window.location.hash.replace("#", "") || "dashboard", session: loadStoredSession(), me: null, dashboard: null, contentSources: [], accounts: [], selectedAccountId: "", selectedWorkspace: null, selectedVideos: { items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 }, documents: [], discoveryQuery: "", currentPlatform: localStorage.getItem(STORAGE_KEY + ":currentPlatform") || "", selectedProjectId: "", selectedAssistantId: "", lastSeenAt: Number(localStorage.getItem(STORAGE_KEY + ":lastSeenAt") || Date.now()), trackingAccounts: [], trackingDigest: null, reviews: [], liveRecorderSources: [], liveRecorderStatus: null, liveRecorderFiles: [], storageStatus: null, integrationHealth: null, localModelCatalog: null, backendCapabilities: null, onelinerProfile: null, onelinerSessions: [], selectedOnelinerSessionId: "", onelinerMessages: [], onelinerActionRegistry: [], platformAgents: [], tenantQuota: null, tenantUsage: null, adminOpsOverview: null, busy: false, message: "", lastAction: null, lastGeneratedCopy: null, lastSimilaritySearch: null, lastJobDetail: null }; const INTEGRATION_ORDER = ["local_model", "live_recorder", "cutvideo", "huobao", "n8n", "asr"]; const ACTIVE_PLATFORMS = [ { value: "douyin", label: "抖音" }, { value: "xiaohongshu", label: "小红书" }, { value: "bilibili", label: "哔哩哔哩" }, { value: "kuaishou", label: "快手" }, { value: "wechat_video", label: "微信视频号" } ]; const ACTIVE_PLATFORM_CHIPS = ["全平台", "抖音", "小红书", "B站", "快手", "视频号"]; function makePlatformRoutes(platform) { return { accounts: `/v2/${platform}/accounts`, workspace: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/workspace`, videos: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/videos?limit=80`, analyzeAccount: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/analysis`, analyzeTopVideos: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/videos/analyze-top`, similarSearches: `/v2/${platform}/similar-searches`, similarSearchDetail: (searchId) => `/v2/${platform}/similar-searches/${encodeURIComponent(searchId)}`, benchmarkLinks: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/benchmark-links`, trackingAccounts: `/v2/${platform}/tracking/accounts`, trackingDigest: `/v2/${platform}/tracking/digest`, trackingRefresh: `/v2/${platform}/tracking/refresh`, trackingCursor: `/v2/${platform}/tracking/cursor`, trackingAccountRefresh: (trackedAccountId) => `/v2/${platform}/tracking/accounts/${encodeURIComponent(trackedAccountId)}/refresh` }; } const PLATFORM_REGISTRY = { douyin: { label: "抖音", shortLabel: "抖音", workbenchReady: true, routes: makePlatformRoutes("douyin") }, xiaohongshu: { label: "小红书", shortLabel: "小红书", workbenchReady: true, routes: makePlatformRoutes("xiaohongshu") }, bilibili: { label: "哔哩哔哩", shortLabel: "B站", workbenchReady: true, routes: makePlatformRoutes("bilibili") }, kuaishou: { label: "快手", shortLabel: "快手", workbenchReady: true, routes: makePlatformRoutes("kuaishou") }, wechat_video: { label: "微信视频号", shortLabel: "视频号", workbenchReady: true, routes: makePlatformRoutes("wechat_video") } }; const INTEGRATION_META = { local_model: { label: "本机模型", hint: "OpenAI-compatible", impacts: ["账号分析", "高分分析", "文案生成"] }, live_recorder: { label: "直播录制", hint: "fnOS NAS", impacts: ["直播源导入", "录制控制"] }, cutvideo: { label: "自动剪辑", hint: "Windows cutvideo", impacts: ["实拍剪辑"] }, huobao: { label: "AI 视频", hint: "huobao-drama", impacts: ["AI 视频"] }, n8n: { label: "编排", hint: "n8n workflow", impacts: ["AI 视频", "实拍剪辑", "自动链路"] }, asr: { label: "ASR", hint: "素材转写", impacts: ["分析转写"] } }; const PIPELINE_GUARDS = { aiVideo: { label: "AI 视频", openAction: "open-ai-video", jobAction: "job-to-ai-video", dependencies: ["n8n", "huobao"] }, realCut: { label: "实拍剪辑", openAction: "open-real-cut", jobAction: "job-to-real-cut", dependencies: ["n8n", "cutvideo"] } }; const ONELINER_INTENT_LABELS = { create_project: "创建项目", create_assistant: "创建 Agent", import_homepage: "导入主页", track_account: "跟踪账号", analyze_account: "分析账号", analyze_top_videos: "分析高分作品", generate_copy: "生成文案", ai_video: "AI 视频", real_cut: "实拍剪辑", review: "发布复盘", live_recorder: "直播录制", storage_status: "存储状态", ops_admin: "运维巡检", custom: "自定义任务" }; function safeArray(value) { return Array.isArray(value) ? value : []; } function getRuntimePlatformValues() { const fromDashboard = safeArray(appState.dashboard?.supported_platforms) .map((item) => normalizePlatformValue(item, "")) .filter((item) => item && PLATFORM_REGISTRY[item]); if (fromDashboard.length) { return fromDashboard; } return ACTIVE_PLATFORMS.map((item) => item.value); } function getPlatformOptions() { return getRuntimePlatformValues().map((value) => ({ value, label: getPlatformMeta(value)?.label || value })); } function normalizePlatformValue(value, fallback = "douyin") { const normalized = String(value || "").trim().toLowerCase(); if (!normalized) return fallback; const byValue = ACTIVE_PLATFORMS.find((item) => item.value === normalized); if (byValue) return byValue.value; const byLabel = ACTIVE_PLATFORMS.find((item) => item.label === value); return byLabel?.value || fallback; } function platformLabel(value) { const matched = ACTIVE_PLATFORMS.find((item) => item.value === normalizePlatformValue(value, "")); return matched?.label || String(value || "抖音"); } function getPlatformMeta(value) { return PLATFORM_REGISTRY[normalizePlatformValue(value, "")] || null; } function getPlatformShortLabel(value) { return getPlatformMeta(value)?.shortLabel || platformLabel(value); } function getPlatformChips() { return ["全平台", ...getRuntimePlatformValues().map((value) => getPlatformShortLabel(value))]; } function isWorkbenchPlatform(value) { return Boolean(getPlatformMeta(value)?.workbenchReady); } function getWorkbenchRoute(platform, key, ...args) { const routes = getPlatformMeta(platform)?.routes; if (!routes) return ""; const route = routes[key]; if (typeof route === "function") return route(...args); return route || ""; } function setCurrentPlatform(value) { const normalized = normalizePlatformValue(value, ""); appState.currentPlatform = normalized; if (normalized) { localStorage.setItem(STORAGE_KEY + ":currentPlatform", normalized); } else { localStorage.removeItem(STORAGE_KEY + ":currentPlatform"); } } function getAccountPlatform(account) { return normalizePlatformValue( account?.platform || account?.source_platform || account?.metadata?.platform || "", "douyin" ); } function getAccountHandle(account) { return String( account?.handle || account?.douyin_id || account?.xhs_id || account?.bilibili_uid || account?.kuaishou_id || account?.wechat_video_id || account?.uid || account?.username || "" ).trim(); } function getAccountProfileUrl(account) { return String(account?.profile_url || account?.source_url || account?.homepage_url || "").trim(); } function getAccountName(account) { return String(account?.nickname || getAccountHandle(account) || "未命名账号").trim(); } function getAccountSubtitle(account) { return getAccountHandle(account) || getAccountProfileUrl(account) || platformLabel(getAccountPlatform(account)); } function getPendingWorkbenchReason(platform) { const meta = getPlatformMeta(platform); return meta?.pendingText || `${platformLabel(platform)}工作台待接入`; } function getPreferredPlatform() { const selectedAccountPlatform = getAccountPlatform(getSelectedAccount()); if (selectedAccountPlatform && isWorkbenchPlatform(selectedAccountPlatform)) return selectedAccountPlatform; const current = normalizePlatformValue(appState.currentPlatform, ""); if (current && isWorkbenchPlatform(current)) return current; const sourcePlatform = normalizePlatformValue( safeArray(appState.contentSources).find((item) => isWorkbenchPlatform(item.platform))?.platform || "", "" ); if (sourcePlatform) return sourcePlatform; return "douyin"; } function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function brief(value, max = 88) { const text = String(value ?? "").trim(); if (!text) return "暂无"; return text.length > max ? text.slice(0, max).trimEnd() + "…" : text; } function formatNumber(value) { const num = Number(value || 0); if (!Number.isFinite(num)) return "-"; if (num >= 100000000) return (num / 100000000).toFixed(1).replace(/\.0$/, "") + "亿"; if (num >= 10000) return (num / 10000).toFixed(1).replace(/\.0$/, "") + "w"; if (num >= 1000) return num.toLocaleString("zh-CN"); return String(Math.round(num * 10) / 10); } function formatBytes(value) { const num = Number(value || 0); if (!Number.isFinite(num) || num <= 0) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB"]; let size = num; let idx = 0; while (size >= 1024 && idx < units.length - 1) { size /= 1024; idx += 1; } const fixed = size >= 10 || idx === 0 ? size.toFixed(0) : size.toFixed(1); return `${fixed}${units[idx]}`; } function formatDateTime(value) { if (!value) return "-"; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value); return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }).format(date); } function formatDate(value) { if (!value) return "-"; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value); return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit" }).format(date); } function daysSince(value) { if (!value) return "-"; const time = new Date(value).getTime(); if (!Number.isFinite(time)) return "-"; const diff = Date.now() - time; return Math.max(0, Math.floor(diff / 86400000)); } function initials(value) { const raw = String(value || "").trim(); if (!raw) return "SF"; return raw.slice(0, 2).toUpperCase(); } function statusTone(status) { const normalized = String(status || "").toLowerCase(); if (["completed", "ready", "approved", "ok"].includes(normalized)) return "green"; if (["failed", "error", "rejected"].includes(normalized)) return "red"; if (["worth_scaling", "good_reference"].includes(normalized)) return "green"; if (["needs_rework"].includes(normalized)) return "red"; if (["hold"].includes(normalized)) return "orange"; if (["running", "processing", "pending", "queued"].includes(normalized)) return "orange"; return "blue"; } function loadStoredSession() { try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; } } function persistSession(session) { appState.session = session; if (session) { localStorage.setItem(STORAGE_KEY, JSON.stringify(session)); } else { localStorage.removeItem(STORAGE_KEY); } } function setLastSeenAt(value) { const date = value instanceof Date ? value : new Date(value); const time = Number.isFinite(date.getTime()) ? date.getTime() : Date.now(); appState.lastSeenAt = time; localStorage.setItem(STORAGE_KEY + ":lastSeenAt", String(time)); } function markSeenNow() { setLastSeenAt(Date.now()); } function setBusy(next, message = "") { appState.busy = next; appState.message = message; renderAuthUi(); } function setScreen(id) { appState.screen = id; navButtons.forEach((button) => { const active = button.dataset.screenTarget === id; button.classList.toggle("is-active", active); }); screens.forEach((screen) => { screen.classList.toggle("is-active", screen.dataset.screen === id); }); window.location.hash = id; } function ensureAuthUi() { const topbarRight = document.querySelector(".topbar-right"); if (!topbarRight) return; if (!document.querySelector(".auth-inline")) { const inline = document.createElement("div"); inline.className = "auth-inline"; inline.innerHTML = ` `; topbarRight.insertBefore(inline, topbarRight.firstChild); } if (!document.querySelector(".auth-modal")) { const modal = document.createElement("div"); modal.className = "auth-modal-backdrop hidden"; modal.innerHTML = `

连接 StoryForge

先登录后端,再加载项目、对标、Agent 和生产数据。

`; document.body.appendChild(modal); } } function renderAuthUi() { ensureAuthUi(); const session = appState.session; const openButton = document.querySelector('[data-action="open-auth"]'); const logoutButton = document.querySelector('[data-action="logout-session"]'); const status = document.querySelector(".auth-status"); const message = document.querySelector('[data-role="auth-message"]'); if (openButton) openButton.textContent = session ? "切换连接" : "连接后端"; if (logoutButton) logoutButton.hidden = !session; if (status) { status.textContent = appState.busy ? appState.message || "正在加载..." : session ? `${session.account?.display_name || session.account?.username || "已连接"} · ${session.backendUrl}` : "未连接"; } if (message) { message.textContent = appState.busy ? appState.message : ""; } } function openAuthModal() { ensureAuthUi(); const modal = document.querySelector(".auth-modal-backdrop"); if (!modal) return; const session = appState.session; modal.classList.remove("hidden"); setAuthField("backendUrl", session?.backendUrl || DEFAULT_BACKEND_URL); setAuthField("username", session?.account?.username || ""); setAuthField("password", ""); setAuthField("token", session?.token || ""); } function closeAuthModal() { document.querySelector(".auth-modal-backdrop")?.classList.add("hidden"); } function setAuthField(name, value) { const input = document.querySelector(`[data-auth-field="${name}"]`); if (input) input.value = value ?? ""; } function readAuthForm() { const pick = (name) => document.querySelector(`[data-auth-field="${name}"]`)?.value?.trim() || ""; return { backendUrl: pick("backendUrl") || DEFAULT_BACKEND_URL, username: pick("username"), password: document.querySelector('[data-auth-field="password"]')?.value || "", token: pick("token") }; } let currentActionConfig = null; function ensureActionUi() { if (document.querySelector(".action-modal-backdrop")) return; const modal = document.createElement("div"); modal.className = "action-modal-backdrop hidden"; modal.innerHTML = `

快速操作

根据当前工作区执行动作。

`; document.body.appendChild(modal); } function renderActionFields(fields) { return fields.map((field) => { const common = `data-action-field="${escapeHtml(field.name)}"`; if (field.type === "html") { return `
${field.html || ""}
`; } if (field.type === "textarea") { return `
`; } if (field.type === "select") { return `
`; } if (field.type === "checkbox") { return ` `; } if (field.type === "file") { return `
`; } return `
`; }).join(""); } function openActionModal(config) { ensureActionUi(); currentActionConfig = config; const modal = document.querySelector(".action-modal-backdrop"); const title = document.querySelector('[data-role="action-title"]'); const description = document.querySelector('[data-role="action-description"]'); const fields = document.querySelector('[data-role="action-fields"]'); const message = document.querySelector('[data-role="action-message"]'); const submit = document.querySelector('[data-action="submit-sheet"]'); if (!modal || !title || !description || !fields || !message || !submit) return; title.textContent = config.title || "快速操作"; description.textContent = config.description || ""; fields.innerHTML = renderActionFields(config.fields || []); message.textContent = ""; submit.textContent = config.submitLabel || "执行"; submit.disabled = false; submit.hidden = Boolean(config.hideSubmit); modal.classList.remove("hidden"); } function closeActionModal() { currentActionConfig = null; document.querySelector(".action-modal-backdrop")?.classList.add("hidden"); } function ensureOneLinerUi() { if (!document.querySelector(".oneliner-fab")) { const fab = document.createElement("button"); fab.className = "oneliner-fab"; fab.type = "button"; fab.dataset.action = "open-oneliner"; fab.innerHTML = ` 1 OneLiner `; document.body.appendChild(fab); } if (!document.querySelector(".oneliner-backdrop")) { const panel = document.createElement("div"); panel.className = "oneliner-backdrop hidden"; panel.innerHTML = `

OneLiner

前端没上的需求,先由总控主 Agent 承接。

`; document.body.appendChild(panel); } } function renderOneLinerSessionTabs() { const sessions = safeArray(appState.onelinerSessions).slice(0, 6); if (!sessions.length) { return `
还没有会话发送第一条需求后自动创建
`; } const currentId = getCurrentOneLinerSession()?.id || ""; return `
${sessions.map((session) => ` ${escapeHtml(brief(session.title || "新会话", 14))} `).join("")}
`; } function renderOneLinerMessagesHtml() { const messages = safeArray(appState.onelinerMessages); if (!messages.length) { return `

还没有对话

你可以直接说目标,不用先理解平台有什么按钮。OneLiner 会先拆目标,再决定交给哪个平台 Agent。

`; } return messages.map((message) => { const roleClass = message.role === "assistant" ? "assistant" : "user"; const result = message.result || {}; const plan = message.plan || {}; const executionCard = result.execution_card || {}; const actions = safeArray(plan.suggested_actions); const secondaryActions = safeArray(executionCard.secondary_actions); return `
${escapeHtml(message.role === "assistant" ? "OneLiner" : "你")}

${escapeHtml(message.content || result.summary_text || "")}

${plan.intent_key ? `
${escapeHtml(onelinerIntentLabel(plan.intent_key))} ${plan.platform_label ? `${escapeHtml(plan.platform_label)}` : ""} ${plan.delivery_mode ? `${escapeHtml(plan.delivery_mode === "oneliner" ? "对话承接" : "可走前端")}` : ""}
` : ""} ${actions.length ? `
${actions.map((item) => `${escapeHtml(item.label)}`).join("")}
` : ""} ${message.role === "assistant" && (executionCard.intent_label || executionCard.platform_label || executionCard.primary_action?.label || safeArray(executionCard.evidence).length) ? `

${escapeHtml(executionCard.intent_label || "本轮执行建议")}

${escapeHtml(executionCard.blocked_reason || `${executionCard.platform_label || "待判断平台"} · ${executionCard.delivery_mode === "ui" ? "优先走前端固定动作" : "优先由 OneLiner 对话承接"}`)}

${executionCard.platform_label ? `${escapeHtml(executionCard.platform_label)}` : ""} ${executionCard.platform_agent_name ? `${escapeHtml(executionCard.platform_agent_name)}` : ""} ${executionCard.assistant_name ? `${escapeHtml(executionCard.assistant_name)}` : ""} ${executionCard.readiness_label ? `= 50 ? "blue" : "orange"}">${escapeHtml(executionCard.readiness_label)} ${escapeHtml(formatNumber(executionCard.readiness_score || 0))}` : ""} ${executionCard.primary_action?.key ? `${escapeHtml(executionCard.primary_action.label || "执行下一步")}` : ""}
${safeArray(executionCard.evidence).length ? `
${safeArray(executionCard.evidence).slice(0, 2).map((item) => `

${escapeHtml(item.kind === "skill" ? "技能证据" : "记忆证据")} · ${escapeHtml(item.title || "未命名")}

${escapeHtml(item.summary || "暂无摘要")}

`).join("")}
` : ""} ${safeArray(executionCard.next_steps).length ? `
${safeArray(executionCard.next_steps).slice(0, 3).map((item) => `${escapeHtml(item)}`).join("")}
` : ""} ${secondaryActions.length ? `
${secondaryActions.map((item) => actionTag( item.label || item.key || "执行", item.key || "", [ item.executor_key ? `data-executor-key="${escapeHtml(item.executor_key)}"` : "", item.platform ? `data-platform="${escapeHtml(item.platform)}"` : "", message.session_id ? `data-session-id="${escapeHtml(message.session_id)}"` : "", ...Object.entries(item.payload || {}).map(([payloadKey, payloadValue]) => { const attrKey = String(payloadKey || "") .replace(/([a-z0-9])([A-Z])/g, "$1-$2") .replace(/_/g, "-") .toLowerCase(); const serialized = typeof payloadValue === "string" ? payloadValue : JSON.stringify(payloadValue); return `data-${escapeHtml(attrKey)}="${escapeHtml(serialized)}"`; }) ].filter(Boolean).join(" ") , { disabledReason: item.disabled_reason || "" } )).join("")}
` : ""}
` : ""}
`; }).join(""); } function renderOneLinerUi() { ensureOneLinerUi(); const fab = document.querySelector(".oneliner-fab"); const meta = document.querySelector('[data-role="oneliner-meta"]'); const sessions = document.querySelector('[data-role="oneliner-sessions"]'); const messages = document.querySelector('[data-role="oneliner-messages"]'); const status = document.querySelector('[data-role="oneliner-status"]'); const input = document.querySelector('[data-role="oneliner-input"]'); const profile = appState.onelinerProfile; if (fab) { fab.hidden = !appState.session; } if (meta) { meta.innerHTML = `
${escapeHtml(profile?.display_name || "OneLiner")} ${escapeHtml(getSelectedProject()?.name || "未选项目")} ${escapeHtml(profile?.default_platform ? platformLabel(profile.default_platform) : "未设默认平台")} ${escapeHtml(formatNumber(safeArray(appState.platformAgents).length))} 个平台 Agent
${escapeHtml(profile?.long_term_goal || "当前没有设置长期目标。你可以先在这里说目标,后续再逐步产品化。")}
`; } if (sessions) sessions.innerHTML = renderOneLinerSessionTabs(); if (messages) { messages.innerHTML = renderOneLinerMessagesHtml(); messages.scrollTop = messages.scrollHeight; } if (status) { status.textContent = appState.busy ? appState.message || "处理中..." : ""; } if (input && !input.value && !safeArray(appState.onelinerMessages).length) { input.value = ""; } } function openOneLinerPanel() { ensureOneLinerUi(); document.querySelector(".oneliner-backdrop")?.classList.remove("hidden"); } function closeOneLinerPanel() { document.querySelector(".oneliner-backdrop")?.classList.add("hidden"); } function readActionForm() { const values = {}; document.querySelectorAll("[data-action-field]").forEach((element) => { const name = element.getAttribute("data-action-field"); if (!name) return; if (element instanceof HTMLInputElement && element.type === "checkbox") { values[name] = element.checked; return; } if (element instanceof HTMLInputElement && element.type === "file") { values[name] = element.files?.[0] || null; return; } values[name] = element.value; }); return values; } async function submitActionModal() { if (!currentActionConfig?.onSubmit) return; const message = document.querySelector('[data-role="action-message"]'); const submit = document.querySelector('[data-action="submit-sheet"]'); const values = readActionForm(); if (submit) submit.disabled = true; if (message) message.textContent = "正在执行..."; try { const result = await currentActionConfig.onSubmit(values); if (result?.keepOpen) { if (message) message.textContent = result.message || "已完成"; } else { closeActionModal(); } } catch (error) { if (message) message.textContent = error.message || "执行失败"; if (submit) submit.disabled = false; return; } if (submit) submit.disabled = false; } async function storyforgeFetch(path, options = {}) { const backendUrl = (options.backendUrl || appState.session?.backendUrl || DEFAULT_BACKEND_URL).replace(/\/$/, ""); const headers = { ...(options.headers || {}) }; const useAuth = options.auth !== false; const token = options.token || appState.session?.token; if (useAuth && token) headers.Authorization = `Bearer ${token}`; let body = options.body; if (body && !(body instanceof FormData) && !headers["Content-Type"] && !headers["content-type"]) { headers["Content-Type"] = "application/json"; body = JSON.stringify(body); } const response = await fetch(`${backendUrl}${path}`, { method: options.method || "GET", headers, body, cache: "no-store" }); const isJson = (response.headers.get("content-type") || "").includes("application/json"); const payload = isJson ? await response.json() : await response.text(); if (!response.ok) { const detail = typeof payload === "object" && payload ? payload.detail || payload.message || JSON.stringify(payload) : String(payload || response.statusText); throw new Error(detail); } return payload; } async function storyforgeFetchBlob(path, options = {}) { const backendUrl = (options.backendUrl || appState.session?.backendUrl || DEFAULT_BACKEND_URL).replace(/\/$/, ""); const headers = { ...(options.headers || {}) }; const useAuth = options.auth !== false; const token = options.token || appState.session?.token; if (useAuth && token) headers.Authorization = `Bearer ${token}`; const response = await fetch(`${backendUrl}${path}`, { method: options.method || "GET", headers, body: options.body, cache: "no-store" }); if (!response.ok) { const payload = (response.headers.get("content-type") || "").includes("application/json") ? await response.json().catch(() => null) : await response.text().catch(() => ""); const detail = typeof payload === "object" && payload ? payload.detail || payload.message || JSON.stringify(payload) : String(payload || response.statusText); throw new Error(detail); } return response.blob(); } async function loadBackendCapabilities(backendUrl) { const normalizedUrl = (backendUrl || DEFAULT_BACKEND_URL).replace(/\/$/, ""); const response = await fetch(`${normalizedUrl}/openapi.json`); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const payload = await response.json(); return new Set(Object.keys(payload.paths || {})); } function backendSupports(path) { if (!(appState.backendCapabilities instanceof Set)) return true; return appState.backendCapabilities.has(path); } async function loginWithForm() { const auth = readAuthForm(); if (!auth.backendUrl) { throw new Error("请先填写后端地址"); } if (auth.token) { const account = await storyforgeFetch("/v2/me", { backendUrl: auth.backendUrl, token: auth.token }); persistSession({ backendUrl: auth.backendUrl, token: auth.token, account }); return; } if (!auth.username || !auth.password) { throw new Error("请填写账号密码,或者直接填 Token"); } const payload = await storyforgeFetch("/v2/auth/login", { backendUrl: auth.backendUrl, auth: false, method: "POST", body: { username: auth.username, password: auth.password } }); persistSession({ backendUrl: auth.backendUrl, token: payload.token, account: payload.account }); } async function logoutSession() { try { if (appState.session) { await storyforgeFetch("/v2/auth/logout", { method: "POST" }); } } catch {} persistSession(null); appState.me = null; appState.dashboard = null; appState.contentSources = []; appState.accounts = []; appState.selectedAccountId = ""; appState.currentPlatform = ""; appState.selectedAssistantId = ""; appState.selectedWorkspace = null; appState.selectedVideos = { items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 }; appState.documents = []; appState.trackingAccounts = []; appState.trackingDigest = null; appState.reviews = []; appState.onelinerProfile = null; appState.onelinerSessions = []; appState.selectedOnelinerSessionId = ""; appState.onelinerMessages = []; appState.onelinerActionRegistry = []; appState.platformAgents = []; appState.tenantQuota = null; appState.tenantUsage = null; appState.adminOpsOverview = null; appState.integrationHealth = null; appState.storageStatus = null; appState.backendCapabilities = null; appState.lastAction = null; appState.lastGeneratedCopy = null; appState.lastSimilaritySearch = null; appState.lastJobDetail = null; localStorage.removeItem(STORAGE_KEY + ":currentPlatform"); renderAll(); } async function loadKnowledgeDocuments(knowledgeBases) { const targets = safeArray(knowledgeBases).slice(0, 3); if (!targets.length) return []; const groups = await Promise.all( targets.map((kb) => storyforgeFetch(`/v2/knowledge-bases/${encodeURIComponent(kb.id)}/documents`).catch(() => []) ) ); return groups.flat().slice(0, 12); } async function loadStorageStatus(projectId = "") { if (!backendSupports("/v2/storage/status")) { appState.storageStatus = null; return null; } const suffix = projectId ? `?project_id=${encodeURIComponent(projectId)}` : ""; const payload = await storyforgeFetch(`/v2/storage/status${suffix}`).catch(() => null); appState.storageStatus = payload; return payload; } async function loadAgentControlSurfaces(projectId = "") { const normalizedProjectId = projectId || getOneLinerProjectId(); const supportsOneLinerProfile = backendSupports("/v2/oneliner/profile"); const supportsOneLinerSessions = backendSupports("/v2/oneliner/sessions"); const supportsActionRegistry = backendSupports("/v2/oneliner/action-registry"); const supportsPlatformAgents = backendSupports("/v2/platform-agents"); const supportsAdminOps = backendSupports("/v2/admin/ops/overview"); const supportsTenantQuota = backendSupports("/v2/tenant/quota"); const supportsTenantUsage = backendSupports("/v2/tenant/usage"); const [profile, sessionsPayload, actionRegistryPayload, platformAgentsPayload, tenantQuota, tenantUsage, adminOpsOverview] = await Promise.all([ supportsOneLinerProfile ? storyforgeFetch(`/v2/oneliner/profile?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null) : Promise.resolve(null), supportsOneLinerSessions ? storyforgeFetch(`/v2/oneliner/sessions?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })) : Promise.resolve({ items: [] }), supportsActionRegistry ? storyforgeFetch(`/v2/oneliner/action-registry?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })) : Promise.resolve({ items: [] }), supportsPlatformAgents ? storyforgeFetch(`/v2/platform-agents?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })) : Promise.resolve({ items: [] }), supportsTenantQuota ? storyforgeFetch(`/v2/tenant/quota?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null) : Promise.resolve(null), supportsTenantUsage ? storyforgeFetch(`/v2/tenant/usage?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null) : Promise.resolve(null), supportsAdminOps && isSuperAdmin() ? storyforgeFetch("/v2/admin/ops/overview").catch(() => null) : Promise.resolve(null) ]); appState.onelinerProfile = profile; appState.onelinerSessions = safeArray(sessionsPayload?.items || sessionsPayload); appState.onelinerActionRegistry = safeArray(actionRegistryPayload?.items || actionRegistryPayload); if (!appState.selectedOnelinerSessionId || !safeArray(appState.onelinerSessions).some((item) => item.id === appState.selectedOnelinerSessionId)) { appState.selectedOnelinerSessionId = safeArray(appState.onelinerSessions)[0]?.id || ""; } appState.platformAgents = safeArray(platformAgentsPayload?.items || platformAgentsPayload); appState.tenantQuota = tenantQuota; appState.tenantUsage = tenantUsage; appState.adminOpsOverview = adminOpsOverview; } async function loadOneLinerMessages(sessionId) { if (!sessionId || !backendSupports("/v2/oneliner/sessions/{session_id}/messages")) { appState.onelinerMessages = []; return []; } const payload = await storyforgeFetch(`/v2/oneliner/sessions/${encodeURIComponent(sessionId)}/messages`).catch(() => ({ items: [] })); appState.onelinerMessages = safeArray(payload?.items || payload); return appState.onelinerMessages; } async function ensureOneLinerSession() { const projectId = getOneLinerProjectId(); if (!projectId) throw new Error("当前还没有项目,OneLiner 需要先绑定项目上下文。"); if (!backendSupports("/v2/oneliner/sessions")) { throw new Error("当前后端还没有接入 OneLiner 会话接口。"); } let session = getCurrentOneLinerSession(); if (!session) { session = await storyforgeFetch("/v2/oneliner/sessions", { method: "POST", body: { project_id: projectId, preferred_platform: getPreferredPlatform() } }); appState.onelinerSessions = [session, ...safeArray(appState.onelinerSessions)]; appState.selectedOnelinerSessionId = session.id; } await loadOneLinerMessages(session.id); return session; } async function submitOneLinerMessage(content) { const projectId = getOneLinerProjectId(); const session = await ensureOneLinerSession(); const payload = await storyforgeFetch(`/v2/oneliner/sessions/${encodeURIComponent(session.id)}/messages`, { method: "POST", body: { content, project_id: projectId, platform: getPreferredPlatform() } }); appState.selectedOnelinerSessionId = payload.session?.id || session.id; await loadAgentControlSurfaces(projectId); if (appState.selectedOnelinerSessionId) { await loadOneLinerMessages(appState.selectedOnelinerSessionId); } else { appState.onelinerMessages = [ ...safeArray(appState.onelinerMessages), payload.user_message, payload.assistant_message ].filter(Boolean); } appState.onelinerProfile = payload.result?.context?.oneliner_profile || appState.onelinerProfile || null; rememberAction("OneLiner 已响应", payload.result?.summary_text || "已返回一版任务拆解。", "blue", payload); return payload; } function renderOneLinerExecutionPayloadHtml(payload) { if (!payload || typeof payload !== "object") { return `

没有返回执行结果

当前执行器没有附带额外数据。

`; } if (payload.job) { const job = payload.job || {}; const sourceJob = payload.source_job || {}; return `

${escapeHtml(job.title || "任务已创建")}

${escapeHtml(payload.brief || sourceJob.title || `已创建 ${job.line_type || job.source_type || "任务"},你可以继续进入生产中心查看进度。`)}

${escapeHtml(job.line_type || job.source_type || "analysis")} ${escapeHtml(job.status || "queued")} ${job.id ? `看任务详情` : ""} 去生产中心
`; } if (payload.saved || payload.started) { const saved = payload.saved || {}; const item = saved.item || {}; const started = payload.started || {}; return `

${escapeHtml(item.binding_title || payload.source_url || "录制源已保存")}

${escapeHtml(item.source_url || payload.source_url || "直播源已经保存到当前租户的录制配置。")}

${escapeHtml(platformLabel(payload.platform || item.platform || "kuaishou"))} ${escapeHtml(item.quality || "原画")} ${escapeHtml(started && started.ok === false ? "启动待重试" : "已同步")} 打开录制控制
`; } if (payload.analyzed_count !== undefined && safeArray(payload.items).length) { return `
平台${escapeHtml(platformLabel(payload.platform || payload.account?.platform || "douyin"))}
账号${escapeHtml(payload.account?.title || payload.account?.handle || payload.account?.source_url || "当前账号")}
拆解作品${escapeHtml(formatNumber(payload.analyzed_count || 0))}
已写记忆${escapeHtml(payload.memory?.title ? "是" : "否")}
${safeArray(payload.items).slice(0, 4).map((item) => `

${escapeHtml(item.video_title || "高分作品")}

${escapeHtml(item.summary_text || "已完成拆解。")}

得分 ${escapeHtml(formatNumber(item.performance_score || 0))} ${item.latest_job_id ? `看原分析` : ""}
`).join("")}
`; } if (payload.route_checks) { return `
平台${escapeHtml(payload.platform_label || payload.platform || "-")}
得分${escapeHtml(formatNumber(payload.score || 0))}
状态${escapeHtml(payload.readiness_label || payload.verdict || "-")}
账号源${escapeHtml(formatNumber(payload.source_count || 0))}
路由检查
${safeArray(payload.route_checks).map((item) => `

${escapeHtml(item.key || item.path || "route")}

${escapeHtml(item.path || "")}

${escapeHtml(item.ok ? "可用" : "缺失")}
`).join("")}
建议动作
${(safeArray(payload.suggestions).length ? safeArray(payload.suggestions) : ["当前已经达到可运行状态。"]).map((item) => `

下一步

${escapeHtml(item)}

`).join("")}
`; } if (payload.strategy && payload.tenant_usage) { return `
jobs${escapeHtml(payload.tenant_usage?.project_jobs?.human_size || "0B")}
downloads${escapeHtml(payload.tenant_usage?.project_downloads?.human_size || "0B")}
模型目录${escapeHtml(payload.strategy?.models?.mode || "-")}
录像${escapeHtml(payload.strategy?.live_recorder?.mode || "-")}
`; } if (payload.items || payload.files) { return `
录制源${escapeHtml(formatNumber(safeArray(payload.items).length))}
最近文件${escapeHtml(formatNumber(safeArray(payload.files).length))}
`; } if (payload.content) { return `

生成文案

${escapeHtml(brief(payload.content, 1200))}

${payload.assistant_id ? `${escapeHtml(payload.assistant_id)}` : ""} ${escapeHtml(formatNumber(safeArray(payload.used_documents).length))} 个参考素材
`; } if (payload.verdict !== undefined || payload.next_actions !== undefined || payload.highlights !== undefined) { return `

${escapeHtml(payload.title || "复盘草稿")}

${escapeHtml(payload.highlights || payload.notes || "已生成一版待补充的复盘草稿。")}

${escapeHtml(platformLabel(payload.platform || "douyin"))} ${escapeHtml(payload.verdict || "待补充")} ${payload.source_job_id ? `看任务详情` : ""} ${payload.id ? `打开复盘` : ""}
`; } return `

原始结果

${escapeHtml(brief(JSON.stringify(payload, null, 2), 1200))}

`; } function parseOneLinerActionPayloadValue(value) { const text = String(value ?? "").trim(); if (!text) return ""; if (text === "true") return true; if (text === "false") return false; if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text); if ((text.startsWith("{") && text.endsWith("}")) || (text.startsWith("[") && text.endsWith("]"))) { try { return JSON.parse(text); } catch (error) { return text; } } return text; } function collectOneLinerActionPayload(action) { const reserved = new Set(["action", "executorKey", "platform", "sessionId", "disabledReason"]); const payload = {}; Object.entries(action?.dataset || {}).forEach(([key, value]) => { if (reserved.has(key)) return; if (value === undefined || value === null || value === "") return; payload[key] = parseOneLinerActionPayloadValue(value); }); return payload; } async function executeOneLinerAction(executorKey, options = {}) { if (!backendSupports("/v2/oneliner/actions/execute")) { throw new Error("当前后端还没有接入 OneLiner 动作执行器。"); } const projectId = getOneLinerProjectId(); const session = getCurrentOneLinerSession() || await ensureOneLinerSession(); const payload = await storyforgeFetch("/v2/oneliner/actions/execute", { method: "POST", body: { action_key: executorKey, project_id: projectId, platform: options.platform || getPreferredPlatform(), session_id: options.sessionId || session?.id || "", payload: options.payload || {} } }); await loadAgentControlSurfaces(projectId); if (appState.selectedOnelinerSessionId) { await loadOneLinerMessages(appState.selectedOnelinerSessionId); } openActionModal({ title: payload.title || "OneLiner 执行结果", description: payload.summary || "已完成一次对话内执行。", hideSubmit: true, fields: [ { type: "html", label: "执行结果", html: `
${renderOneLinerExecutionPayloadHtml(payload.payload || {})}
` } ] }); rememberAction("OneLiner 已执行", payload.summary || "当前动作已在对话内执行。", "green", payload); renderAll(); return payload; } async function loadPlatformAccount(platform, accountId) { if (!accountId) return; const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform()); appState.selectedAccountId = accountId; setCurrentPlatform(normalizedPlatform); const workspacePath = getWorkbenchRoute(normalizedPlatform, "workspace", accountId); if (!workspacePath) { appState.selectedWorkspace = null; appState.selectedVideos = { items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 }; return; } const videosPath = getWorkbenchRoute(normalizedPlatform, "videos", accountId); const supportsAccountVideos = videosPath && backendSupports(`/v2/${normalizedPlatform}/accounts/{account_id}/videos`); const [workspace, videos] = await Promise.all([ storyforgeFetch(workspacePath), supportsAccountVideos ? storyforgeFetch(videosPath).catch(() => ({ items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 })) : Promise.resolve({ items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 }) ]); appState.selectedWorkspace = workspace; appState.selectedVideos = videos; } function getTrackingSinceIso() { const date = new Date(appState.lastSeenAt || Date.now()); if (Number.isNaN(date.getTime())) return new Date(Date.now() - 86400000).toISOString(); return date.toISOString(); } async function bootstrap() { renderAll(); if (!appState.session) { renderAuthUi(); return; } setBusy(true, "正在同步工作区..."); try { appState.me = await storyforgeFetch("/v2/me"); if (appState.me.approval_status !== "approved" && appState.me.role !== "super_admin") { appState.dashboard = null; appState.accounts = []; appState.contentSources = []; appState.documents = []; renderAll(); return; } appState.backendCapabilities = await loadBackendCapabilities(appState.session.backendUrl).catch(() => null); const preferredPlatform = getPreferredPlatform(); setCurrentPlatform(preferredPlatform); const accountListPath = getWorkbenchRoute(preferredPlatform, "accounts"); const trackingAccountsPath = getWorkbenchRoute(preferredPlatform, "trackingAccounts"); const trackingDigestPath = getWorkbenchRoute(preferredPlatform, "trackingDigest"); const supportsTrackingDigest = trackingDigestPath && backendSupports(trackingDigestPath); const supportsReviews = backendSupports("/v2/reviews"); const supportsIntegrationHealth = backendSupports("/v2/integrations/health"); const supportsLocalModels = backendSupports("/v2/integrations/local-models"); const supportsStorageStatus = backendSupports("/v2/storage/status"); const supportsLiveRecorderSources = backendSupports("/v2/live-recorder/sources"); const supportsLiveRecorderStatus = backendSupports("/v2/live-recorder/status"); const supportsLiveRecorderFiles = backendSupports("/v2/live-recorder/files"); const [dashboard, contentSources, accounts, trackingAccountsPayload, reviews, integrationHealth, localModelCatalog, liveRecorderSourcesPayload, liveRecorderStatus, liveRecorderFilesPayload] = await Promise.all([ storyforgeFetch("/v2/me/dashboard"), storyforgeFetch("/v2/content-sources").catch(() => []), accountListPath ? storyforgeFetch(accountListPath).catch(() => []) : Promise.resolve([]), trackingAccountsPath ? storyforgeFetch(trackingAccountsPath).catch(() => ({ items: [], cursor_last_seen_at: "" })) : Promise.resolve({ items: [], cursor_last_seen_at: "" }), supportsReviews ? storyforgeFetch("/v2/reviews").catch(() => []) : Promise.resolve([]), supportsIntegrationHealth ? storyforgeFetch("/v2/integrations/health").catch(() => null) : Promise.resolve(null), supportsLocalModels ? storyforgeFetch("/v2/integrations/local-models").catch(() => null) : Promise.resolve(null), supportsLiveRecorderSources ? storyforgeFetch("/v2/live-recorder/sources").catch(() => ({ items: [] })) : Promise.resolve({ items: [] }), supportsLiveRecorderStatus ? storyforgeFetch("/v2/live-recorder/status").catch(() => null) : Promise.resolve(null), supportsLiveRecorderFiles ? storyforgeFetch("/v2/live-recorder/files?limit=16").catch(() => ({ items: [] })) : Promise.resolve({ items: [] }) ]); const trackingCursorLastSeenAt = trackingAccountsPayload?.cursor_last_seen_at || ""; if (trackingCursorLastSeenAt) { setLastSeenAt(trackingCursorLastSeenAt); } const trackingSince = trackingCursorLastSeenAt || getTrackingSinceIso(); const trackingDigest = trackingDigestPath ? await storyforgeFetch(`${trackingDigestPath}?since=${encodeURIComponent(trackingSince)}&limit=24`).catch(() => ({ items: [], tracked_accounts: [], cursor_last_seen_at: trackingCursorLastSeenAt })) : ({ items: [], tracked_accounts: [], cursor_last_seen_at: trackingCursorLastSeenAt }); appState.dashboard = dashboard; appState.contentSources = safeArray(contentSources); appState.accounts = safeArray(accounts); appState.trackingAccounts = safeArray(trackingAccountsPayload.items || trackingAccountsPayload); appState.trackingDigest = trackingDigest; appState.reviews = safeArray(reviews); appState.liveRecorderSources = safeArray(liveRecorderSourcesPayload?.items || liveRecorderSourcesPayload); appState.liveRecorderStatus = liveRecorderStatus; appState.liveRecorderFiles = safeArray(liveRecorderFilesPayload?.items || liveRecorderFilesPayload); appState.integrationHealth = integrationHealth; appState.localModelCatalog = localModelCatalog; appState.documents = await loadKnowledgeDocuments(dashboard.knowledge_bases); appState.selectedProjectId = appState.selectedProjectId || dashboard.projects?.[0]?.id || ""; if (supportsStorageStatus) { await loadStorageStatus(appState.selectedProjectId || ""); } else { appState.storageStatus = null; } await loadAgentControlSurfaces(appState.selectedProjectId || ""); if (appState.selectedOnelinerSessionId) { await loadOneLinerMessages(appState.selectedOnelinerSessionId); } else { appState.onelinerMessages = []; } const selectedAssistantExists = safeArray(dashboard.assistants).some((item) => item.id === appState.selectedAssistantId); appState.selectedAssistantId = selectedAssistantExists ? appState.selectedAssistantId : (dashboard.assistants?.[0]?.id || ""); const selectedAccountExists = appState.accounts.some((item) => item.id === appState.selectedAccountId); const nextAccountId = selectedAccountExists ? appState.selectedAccountId : appState.accounts[0]?.id || ""; if (nextAccountId) { const nextAccount = appState.accounts.find((item) => item.id === nextAccountId) || null; await loadPlatformAccount(getAccountPlatform(nextAccount), nextAccountId); } else { appState.selectedAccountId = ""; appState.selectedWorkspace = null; appState.selectedVideos = { items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 }; } } catch (error) { appState.message = error.message; if (String(error.message || "").includes("401") || String(error.message || "").includes("Not authenticated")) { persistSession(null); } } finally { setBusy(false, ""); renderAll(); } } async function markTrackingDigestRead() { const platform = getPreferredPlatform(); const trackingCursorPath = getWorkbenchRoute(platform, "trackingCursor"); if (!trackingCursorPath || !backendSupports(trackingCursorPath)) { rememberAction("当前后端暂不支持", "这套 live collector 还没有接入跟踪已读游标。", "orange"); renderAll(); return; } const nextSeenAt = new Date().toISOString(); await storyforgeFetch(trackingCursorPath, { method: "POST", body: { last_seen_at: nextSeenAt } }); setLastSeenAt(nextSeenAt); } async function refreshTrackingAccountsAction() { const platform = getPreferredPlatform(); const trackingRefreshPath = getWorkbenchRoute(platform, "trackingRefresh"); if (!trackingRefreshPath || !backendSupports(trackingRefreshPath)) { rememberAction("当前后端暂不支持", "这套 live collector 还没有接入批量跟踪同步。", "orange"); renderAll(); return; } setBusy(true, "正在同步跟踪账号..."); try { const payload = await storyforgeFetch(trackingRefreshPath, { method: "POST" }); rememberAction( "跟踪已同步", `已刷新 ${formatNumber(payload.refreshed || 0)} 个账号${payload.failed ? `,失败 ${formatNumber(payload.failed)} 个` : ""}。`, payload.failed ? "orange" : "green", payload ); await bootstrap(); } finally { setBusy(false, ""); } } async function refreshTrackedAccountAction(trackedAccountId) { if (!trackedAccountId) { throw new Error("trackedAccountId is required"); } const platform = getPreferredPlatform(); const trackingRefreshPath = getWorkbenchRoute(platform, "trackingAccountRefresh", trackedAccountId); if (!trackingRefreshPath || !backendSupports(`/v2/${platform}/tracking/accounts/{tracked_account_id}/refresh`)) { rememberAction("当前后端暂不支持", "这套 live collector 还没有接入单账号跟踪同步。", "orange"); renderAll(); return; } setBusy(true, "正在同步该跟踪账号..."); try { const payload = await storyforgeFetch(trackingRefreshPath, { method: "POST" }); const success = payload.success !== false; rememberAction( success ? "单账号已同步" : "单账号刷新失败", success ? `已刷新「${payload.account?.nickname || trackedAccountId}」的最新作品。` : `暂时无法刷新「${payload.account?.nickname || trackedAccountId}」:${payload.message || "请稍后重试"}`, success ? (safeArray(payload.sync_errors).length ? "orange" : "green") : "orange", payload ); await bootstrap(); } finally { setBusy(false, ""); } } function getSelectedProject() { const projects = safeArray(appState.dashboard?.projects); return projects.find((item) => item.id === appState.selectedProjectId) || projects[0] || null; } function isSuperAdmin() { return appState.me?.role === "super_admin"; } function getOneLinerProjectId() { return getSelectedProject()?.id || appState.selectedProjectId || safeArray(appState.dashboard?.projects)[0]?.id || ""; } function getCurrentOneLinerSession() { const sessions = safeArray(appState.onelinerSessions); return sessions.find((item) => item.id === appState.selectedOnelinerSessionId) || sessions[0] || null; } function onelinerIntentLabel(value) { return ONELINER_INTENT_LABELS[value] || value || "自定义任务"; } function getProjectKnowledgeBases(projectId) { return safeArray(appState.dashboard?.knowledge_bases).filter((item) => item.project_id === projectId); } function getProjectAssistants(projectId) { return safeArray(appState.dashboard?.assistants).filter((item) => item.project_id === projectId); } function getSelectedAssistant() { const assistants = safeArray(appState.dashboard?.assistants); return assistants.find((item) => item.id === appState.selectedAssistantId) || assistants[0] || null; } function getProjectOptions() { return safeArray(appState.dashboard?.projects).map((project) => ({ value: project.id, label: project.name })); } function getAssistantOptions(projectId) { return getProjectAssistants(projectId).map((assistant) => ({ value: assistant.id, label: assistant.name })); } function getKnowledgeBaseOptions(projectId) { return getProjectKnowledgeBases(projectId).map((kb) => ({ value: kb.id, label: kb.name })); } function getModelOptions() { return safeArray(appState.dashboard?.model_profiles).map((model) => ({ value: model.id, label: model.name })); } function getCurrentModelProfile() { const models = safeArray(appState.dashboard?.model_profiles); const currentId = appState.me?.preferred_analysis_model_id || models.find((item) => item.is_default)?.id || ""; return models.find((item) => item.id === currentId) || models.find((item) => item.is_default) || models[0] || null; } function getCompletedJobOptions() { return safeArray(appState.dashboard?.recent_jobs) .filter((item) => item.status === "completed") .map((job) => ({ value: job.id, label: `${job.title} · ${job.line_type || "analysis"}` })); } function getProjectStats(projectId) { const dashboard = appState.dashboard || {}; const knowledgeBases = safeArray(dashboard.knowledge_bases).filter((item) => item.project_id === projectId); const assistants = safeArray(dashboard.assistants).filter((item) => item.project_id === projectId); const jobs = safeArray(dashboard.recent_jobs).filter((item) => item.project_id === projectId); const sources = safeArray(appState.contentSources).filter((item) => item.project_id === projectId); return { knowledgeBases, assistants, jobs, sources }; } function getProjectReviews(projectId) { return safeArray(appState.reviews).filter((item) => item.project_id === projectId); } function getReviewById(reviewId) { return safeArray(appState.reviews).find((item) => item.id === reviewId) || null; } function getContentSourcesForAccount(account) { if (!account) return []; const platform = getAccountPlatform(account); const profileUrl = getAccountProfileUrl(account); const handle = getAccountHandle(account); const nickname = getAccountName(account); return safeArray(appState.contentSources).filter((source) => { const sourceUrl = String(source.source_url || "").trim(); const sourceHandle = String(source.handle || "").trim(); const title = String(source.title || "").trim(); const sourcePlatform = normalizePlatformValue(source.platform || "", platform); return ( sourcePlatform === platform && ( (profileUrl && sourceUrl === profileUrl) || (handle && sourceHandle === handle) || (nickname && title.includes(nickname)) ) ); }); } function getCurrentProjectSourcesForAccount(account, projectId) { return getContentSourcesForAccount(account).filter((source) => source.project_id === projectId); } function isTrackedAccount(accountId) { return safeArray(appState.trackingAccounts).some((item) => item.tracked_account_id === accountId); } function getTrackingDigestItems(limit = 6) { return safeArray(appState.trackingDigest?.items).slice(0, limit); } function getSelectedAccount() { return appState.selectedWorkspace?.account || appState.accounts.find((item) => item.id === appState.selectedAccountId) || null; } function getHighScoreVideos(limit = 3) { const items = safeArray(appState.selectedVideos?.items); const fallback = safeArray(getSelectedAccount()?.video_summary?.videos); const pool = items.length ? items : fallback; return pool .slice() .sort((a, b) => Number(b.score?.performance_score || 0) - Number(a.score?.performance_score || 0)) .slice(0, limit); } function getLatestVideos(limit = 3) { const items = safeArray(appState.selectedVideos?.items); const fallback = safeArray(getSelectedAccount()?.video_summary?.videos); const pool = items.length ? items : fallback; return pool .slice() .sort((a, b) => new Date(b.published_at || 0).getTime() - new Date(a.published_at || 0).getTime()) .slice(0, limit); } function getProductionWorks(limit = 6) { const preferred = safeArray(appState.selectedVideos?.items); const fallback = safeArray(appState.accounts) .flatMap((account) => safeArray(account.video_summary?.videos)) .filter(Boolean); const pool = preferred.length ? preferred : fallback; const scored = pool .slice() .sort((a, b) => Number(b.score?.performance_score || 0) - Number(a.score?.performance_score || 0)) .slice(0, Math.ceil(limit / 2)); const latest = pool .slice() .sort((a, b) => new Date(b.published_at || 0).getTime() - new Date(a.published_at || 0).getTime()) .slice(0, Math.ceil(limit / 2) + 1); const deduped = []; const seen = new Set(); [...scored, ...latest].forEach((item) => { const key = item.aweme_id || item.share_url || item.title || item.description; if (!key || seen.has(key)) return; seen.add(key); deduped.push(item); }); return deduped.slice(0, limit); } function describeVideo(video) { return video.title || video.description || video.aweme_id || "未命名作品"; } function getVideoLink(video) { return video.share_url || video.play_url || ""; } async function loadJobDetail(jobId) { const [job, events, childJobs] = await Promise.all([ storyforgeFetch(`/v2/explore/jobs/${encodeURIComponent(jobId)}`), storyforgeFetch(`/v2/explore/jobs/${encodeURIComponent(jobId)}/events`).catch(() => []), storyforgeFetch(`/v2/explore/jobs?parent_job_id=${encodeURIComponent(jobId)}`).catch(() => []) ]); appState.lastJobDetail = { job, events: safeArray(events), childJobs: safeArray(childJobs) }; return appState.lastJobDetail; } function isJobCompleted(job) { return String(job?.status || "").toLowerCase() === "completed"; } function canDeriveAiVideo(job) { if (!job || !isJobCompleted(job)) return false; return String(job.line_type || "").toLowerCase() !== "ai_video"; } function canDeriveRealCut(job) { if (!job || !isJobCompleted(job)) return false; const sourceType = String(job.source_type || "").toLowerCase(); return ["video_link", "upload_video"].includes(sourceType); } function hasIntegrationHealthData() { return Boolean(appState.integrationHealth && typeof appState.integrationHealth === "object"); } function getIntegrationDetail(key) { const raw = hasIntegrationHealthData() ? appState.integrationHealth?.[key] : null; return { key, available: Boolean(raw && typeof raw === "object"), configured: Boolean(raw?.configured), reachable: Boolean(raw?.reachable), statusCode: Number(raw?.status_code || 0), error: String(raw?.error || ""), url: String(raw?.url || raw?.base_url || ""), baseUrl: String(raw?.base_url || ""), supportsUploads: raw?.supports_uploads !== undefined ? Boolean(raw?.supports_uploads) : true, uploadStatusCode: Number(raw?.upload_status_code || 0), uploadError: String(raw?.upload_error || ""), uploadUrl: String(raw?.upload_url || "") }; } function getIntegrationStatus(detail) { if (!detail.available) { return { tone: "blue", summary: "未拉取" }; } if (detail.key === "cutvideo" && detail.reachable && !detail.supportsUploads) { return { tone: "orange", summary: "缺上传能力" }; } if (detail.reachable) { return { tone: "green", summary: "在线" }; } if (detail.configured) { return { tone: "red", summary: "不可达" }; } return { tone: "orange", summary: "未配置" }; } function describeIntegrationFailure(key) { const detail = getIntegrationDetail(key); const meta = INTEGRATION_META[key] || { label: key }; if (!detail.available) return `${meta.label}健康状态未拉取`; if (key === "cutvideo" && detail.reachable && !detail.supportsUploads) { return `${meta.label}缺少 /api/uploads`; } if (!detail.configured) return `${meta.label}未配置`; if (detail.statusCode) return `${meta.label}返回 HTTP ${detail.statusCode}`; if (detail.error) return `${meta.label}${brief(detail.error, 42)}`; return `${meta.label}不可达`; } function getPipelineGuard(kind) { const config = PIPELINE_GUARDS[kind]; if (!config) { return { enabled: true, reason: "", blocked: [] }; } const blocked = config.dependencies .map((key) => ({ key, detail: getIntegrationDetail(key), meta: INTEGRATION_META[key] || { label: key } })) .filter((item) => { if (!item.detail.available) return false; if (!item.detail.reachable) return true; if (item.key === "cutvideo" && !item.detail.supportsUploads) return true; return false; }); if (!blocked.length) { return { enabled: true, reason: "", blocked: [] }; } return { enabled: false, blocked, reason: `${config.label}暂不可用:${blocked.map((item) => describeIntegrationFailure(item.key)).join(";")}` }; } function getIntegrationCards() { const currentModel = getCurrentModelProfile(); const localCatalog = appState.localModelCatalog || {}; return INTEGRATION_ORDER.map((key) => { const detail = getIntegrationDetail(key); const status = getIntegrationStatus(detail); const meta = INTEGRATION_META[key] || { label: key, hint: key, impacts: [] }; let note = "尚未获取健康检查数据"; if (detail.available) { if (detail.reachable) { if (key === "cutvideo" && !detail.supportsUploads) { note = detail.uploadStatusCode ? `主服务在线,但 /api/uploads 返回 HTTP ${detail.uploadStatusCode}` : (detail.uploadError ? brief(detail.uploadError, 72) : "主服务在线,但缺少上传接口"); } else { note = detail.statusCode ? `健康探测返回 HTTP ${detail.statusCode}` : "TCP 探测已通过"; } } else if (!detail.configured) { note = "后端还没有配置该依赖地址"; } else if (detail.statusCode) { note = `探测返回 HTTP ${detail.statusCode}`; } else if (detail.error) { note = brief(detail.error, 72); } else { note = "探测失败,请检查服务进程和网络"; } } let extra = ""; let actions = ""; if (key === "local_model") { const availableModels = safeArray(localCatalog.models).map((item) => item.id).filter(Boolean); extra = currentModel ? `当前主模型:${currentModel.name} · ${currentModel.model_name || "-"}` : `默认模型:${localCatalog.default_model || "GLM-5"}`; if (availableModels.length) { extra += ` · 可用:${availableModels.slice(0, 4).join(" / ")}${availableModels.length > 4 ? "…" : ""}`; } actions = [ localCatalog.management_url ? `打开管理页` : "", `设主模型` ].filter(Boolean).join(""); } if (key === "live_recorder") { const ownedSources = safeArray(appState.liveRecorderSources); const ownedFiles = safeArray(appState.liveRecorderFiles); const activeCount = Number(appState.liveRecorderStatus?.recording_count || 0); extra = ownedSources.length ? `我的录制源 ${ownedSources.length} · 录像 ${ownedFiles.length} · 正在录制 ${activeCount}` : "当前还没有你的录制源"; actions = `录制控制`; } return { key, meta, detail, status, note, extra, actions }; }); } function renderLiveRecorderSummaryHtml() { const sources = safeArray(appState.liveRecorderSources); const files = safeArray(appState.liveRecorderFiles); const status = appState.liveRecorderStatus || {}; const activeItems = safeArray(status.active_recordings); const sourceHtml = sources.slice(0, 4).map((item) => `

${escapeHtml(item.title || item.remote_name || item.source_url || "录制源")}

${escapeHtml(platformLabel(item.platform))} · ${escapeHtml(item.quality || "原画")} · ${escapeHtml(item.enabled ? "启用中" : "已停用")}

`).join(""); const fileHtml = files.slice(0, 4).map((item) => `

${escapeHtml(item.title || item.name || "录像文件")}

${escapeHtml(item.mtime || "-")} · ${escapeHtml(item.name || item.relative_path || "-")}

打开录像
`).join(""); return `

租户隔离状态

当前只展示你自己名下的录制源、活动录制和录像文件。全局 NAS 配置不会直接暴露给前端。

${escapeHtml(`录制源 ${sources.length}`)} ${escapeHtml(`活动 ${activeItems.length}`)} ${escapeHtml(`文件 ${files.length}`)}
${sourceHtml || `

还没有录制源

新增直播源后会自动挂到你的租户空间下。

`} ${fileHtml || `

还没有录像文件

录制完成后的文件会只出现在你的当前租户视图里。

`} `; } function getStorageItemPath(item) { return ( item?.artifacts?.source_path || item?.artifacts?.uploaded_path || item?.artifacts?.output_path || item?.artifacts?.file_path || item?.result?.source_path || item?.result?.output_path || item?.result?.file_path || item?.result?.path || item?.save_path || item?.path || item?.relative_path || item?.content_url || item?.job_id || item?.id || "-" ); } function renderStorageJobCards(items, emptyTitle, emptyText) { return safeArray(items).slice(0, 4).map((item) => `

${escapeHtml(item.title || item.name || item.job_id || "任务")}

${escapeHtml(brief(getStorageItemPath(item), 140))}

${escapeHtml(item.status || "-")} ${item.project_name ? `${escapeHtml(item.project_name)}` : ""} ${item.line_type || item.source_type ? `${escapeHtml(item.line_type || item.source_type)}` : ""} ${item.id ? `看详情` : ""}
`).join("") || `

${escapeHtml(emptyTitle)}

${escapeHtml(emptyText)}

`; } function renderStorageFileCards(items, emptyTitle, emptyText) { return safeArray(items).slice(0, 4).map((item) => `

${escapeHtml(item.title || item.name || item.relative_path || "文件")}

${escapeHtml(brief(item.relative_path || item.name || item.content_url || "-", 140))}

${(item.updated_at || item.mtime) ? `${escapeHtml(formatDateTime(item.updated_at || item.mtime))}` : ""} ${(item.size_bytes || item.size) ? `${escapeHtml(formatBytes(item.size_bytes || item.size))}` : ""} ${item.id ? `打开文件` : ""}
`).join("") || `

${escapeHtml(emptyTitle)}

${escapeHtml(emptyText)}

`; } function renderStorageStatusPanel() { const storage = appState.storageStatus; const dashboardJobs = safeArray(appState.dashboard?.recent_jobs); const currentProject = getSelectedProject(); const currentAccount = getSelectedAccount(); const liveRecorderSources = safeArray(appState.liveRecorderSources); const liveRecorderFiles = safeArray(appState.liveRecorderFiles); const fallbackRecentJobs = dashboardJobs.slice(0, 4); const recentJobs = safeArray(storage?.tenant_usage?.recent_jobs).length ? safeArray(storage?.tenant_usage?.recent_jobs) : fallbackRecentJobs; const recentFiles = [ ...safeArray(storage?.tenant_usage?.recent_download_artifacts), ...safeArray(storage?.tenant_usage?.recent_job_artifacts), ...safeArray(storage?.recent_files || storage?.recent_artifacts || storage?.tenant_usage?.recent_files || storage?.tenant_usage?.recent_artifacts) ]; if (!storage) { const projectJobCount = appState.selectedProjectId ? dashboardJobs.filter((item) => item.project_id === appState.selectedProjectId).length : dashboardJobs.length; const accountJobCount = appState.selectedAccountId ? dashboardJobs.filter((item) => item.account_id === appState.selectedAccountId).length : dashboardJobs.length; return `

存储状态

后端暂未提供 /v2/storage/status,先用任务和录像文件做本地观察
降级视图

未拉取到 NAS 策略

后端补上 storage/status 后,这里会自动显示账号 / 项目 / 任务分层、容量和最近写入路径。

最近任务 ${escapeHtml(formatNumber(projectJobCount))} 录制源 ${escapeHtml(formatNumber(liveRecorderSources.length))} 录像文件 ${escapeHtml(formatNumber(liveRecorderFiles.length))}
当前项目 ${escapeHtml(currentProject?.name || appState.selectedProjectId || "未选择")} 任务 ${escapeHtml(formatNumber(projectJobCount))}
当前账号 ${escapeHtml(currentAccount ? getAccountName(currentAccount) : "未选择")} 任务 ${escapeHtml(formatNumber(accountJobCount))}
录像源 ${escapeHtml(formatNumber(liveRecorderSources.length))} 仅当前租户可见
最近文件 ${escapeHtml(formatNumber(liveRecorderFiles.length))} 可直接打开

最近任务

优先展示 dashboard.recent_jobs,方便在没有 storage/status 时也能继续追踪产物。

${renderStorageJobCards( fallbackRecentJobs, "还没有任务样本", "等你完成一次分析、下载或剪辑后,这里就会出现最近的任务路径和详情入口。" )}

最近录像文件

录像文件沿用 live-recorder 的当前租户视图,支持直接打开查看。

${renderStorageFileCards( liveRecorderFiles, "还没有录像文件", "录制完成后,这里会直接暴露当前租户的最近文件入口。" )}
`; } const strategy = storage.strategy || {}; const disk = storage.disk || {}; const usage = storage.tenant_usage || {}; const strategyMode = (strategy.jobs?.mode || "local").toUpperCase(); const projectName = currentProject?.name || appState.selectedProjectId || "未选择"; const accountName = currentAccount ? getAccountName(currentAccount) : "未选择"; const strategyTags = [ `数据库 ${strategy.database?.mode || "local"}`, `分析缓存 ${strategy.jobs?.mode || "local"}`, `下载缓存 ${strategy.downloads?.mode || "local"}`, `直播录制 ${strategy.live_recorder?.mode || "nas_service"}`, ]; const usageCards = [ { label: "当前项目缓存", value: formatBytes(usage.project_jobs?.bytes), sub: `文件 ${formatNumber(usage.project_jobs?.file_count)}` }, { label: "当前项目下载", value: formatBytes(usage.project_downloads?.bytes), sub: `文件 ${formatNumber(usage.project_downloads?.file_count)}` }, { label: "当前账号缓存", value: formatBytes(usage.account_jobs?.bytes), sub: `文件 ${formatNumber(usage.account_jobs?.file_count)}` }, { label: "当前账号下载", value: formatBytes(usage.account_downloads?.bytes), sub: `文件 ${formatNumber(usage.account_downloads?.file_count)}` }, { label: "NAS 剩余", value: formatBytes(disk.jobs?.free_bytes), sub: `总量 ${formatBytes(disk.jobs?.total_bytes)}` } ]; return `

存储状态

数据库留本机,大文件缓存优先走 NAS
${escapeHtml(strategyMode)}

当前观察范围

${escapeHtml(`项目 ${projectName} · 账号 ${accountName} · 最近任务 ${recentJobs.length} 条 · 最近录像 ${liveRecorderFiles.length} 个`)}

${strategyTags.map((item) => `${escapeHtml(item)}`).join("")}
${usageCards.map((item) => `
${escapeHtml(item.label)} ${escapeHtml(item.value)} ${escapeHtml(item.sub)}
`).join("")}

目录策略

${escapeHtml([ `数据库 ${strategy.database?.path || "本机"}`, `任务缓存 ${usage.project_jobs?.path || strategy.jobs?.path || "-"}`, `下载缓存 ${usage.project_downloads?.path || strategy.downloads?.path || "-"}`, `录制缓存 ${strategy.live_recorder?.path || strategy.live_recorder?.base_path || "-"}` ].join(" · "))}

项目层 ${escapeHtml(usage.project_jobs?.path || strategy.jobs?.path || "-")} 账号层 ${escapeHtml(usage.account_jobs?.path || "-")}

产物入口

最近任务、分析产物和录像文件都能直接点开,便于从 NAS 面板跳回详情或原文件。

任务 ${escapeHtml(formatNumber(recentJobs.length))} 产物 ${escapeHtml(formatNumber(recentFiles.length))} 录像 ${escapeHtml(formatNumber(liveRecorderFiles.length))}

最近任务样本

默认取 storage.status 里的 recent_jobs;如果后端没给,会退回到 dashboard.recent_jobs。

${renderStorageJobCards( recentJobs, "还没有缓存样本", "上传视频、导入作品后,这里会显示最近写入 NAS 的缓存路径。" )} ${recentFiles.length ? `

最近产物文件

后端如果提供产物文件索引,这里会优先直接露出最近写入的文件入口。

${renderStorageFileCards( recentFiles, "还没有产物文件", "当前 storage/status 没有返回可直接打开的产物文件。" )} ` : ""}

最近录像文件

如果 live-recorder 已接入,这里会继续显示当前租户的录像文件入口。

${renderStorageFileCards( liveRecorderFiles, "还没有录像文件", "录制完成后的文件会出现在当前租户的录像列表里。" )}
`; } function renderOneLinerActionRegistryPanel() { const items = safeArray(appState.onelinerActionRegistry); if (!items.length) { return `

OneLiner 动作注册表

当前后端还没返回动作注册表,先沿用默认动作。

暂未接入

/v2/oneliner/action-registry 可用后,这里会显示动作开关、描述和租户级配置。

`; } const grouped = items.reduce((acc, item) => { const category = item.category || "custom"; acc[category] = acc[category] || []; acc[category].push(item); return acc; }, {}); return `

OneLiner 动作注册表

把 OneLiner 可执行动作做成租户级注册中心,便于商业化灰度和定制。
${escapeHtml(formatNumber(items.length))} 条
${Object.entries(grouped).map(([category, list]) => `

${escapeHtml(category)}

${escapeHtml(`当前分类下 ${list.length} 条动作。`)}

${list.map((item) => ` ${escapeHtml(item.label || item.action_key || "action")} `).join("")}
`).join("")}
`; } function renderTenantQuotaPanel() { const quota = appState.tenantQuota; const usage = appState.tenantUsage || quota?.usage || {}; if (!quota && !usage) { return `

租户额度与审计

当前后端还没接入 quota / usage。

暂未接入

等 live collector 同步 `/v2/tenant/quota` 和 `/v2/tenant/usage` 后,这里会展示本周期预算、动作配额和最近计量记录。

`; } const categories = usage?.categories || {}; const recentItems = safeArray(usage?.recent_items); const cards = [ { label: "预算", value: `${formatNumber((quota?.monthly_budget_cents || 0) / 100)} 元`, sub: `已用 ${formatNumber((usage?.total_cost_cents || 0) / 100)} 元` }, { label: "分析配额", value: formatNumber(quota?.analysis_quota || 0), sub: `已用 ${formatNumber(categories.analysis?.quantity || 0)}` }, { label: "文案配额", value: formatNumber(quota?.copy_quota || 0), sub: `已用 ${formatNumber(categories.copy?.quantity || 0)}` }, { label: "AI 视频配额", value: formatNumber(quota?.ai_video_quota || 0), sub: `已用 ${formatNumber(categories.ai_video?.quantity || 0)}` }, { label: "实拍剪辑配额", value: formatNumber(quota?.real_cut_quota || 0), sub: `已用 ${formatNumber(categories.real_cut?.quantity || 0)}` }, { label: "存储上限", value: formatBytes(quota?.storage_limit_bytes || 0), sub: `当前 ${formatBytes(usage?.storage_bytes || 0)}` } ]; return `

租户额度与审计

预算、动作配额和最近计量都按租户 + 项目隔离。
${escapeHtml(quota?.enabled === false ? "已停用额度保护" : "额度保护开启")} ${quota?.storage_over_limit ? `存储超限` : ""} 编辑额度
${cards.map((item) => `
${escapeHtml(item.label)} ${escapeHtml(item.value)} ${escapeHtml(item.sub)}
`).join("")}

最近计量记录

动作执行后会写入租户级 ledger,便于后面做商业化配额、成本和审计。

${recentItems.map((item) => `

${escapeHtml(item.category || "usage")}

${escapeHtml(formatDateTime(item.created_at))}

次数 ${escapeHtml(formatNumber(item.quantity || 0))} 成本 ${(item.cost_cents || 0) / 100} 元 ${item.reference_type ? `${escapeHtml(item.reference_type)}` : ""} ${item.reference_id ? `${escapeHtml(brief(item.reference_id, 14))}` : ""}
`).join("") || `

还没有计量记录

等 OneLiner 或生产动作实际执行后,这里会累积本周期的 usage ledger。

`}
`; } function renderPlatformAgentPanel() { const items = safeArray(appState.platformAgents); if (!items.length) { return `

平台 Agent

当前后端还没接入平台 Agent 控制面。

暂未接入

等 live collector 同步 `/v2/platform-agents` 后,这里会切成真实视图。

`; } return `

平台 Agent

按用户 + 平台隔离,沉淀该平台的方法论、记忆和技能。
${escapeHtml(formatNumber(items.length))} 个
${items.map((item) => `
${escapeHtml(item.name || item.platform_label)}
${escapeHtml(item.mission || item.notes || "先绑定执行 Agent,再补任务目标和方法论。")}
${escapeHtml(item.status || "draft")} ${item.readiness_label ? `= 50 ? "blue" : "orange"}">${escapeHtml(item.readiness_label)} ${escapeHtml(formatNumber(item.readiness_score || 0))}` : ""} 记忆 ${escapeHtml(formatNumber(item.memory_count))} 技能 ${escapeHtml(formatNumber(item.skill_count))} ${escapeHtml(item.assistant?.name || "未绑 Agent")}
${item.recent_memory || item.recent_skill ? `
${item.recent_memory ? `

最近记忆 · ${escapeHtml(item.recent_memory.title || item.recent_memory.memory_key || "未命名")}

${escapeHtml(brief(item.recent_memory.summary || "暂无摘要", 68))}

` : ""} ${item.recent_skill ? `

最近技能 · ${escapeHtml(item.recent_skill.name || item.recent_skill.skill_key || "未命名")}

${escapeHtml(brief(item.recent_skill.test_spec?.summary || item.recent_skill.method?.summary || "暂无方法摘要", 68))}

${escapeHtml(item.recent_skill.status || "draft")} 得分 ${escapeHtml(formatNumber(item.recent_skill.last_score || 0))}
` : ""}
` : ""}
查看详情 配置 补记忆 补技能
`).join("")}
`; } function renderAdminOpsPanel() { if (!isSuperAdmin()) return ""; const overview = appState.adminOpsOverview; if (!overview) { return `

运维与审计 Agent

仅平台最高权限用户可见。

尚未拉到概览

刷新后会自动读取失败任务、集成健康和待审事件。

`; } const incidents = safeArray(overview.incidents).slice(0, 6); const audits = safeArray(overview.recent_audits).slice(0, 5); const fixRuns = safeArray(overview.recent_fix_runs).slice(0, 5); return `

运维与审计 Agent

只给管理员开放,主要盯日志、失败任务和集成异常。
${escapeHtml(formatNumber(overview.incident_count))} 条事件 待处理 ${escapeHtml(formatNumber(overview.open_incident_count || 0))} 错误 ${escapeHtml(formatNumber(overview.severity_counts?.error || 0))} ${escapeHtml(formatNumber(overview.failed_job_count))} 个失败任务 修复计划 ${escapeHtml(formatNumber(overview.fix_run_count || 0))} 重新扫描
${incidents.map((item) => `

${escapeHtml(item.title)}

${escapeHtml(item.summary || "待补详情")}

${escapeHtml(item.severity || "warn")} ${escapeHtml(item.status || "open")} ${item.source_type ? `${escapeHtml(item.source_type)}` : ""} ${item.tenant_user_id ? `租户 ${escapeHtml(brief(item.tenant_user_id, 12))}` : ""} ${item.source_type === "job" ? actionTag("看任务详情", "open-job-detail", `data-job-id="${escapeHtml(item.source_id || "")}"`) : ""} ${item.source_type === "integration" ? actionTag("去自动流程", "goto-automation") : ""} ${item.tenant_project_id ? actionTag("去生产中心", "goto-production") : ""} 生成修复计划 审计处理
`).join("") || `

当前没有待处理事件

最近主链比较稳定,继续观察即可。

`}

最近修复计划

这里代表运维 Agent 输出的修复方案,必须经过审计 Agent 放行才算闭环。

${fixRuns.map((item) => `

${escapeHtml(item.plan?.summary || item.id || "修复计划")}

${escapeHtml(item.plan?.steps?.[0] || "待补充修复步骤")}

${escapeHtml(item.plan_scope || "plan")} ${escapeHtml(item.audit_status || "pending")} ${item.incident_id ? `事件 ${escapeHtml(brief(item.incident_id, 10))}` : ""} 审计放行
`).join("") || `

还没有修复计划

当运维 Agent 针对故障事件生成 repair plan 后,这里会自动出现。

`}

最近审计记录

保留管理员扫描、放行、驳回等动作,方便商业化量产时追责和复盘。

${audits.map((item) => `

${escapeHtml(item.summary || item.action_key || "审计记录")}

${escapeHtml(formatDateTime(item.created_at))}

${escapeHtml(item.action_key || "audit")} ${escapeHtml(item.status || "recorded")} ${item.incident_id ? `事件 ${escapeHtml(brief(item.incident_id, 10))}` : ""}
`).join("") || `

还没有审计记录

等管理员做一次扫描或审计处理后,这里会自动出现。

`}
`; } function getIntegrationOverview() { const cards = getIntegrationCards(); const reachableCount = cards.filter((item) => item.detail.available && item.detail.reachable).length; const availableCount = cards.filter((item) => item.detail.available).length; const aiVideoGuard = getPipelineGuard("aiVideo"); const realCutGuard = getPipelineGuard("realCut"); const blockedActions = [ !aiVideoGuard.enabled ? aiVideoGuard.reason : "", !realCutGuard.enabled ? realCutGuard.reason : "" ].filter(Boolean); const tone = !availableCount ? "blue" : blockedActions.length ? "red" : cards.some((item) => item.detail.available && !item.detail.reachable) ? "orange" : "green"; const headline = !availableCount ? "依赖健康尚未拉取" : blockedActions.length ? `自动链路受阻:${blockedActions.length} 项` : `${reachableCount}/${cards.length} 项依赖在线`; const subtitle = !availableCount ? "刷新后会显示直播录制 / cutvideo / huobao / n8n / ASR 的真实状态。" : blockedActions.length ? blockedActions.join(";") : "AI 视频与实拍剪辑链路当前可直接发起。"; return { cards, tone, headline, subtitle }; } function getJobSeedBrief(job) { return [ job?.style_summary, job?.transcript_text, job?.result?.summary, job?.artifacts?.brief, job?.title ].find((value) => String(value || "").trim()) || ""; } function collectHttpLinks(input, path = "result", bucket = []) { if (!input) return bucket; if (typeof input === "string") { const value = input.trim(); if (/^https?:\/\//i.test(value)) { bucket.push({ label: path, url: value }); } return bucket; } if (Array.isArray(input)) { input.forEach((item, index) => collectHttpLinks(item, `${path}[${index}]`, bucket)); return bucket; } if (typeof input === "object") { Object.entries(input).forEach(([key, value]) => collectHttpLinks(value, `${path}.${key}`, bucket)); } return bucket; } function getJobPreviewLinks(job) { const deduped = []; const seen = new Set(); collectHttpLinks(job?.result, "result", deduped); collectHttpLinks(job?.artifacts, "artifacts", deduped); return deduped.filter((item) => { if (!item.url || seen.has(item.url)) return false; seen.add(item.url); return true; }).slice(0, 8); } function isCandidateLinked(candidate, links) { const accountId = String(candidate?.candidate_account_id || ""); const profileUrl = String(candidate?.candidate_profile_url || ""); return safeArray(links).some((link) => ( (accountId && String(link.target_account_id || "") === accountId) || (profileUrl && String(link.target_profile_url || "") === profileUrl) )); } function markSavedCandidate(candidate, links) { const nextCandidates = safeArray(appState.lastSimilaritySearch?.candidates).map((item) => { const sameAccount = String(item.candidate_account_id || "") && String(item.candidate_account_id || "") === String(candidate.candidate_account_id || ""); const sameUrl = String(item.candidate_profile_url || "") && String(item.candidate_profile_url || "") === String(candidate.candidate_profile_url || ""); if (!sameAccount && !sameUrl) return item; return { ...item, saved: true }; }); if (appState.lastSimilaritySearch) { appState.lastSimilaritySearch = { ...appState.lastSimilaritySearch, candidates: nextCandidates }; } if (appState.selectedWorkspace) { appState.selectedWorkspace = { ...appState.selectedWorkspace, linked_accounts: safeArray(links) }; } } async function saveCandidateAsBenchmark(candidateIndex, relationType = "benchmark") { const account = requireSelectedAccountRow(); const platform = getAccountPlatform(account); const benchmarkPath = getWorkbenchRoute(platform, "benchmarkLinks", account.id); if (!benchmarkPath) throw new Error(getPendingWorkbenchReason(platform)); const candidate = safeArray(appState.lastSimilaritySearch?.candidates)[Number(candidateIndex)]; if (!candidate) throw new Error("当前候选不存在,请先重新查相似"); const payload = { target_account_ids: candidate.candidate_account_id ? [candidate.candidate_account_id] : [], target_profile_urls: candidate.candidate_account_id ? [] : [candidate.candidate_profile_url].filter(Boolean), relation_type: relationType, note: brief(candidate.rationale_text || "由相似搜索自动加入对标库", 120), search_id: appState.lastSimilaritySearch?.id || "" }; if (!payload.target_account_ids.length && !payload.target_profile_urls.length) { throw new Error("当前候选没有可保存的账号或主页链接"); } const result = await storyforgeFetch(benchmarkPath, { method: "POST", body: payload }); markSavedCandidate(candidate, result.links); rememberAction("候选已存对标", `已把「${candidate.candidate_nickname || candidate.candidate_profile_url || "候选账号"}」加入对标关系。`, "green", result); renderAll(); } function screenShell(title, subtitle, actionsHtml, bodyHtml) { return `

${escapeHtml(title)}

${escapeHtml(subtitle)}

${actionsHtml || ""}
${bodyHtml} `; } function button(label, action, tone = "secondary", options = {}) { const classes = ["btn", `btn-${tone}`]; if (options.className) classes.push(options.className); if (options.disabledReason) classes.push("is-disabled"); const targetAction = options.disabledReason ? "show-disabled-reason" : action; const title = options.disabledReason || options.title || ""; return ` `.replace(/\s+/g, " ").trim(); } function actionTag(label, action, attrs = "", options = {}) { const classes = ["tag"]; const targetAction = options.disabledReason ? "show-disabled-reason" : action; if (options.disabledReason) { classes.push("tag-disabled"); } else if (targetAction) { classes.push("clickable-tag"); } const title = options.disabledReason || options.title || ""; return ` ${escapeHtml(label)} `.replace(/\s+/g, " ").trim(); } function renderPipelineButton(kind, tone = "secondary") { const config = PIPELINE_GUARDS[kind]; if (!config) return ""; const guard = getPipelineGuard(kind); return button(config.label, config.openAction, tone, { disabledReason: guard.enabled ? "" : guard.reason }); } function renderPipelineJobTag(kind, job, label) { const config = PIPELINE_GUARDS[kind]; if (!config || !job?.id) return ""; const guard = getPipelineGuard(kind); return actionTag(label, config.jobAction, `data-job-id="${escapeHtml(job.id)}"`, { disabledReason: guard.enabled ? "" : guard.reason }); } function renderIntegrationOverviewPanel(options = {}) { const overview = getIntegrationOverview(); const cards = overview.cards; const showActions = options.showActions !== false; return `
${escapeHtml(overview.headline)}

${escapeHtml(overview.subtitle)}

${showActions ? `
${renderPipelineButton("aiVideo", "primary")} ${renderPipelineButton("realCut")}
` : ""}
${cards.map((item) => `${escapeHtml(item.meta.label)} ${escapeHtml(item.status.summary)}`).join("")}
${cards.map((item) => `

${escapeHtml(item.meta.label)}

${escapeHtml(item.meta.hint)}

${escapeHtml(item.status.summary)}
${safeArray(item.meta.impacts).map((impact) => `${escapeHtml(impact)}`).join("")} ${item.detail.statusCode ? `HTTP ${escapeHtml(item.detail.statusCode)}` : ""}
${escapeHtml(item.note)}
${item.extra ? `
${escapeHtml(item.extra)}
` : ""}
${escapeHtml(item.key === "live_recorder" ? "仅通过当前租户的后端代理访问" : (item.detail.url || item.detail.baseUrl || "未提供探测地址"))}
${item.actions ? `
${item.actions}
` : ""}
`).join("")}
`; } function renderEmptyState(title, description) { return `
${escapeHtml(title)}

${escapeHtml(description)}

`; } function renderDashboardScreen() { if (!appState.session) { return screenShell( "项目总台", "先连接后端,再加载项目、对标、Agent 和生产状态。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("还没有连接 StoryForge", "登录后就能把项目总台替换成真实数据。") ); } if (!appState.dashboard) { return screenShell( "项目总台", appState.me?.approval_status === "pending" ? "当前账号还在等待审批。" : "正在加载项目数据。", `${button("刷新", "refresh-data")} ${button("切换连接", "open-auth")}`, renderEmptyState("工作区暂未就绪", appState.message || "如果账号未审批通过,当前页会先停在待审批状态。") ); } const dashboard = appState.dashboard; const projects = safeArray(dashboard.projects); const jobs = safeArray(dashboard.recent_jobs); const assistants = safeArray(dashboard.assistants); const accounts = safeArray(appState.accounts); const trackedAccounts = safeArray(appState.trackingAccounts); const digestItems = getTrackingDigestItems(3); const actions = []; if (!projects.length) actions.push("先新建一个项目"); if (!assistants.length) actions.push("先创建第一个 Agent"); if (!accounts.length) actions.push("先导入一个抖音主页或作品"); if (!trackedAccounts.length && accounts.length) actions.push("挑 1 个重点账号加入跟踪"); if (jobs.some((item) => item.status !== "completed")) actions.push("处理进行中的生产任务"); if (!actions.length) actions.push("继续补高分对标并安排生产"); return screenShell( "项目总台", "先看项目状态、待办动作和高价值对标。", `${button("新建项目", "create-project")} ${button("导入主页", "open-import-homepage")} ${button("OneLiner", "open-oneliner")} ${button("创建 Agent", "open-create-assistant", "primary")}`, `
活跃项目${escapeHtml(formatNumber(projects.length))}
项目总数${escapeHtml(formatNumber(projects.filter((item) => item.description).length))} 个有说明
导入内容${escapeHtml(formatNumber(appState.contentSources.length))}
主页 / 作品 / 本地素材${escapeHtml(formatNumber(appState.contentSources.filter((item) => item.source_kind === "creator_account").length))} 个主页
跟踪账号${escapeHtml(formatNumber(trackedAccounts.length))}
可生成日报${escapeHtml(formatNumber(digestItems.length))} 条新摘要
Agent${escapeHtml(formatNumber(assistants.length))}
已创建${escapeHtml(formatNumber(assistants.filter((item) => !(item.model_profile_id || "")).length))} 个待补模型
生产任务${escapeHtml(formatNumber(jobs.length))}
最近 20 条${escapeHtml(formatNumber(jobs.filter((item) => item.status === "completed").length))} 条已完成
${renderIntegrationOverviewPanel({ compact: true })}

当前主流程

项目 → Agent → 调研 → 导入并绑定 → 生产 → 复盘

我的项目 找对标 跟踪账号 Agent 生产中心

今日重点动作

按当前数据自动生成
${escapeHtml(formatNumber(actions.length))} 项
${actions.map((item, index) => `

${index + 1}. ${escapeHtml(item)}

${escapeHtml(index === 0 ? "先把最影响主流程的动作做掉。" : "做完上一步再继续推进。")}

`).join("")}
${renderLastActionCard()}

高分对标

优先看当前已同步账号
${accounts.slice(0, 3).map((account) => `
${escapeHtml(initials(getAccountName(account)))}
${escapeHtml(getAccountName(account))}
${escapeHtml(account.signature || getAccountProfileUrl(account) || `已同步${platformLabel(getAccountPlatform(account))}账号`)}
作品 ${escapeHtml(formatNumber(account.video_summary?.count))} 均播 ${escapeHtml(formatNumber(account.video_summary?.avg_play))} ${escapeHtml(account.sync_status || "synced")}
`).join("") || `
先到“找对标”导入一个账号。
`}

当前项目

${escapeHtml(getSelectedProject()?.name || "还没有项目")}

知识库${escapeHtml(formatNumber(getSelectedProject() ? getProjectStats(getSelectedProject().id).knowledgeBases.length : 0))}
Agent${escapeHtml(formatNumber(getSelectedProject() ? getProjectStats(getSelectedProject().id).assistants.length : 0))}
任务${escapeHtml(formatNumber(getSelectedProject() ? getProjectStats(getSelectedProject().id).jobs.length : 0))}
来源${escapeHtml(formatNumber(getSelectedProject() ? getProjectStats(getSelectedProject().id).sources.length : 0))}
${renderPlatformAgentPanel()}
${renderStorageStatusPanel()}

跟踪摘要

按最近同步的账号作品生成
${escapeHtml(daysSince(appState.lastSeenAt))} 天汇总
${digestItems.map((item) => `

${escapeHtml(item.account?.nickname || "未命名账号")} · ${escapeHtml(item.video?.title || item.video?.description || "最新作品")}

${escapeHtml(item.summary || `最近发布时间 ${formatDateTime(item.video?.published_at)},适合继续交给 Agent 做借鉴点标注。`)}

抖音 ${escapeHtml(item.is_high_value ? "高价值" : "可学习")} ${item.assistant_name ? `${escapeHtml(item.assistant_name)}` : ""}
`).join("") || `

还没有日报

先把重点账号加入跟踪,日报才会开始累积。

`}

最新异常

直接看需要处理的阻塞
${jobs.filter((item) => item.status === "failed").slice(0, 3).map((job) => `

${escapeHtml(job.title)}

${escapeHtml(job.error || "任务失败,请重新进入生产中心查看。")}

失败${escapeHtml(job.line_type || "analysis")}
`).join("") || `

当前无异常

最近任务运行正常,继续推进导入和生产即可。

`}
` ); } function renderProjectsScreen() { if (!appState.dashboard) { return screenShell("我的项目", "先连接工作区,再加载项目。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("项目未加载", "登录成功后,这里会显示真实项目和导入队列。")); } const projects = safeArray(appState.dashboard.projects); const selectedProject = getSelectedProject(); return screenShell( "我的项目", "先建项目,再决定是否绑定自己的账号。", `${button("新建项目", "create-project", "primary")} ${button("导入作品", "open-import-video-link")} ${button("导入文本", "open-import-text")} ${button("上传视频", "open-upload-video")}`, `

当前项目

${escapeHtml(selectedProject?.name || "还没有项目")} · ${escapeHtml(selectedProject?.description || "创建后即可承接对标、Agent 和生产任务。")}

${projects.map((project) => `${escapeHtml(project.name)}`).join("") || `暂无项目`}

项目状态

真实项目列表
${projects.map((project) => { const stats = getProjectStats(project.id); return `
${escapeHtml(project.name)}
${escapeHtml(project.description || "未填写说明")}
知识库 ${escapeHtml(formatNumber(stats.knowledgeBases.length))} Agent ${escapeHtml(formatNumber(stats.assistants.length))} 任务 ${escapeHtml(formatNumber(stats.jobs.length))}
`; }).join("") || `
当前还没有项目。
`}

最近导入队列

按内容源展示
${safeArray(appState.contentSources).slice(0, 6).map((source) => `

${escapeHtml(source.title || source.handle || source.source_url || source.id)}

${escapeHtml(source.platform || source.source_kind || "内容源")} · ${escapeHtml(source.source_url || source.local_path || "暂无链接")}

${source.project_id === appState.selectedProjectId ? "当前项目" : "其他项目"} ${escapeHtml(source.source_kind || "-")}
`).join("") || `

还没有导入内容

先去“找对标”导入主页、作品或本地视频。

`}
` ); } function renderDiscoveryScreen() { if (!appState.dashboard) { return screenShell("找对标", "连接后端后才能加载真实对标账号。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("对标库未加载", "登录后这里会显示当前平台的账号列表和详情。")); } const query = appState.discoveryQuery.toLowerCase(); const accounts = safeArray(appState.accounts).filter((account) => { if (!query) return true; return [getAccountName(account), account.signature, getAccountProfileUrl(account), getAccountHandle(account), ...safeArray(account.tags), ...safeArray(account.keywords)] .join(" ") .toLowerCase() .includes(query); }); const selected = getSelectedAccount(); const currentPlatform = getAccountPlatform(selected) || getPreferredPlatform(); const currentPlatformLabel = getPlatformShortLabel(currentPlatform); const workbenchReason = !isWorkbenchPlatform(currentPlatform) ? getPendingWorkbenchReason(currentPlatform) : ""; const reports = safeArray(appState.selectedWorkspace?.recent_reports); const linkedAccounts = safeArray(appState.selectedWorkspace?.linked_accounts); const videos = safeArray(appState.selectedVideos?.items); const fallbackVideos = safeArray(selected?.video_summary?.videos); const effectiveVideos = videos.length ? videos : fallbackVideos; const topVideos = getHighScoreVideos(3); const latestVideos = getLatestVideos(2); const similarCandidates = safeArray(appState.lastSimilaritySearch?.candidates).slice(0, 5); const selectedProject = getSelectedProject(); const importedSources = getCurrentProjectSourcesForAccount(selected, selectedProject?.id || ""); const tracked = selected?.id ? isTrackedAccount(selected.id) : false; return screenShell( "找对标", isWorkbenchPlatform(currentPlatform) ? `这里已经接入真实${currentPlatformLabel}账号列表和单账号详情。` : `${workbenchReason}。当前仍可导入内容源、绑定 Agent 和沉淀复盘。`, `${button("导入主页", "open-import-homepage")} ${button("导入当前对标", "open-import-selected-account")} ${button(tracked ? "已在跟踪" : "加入跟踪", "open-track-selected-account", "secondary", { disabledReason: workbenchReason || "" })} ${button("账号分析", "analyze-selected-account", "secondary", { disabledReason: workbenchReason || "" })} ${button("高分分析", "analyze-top-videos", "secondary", { disabledReason: workbenchReason || "" })} ${button("查相似", "open-similar-search", "secondary", { disabledReason: workbenchReason || "" })} ${button("存对标", "open-benchmark-link", "primary", { disabledReason: workbenchReason || "" })}`, `
平台:${escapeHtml(currentPlatformLabel)}
账号数:${escapeHtml(formatNumber(accounts.length))}
报告:${escapeHtml(formatNumber(reports.length))}
作品:${escapeHtml(formatNumber(effectiveVideos.length))}
真实接口 工作台详情 高分作品 绑定关系
${accounts.map((account) => { const active = account.id === appState.selectedAccountId; return ` `; }).join("") || `
当前平台没有账号数据。
`}
${accounts.map((account) => ` `).join("") || ``}

当前选中对标

直接来自当前平台工作台
${escapeHtml(getAccountName(selected) || "未选中")}
${escapeHtml(initials(getAccountName(selected) || "SF"))}

${escapeHtml(getAccountName(selected) || "还没有选中账号")}

${escapeHtml(getAccountProfileUrl(selected) || selected?.signature || "左侧点一个账号,这里会展示详情。")}

作品 ${escapeHtml(formatNumber(selected?.video_summary?.count))} 高分 ${escapeHtml(formatNumber(topVideos.length))} 报告 ${escapeHtml(formatNumber(reports.length))} 对标 ${escapeHtml(formatNumber(linkedAccounts.length))}
作品数${escapeHtml(formatNumber(selected?.video_summary?.count))}
高分作品${escapeHtml(formatNumber(topVideos.length))}
报告数${escapeHtml(formatNumber(reports.length))}
已绑对标${escapeHtml(formatNumber(linkedAccounts.length))}

接入当前项目

把当前对标导入到项目,并绑定 Agent 做持续同步
${escapeHtml(importedSources.length ? "已接入" : "未接入")}
${selected ? `

${escapeHtml(selectedProject?.name || "未选项目")}

${escapeHtml(importedSources.length ? `当前项目已接入 ${formatNumber(importedSources.length)} 个内容源,可继续同步或换 Agent。` : "当前项目还没有接入这个对标账号,可直接导入主页并绑定 Agent。")}

${escapeHtml(selectedProject?.name || "未选项目")} ${escapeHtml(getSelectedAssistant()?.name || "未选 Agent")} ${importedSources.length ? "继续同步" : "导入当前对标"} ${escapeHtml(tracked ? "已在跟踪" : "加入跟踪")}
` : `

还没有选中账号

先从左侧列表选一个对标账号,再决定是否导入到当前项目。

`}

账号画像

  • ${escapeHtml(selected?.signature || "暂无签名")}
  • ${escapeHtml("平台:" + currentPlatformLabel)}
  • ${escapeHtml("标签:" + safeArray(selected?.tags).slice(0, 4).join(" / ") || "暂无标签")}
  • ${escapeHtml("同步状态:" + (selected?.sync_status || "-"))}

高分作品

    ${topVideos.map((video) => `
  • ${escapeHtml(describeVideo(video))}
  • `).join("") || "
  • 暂无高分作品
  • "}

最近报告

    ${reports.slice(0, 3).map((report) => { const suggestion = safeArray(report.suggestions)[0]; const summary = suggestion?.parsed_json?.executive_summary || suggestion?.suggestion_text || report.focus_text || "暂无结论"; return `
  • ${escapeHtml(brief(summary, 48))}
  • `; }).join("") || "
  • 暂无分析报告
  • "}

最新作品

优先看近期更新与窗口期题材
${escapeHtml(formatNumber(effectiveVideos.length))} 条
${latestVideos.map((video) => `

${escapeHtml(describeVideo(video))}

发布时间 ${escapeHtml(formatDateTime(video.published_at))} · 播放 ${escapeHtml(formatNumber(video.stats?.play))} · 点赞 ${escapeHtml(formatNumber(video.stats?.like))}

${escapeHtml(video.content_type || "video")} 得分 ${escapeHtml(formatNumber(video.score?.performance_score || 0))} ${getVideoLink(video) ? `打开原作品` : ""}
`).join("") || `

还没有最近作品

当前账号只同步了基础信息,还没拉到完整作品列表。

`}

相似对标 / 已绑关系

来自账号工作台
${linkedAccounts.map((link) => `

${escapeHtml(link.target_nickname || link.target_profile_url || "未命名对标")}

${escapeHtml(link.note || link.target_profile_url || "已保存对标关系")}

${escapeHtml(link.relation_type || "benchmark")} ${link.target_account_id ? `看详情` : ""} ${link.target_profile_url ? `打开主页` : ""}
`).join("") || `

暂无已保存对标

当前账号还没有保存过对标关系。

`}

最近相似候选

由 Agent 辅助生成
${escapeHtml(formatNumber(similarCandidates.length))} 个
${similarCandidates.map((candidate, index) => `

${escapeHtml(candidate.candidate_nickname || candidate.candidate_profile_url || "候选账号")}

${escapeHtml(brief(candidate.rationale_text || "暂无理由", 96))}

启发分 ${escapeHtml(formatNumber(candidate.agent_score || candidate.heuristic_score || 0))} ${candidate.candidate_account_id ? `看详情` : ""} ${isCandidateLinked(candidate, linkedAccounts) || candidate.saved ? `已保存` : `存对标`} ${candidate.candidate_profile_url ? `打开主页` : ""}
`).join("") || `

还没有相似候选

先点“查相似”,这里会展示最近一轮结果。

`}
` ); } function renderTrackingScreen() { if (!appState.dashboard) { return screenShell("跟踪账号", "登录后才能生成真实日报。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("日报未加载", "当前还没有可用的对标账号数据。")); } const currentPlatform = getPreferredPlatform(); const trackingAccountsPath = getWorkbenchRoute(currentPlatform, "trackingAccounts"); if (!trackingAccountsPath || !backendSupports(trackingAccountsPath)) { return screenShell( "跟踪账号", `${getPendingWorkbenchReason(currentPlatform)}。`, `${button("跳到找对标", "goto-discovery", "primary")}`, renderEmptyState("跟踪能力暂未接入", `这套后端还没有接入 ${platformLabel(currentPlatform)} 跟踪接口,等 live collector 同步后这里会自动切成真实日报。`) ); } const trackedAccounts = safeArray(appState.trackingAccounts); const digestItems = getTrackingDigestItems(12); const cursorLabel = appState.lastSeenAt ? formatDateTime(appState.lastSeenAt) : "尚未记录"; return screenShell( "跟踪账号", `这里已经接上真实${getPlatformShortLabel(currentPlatform)}跟踪对象和按上次打开后的更新日报。`, `${button("同步全部", "refresh-tracking")} ${button("标记已读", "mark-tracking-read")} ${button("跳到找对标", "goto-discovery", "primary")}`, `

日报逻辑

按上次打开后汇总。上次打开距今 ${escapeHtml(daysSince(appState.lastSeenAt))} 天,本次优先展示有更新且值得借鉴的内容。

按上次打开汇总 Agent 标借鉴点 高价值内容可进学习集 上次已读 ${escapeHtml(cursorLabel)}

跟踪列表

真实跟踪对象与绑定 Agent
${escapeHtml(formatNumber(trackedAccounts.length))} 个
跟踪 ${escapeHtml(formatNumber(trackedAccounts.length))} 日报 ${escapeHtml(formatNumber(digestItems.length))} ${escapeHtml(daysSince(appState.lastSeenAt))} 天窗口
${trackedAccounts.map((item) => `

${escapeHtml(item.account?.nickname || "未命名账号")}

最近作品 ${escapeHtml(formatNumber(item.account?.video_summary?.count))} 条 · 平均播放 ${escapeHtml(formatNumber(item.account?.video_summary?.avg_play))}

已跟踪 ${escapeHtml(item.assistant_name || "未绑 Agent")} ${actionTag("立即同步", "refresh-tracked-account", `data-tracked-account-id="${escapeHtml(item.tracked_account_id)}"`)} ${actionTag("看详情", "select-account", `data-account-id="${escapeHtml(item.tracked_account_id)}"`)}
`).join("") || `

暂无跟踪账号

先去找对标把重点账号加入跟踪。

`}

更新日报

优先看最近更新的作品摘要
${escapeHtml(formatNumber(digestItems.length))} 条
${digestItems.map((item) => `

${escapeHtml(item.account?.nickname || "账号")} · ${escapeHtml(item.video?.title || item.video?.description || "最新作品")}

${escapeHtml(item.summary || `发布时间 ${formatDateTime(item.video?.published_at)},建议继续判断借鉴点。`)}

抖音 ${escapeHtml(item.is_high_value ? "高价值" : "可学习")} ${item.assistant_name ? `${escapeHtml(item.assistant_name)}` : ""} ${item.video?.share_url ? `打开作品` : ""}
${safeArray(item.borrowing_points).length ? `
${safeArray(item.borrowing_points).slice(0, 3).map((point) => `${escapeHtml(point)}`).join("")}
` : ""}
`).join("") || `

暂无日报

先把账号加入跟踪,并等待新作品更新。

`}
` ); } function renderAutomationScreen() { const jobs = safeArray(appState.dashboard?.recent_jobs); const analysisJobs = jobs.filter((item) => item.line_type === "analysis").length; const aiVideoJobs = jobs.filter((item) => item.line_type === "ai_video").length; const realCutJobs = jobs.filter((item) => item.line_type === "real_cut").length; const overview = getIntegrationOverview(); return screenShell( "自动流程", "自动同步、日报生成和失败补跑先统一看这里。", `${button("刷新", "refresh-data")} ${button("OneLiner", "open-oneliner")} ${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${button("去生产", "goto-production", "primary")}`, `

自动流程

当前按真实任务量和依赖健康状态给出看板,自动流程受阻时会直接在这里拦住动作。

分析任务${escapeHtml(formatNumber(analysisJobs))}
AI 视频${escapeHtml(formatNumber(aiVideoJobs))}
实拍剪辑${escapeHtml(formatNumber(realCutJobs))}
内容源${escapeHtml(formatNumber(appState.contentSources.length))}
${renderIntegrationOverviewPanel({ showActions: false })}

动作防呆

依赖不可用时,相关动作会在这里和生产页一起被拦住。
${escapeHtml(overview.headline)}
AI 视频 ${escapeHtml(getPipelineGuard("aiVideo").enabled ? "可执行" : "已拦截")} 实拍剪辑 ${escapeHtml(getPipelineGuard("realCut").enabled ? "可执行" : "已拦截")} ASR ${escapeHtml(getIntegrationStatus(getIntegrationDetail("asr")).summary)}
${renderPipelineButton("aiVideo", "primary")} ${renderPipelineButton("realCut")}
${escapeHtml(overview.subtitle)}
${renderTenantQuotaPanel()}
${renderOneLinerActionRegistryPanel()}
${renderAdminOpsPanel()} ` ); } function renderOwnedScreen() { if (!appState.dashboard) { return screenShell("我的账号", "先连接后端。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("我的账号未加载", "登录后这里会展示当前账号和建议动作。")); } const me = appState.me || appState.session?.account || {}; const firstAssistant = safeArray(appState.dashboard.assistants)[0]; return screenShell( "我的账号", "这里先用当前登录账号和最近产出组合成第一版总览。", `${button("刷新", "refresh-data")} ${button("去 Agent", "goto-playbook", "primary")}`, `
${escapeHtml(initials(me.display_name || me.username))}

${escapeHtml(me.display_name || me.username || "当前账号")}

${escapeHtml(firstAssistant?.generation_goal || "先创建 Agent,再把平台目标和变现方式补齐。")}

项目${escapeHtml(formatNumber(appState.dashboard.projects?.length))}
Agent${escapeHtml(formatNumber(appState.dashboard.assistants?.length))}
任务${escapeHtml(formatNumber(appState.dashboard.recent_jobs?.length))}
素材${escapeHtml(formatNumber(appState.documents.length))}
` ); } function renderPlaybookScreen() { if (!appState.dashboard) { return screenShell("Agent", "先连接后端。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("Agent 未加载", "登录后这里会展示真实 Agent 列表和模型。")); } const assistants = safeArray(appState.dashboard.assistants); const models = safeArray(appState.dashboard.model_profiles); const currentModel = getCurrentModelProfile(); const currentAssistant = getSelectedAssistant(); const localCatalog = appState.localModelCatalog || {}; const gatewayModels = safeArray(localCatalog.models).map((item) => item.id).filter(Boolean); return screenShell( "Agent", "这里接真实 Agent 列表,当前已经支持切换和编辑 Agent。", `${button("配置 OneLiner", "open-oneliner-profile")} ${button("设主模型", "open-preferred-model")} ${button("新建 Agent", "open-create-assistant")} ${button("生成文案", "open-generate-copy")} ${button("去生产", "goto-production", "primary")}`, `

Agent 概览

先定项目、平台和主模型,再导入内容让 Agent 学习。

${models.slice(0, 6).map((model) => `${escapeHtml(model.name)}`).join("") || `暂无模型`}

OneLiner 主 Agent

前端还没上的功能由它兜底承接,并调度平台 Agent。
${escapeHtml(appState.onelinerProfile?.display_name || "OneLiner")} ${escapeHtml(appState.onelinerProfile?.default_platform ? platformLabel(appState.onelinerProfile.default_platform) : "未设默认平台")} 打开对话

${escapeHtml(appState.onelinerProfile?.long_term_goal || "还没有设置长期目标")}

${escapeHtml(appState.onelinerProfile?.notes || "你可以把用户长期目标、账号目标、默认平台都绑给 OneLiner,再让它去调度平台 Agent。")}

会话 ${escapeHtml(formatNumber(safeArray(appState.onelinerSessions).length))} 平台 Agent ${escapeHtml(formatNumber(safeArray(appState.platformAgents).length))} 编辑配置
${renderOneLinerActionRegistryPanel()}
${renderPlatformAgentPanel()}
${renderTenantQuotaPanel()}

当前 Agent

后续文案生成、对标绑定和复盘默认都会优先使用这里选中的 Agent
${currentAssistant ? `已选` : `未选`} ${currentAssistant ? `编辑` : ""}
${currentAssistant ? `

${escapeHtml(currentAssistant.name)}

${escapeHtml(currentAssistant.generation_goal || currentAssistant.description || "先补齐这个 Agent 的目标和说明。")}

${escapeHtml(models.find((item) => item.id === currentAssistant.model_profile_id)?.name || "默认模型")} ${escapeHtml(formatNumber(safeArray(currentAssistant.knowledge_base_ids).length))} 条知识库 ${escapeHtml(brief(currentAssistant.description || "暂无说明", 22))}
` : `

还没有可用 Agent

先创建一个 Agent,再把当前项目的内容都交给它学习。

`}

本机模型网关

当前默认分析会优先走本机 cli-proxy-api
${escapeHtml(localCatalog.reachable ? "在线" : "离线")} ${localCatalog.management_url ? `打开管理页` : ""}

${escapeHtml(currentModel?.name || localCatalog.default_model || "GLM-5")}

${escapeHtml(currentModel ? `${currentModel.model_name || "-"} · ${currentModel.base_url || "-"}` : (localCatalog.public_base_url || localCatalog.base_url || "尚未读取到网关地址"))}

${gatewayModels.slice(0, 6).map((model) => `${escapeHtml(model)}`).join("") || `暂无可见模型`}

模型列表

来自真实 model_profiles
${models.map((model) => `

${escapeHtml(model.name)}

${escapeHtml(model.model_name || "-")} · ${escapeHtml(model.base_url || "-")}

`).join("") || `

暂无模型

先在“我的”里配置模型。

`}

Agent 列表

当前接的是后端 assistants
${assistants.map((assistant) => `

${escapeHtml(assistant.name)}

${escapeHtml(assistant.description || assistant.generation_goal || "暂无说明")}

知识库 ${escapeHtml(formatNumber(safeArray(assistant.knowledge_base_ids).length))} ${escapeHtml(models.find((item) => item.id === assistant.model_profile_id)?.name || "默认模型")} ${assistant.id === currentAssistant?.id ? "当前 Agent" : "设为当前"} 编辑
`).join("") || `

还没有 Agent

下一步可以直接把创建动作接进来。

`}

最近学习素材

从知识库文档取最近内容
${appState.documents.slice(0, 4).map((doc) => `

${escapeHtml(doc.title)}

${escapeHtml(brief(doc.style_summary || doc.transcript_text || doc.combined_text, 72))}

`).join("") || `

还没有学习素材

先去找对标导入一条主页或作品。

`}

最近生成

当前先承接文案生成结果
${appState.lastGeneratedCopy ? `

${escapeHtml(appState.lastGeneratedCopy.assistantName)}

${escapeHtml(appState.lastGeneratedCopy.content)}

需求:${escapeHtml(brief(appState.lastGeneratedCopy.prompt, 24))} ${escapeHtml(formatNumber(appState.lastGeneratedCopy.usedDocuments.length))} 条参考
` : `

还没有生成结果

先点“生成文案”,这里会保留最近一次结果。

`}
` ); } function renderProductionScreen() { if (!appState.dashboard) { return screenShell("生产中心", "先连接后端。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("生产中心未加载", "登录后这里会展示真实任务和作品。")); } const jobs = safeArray(appState.dashboard.recent_jobs); const activeJobs = jobs.filter((item) => item.status !== "completed").slice(0, 4); const recentDocs = appState.documents.slice(0, 3); const works = getProductionWorks(6); return screenShell( "生产中心", "这里已经接上真实任务和知识库文档,后续再继续补任务创建动作。", `${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${button("去复盘", "goto-review", "primary")}`, `

生产队列

最近任务的真实状态

分析任务

最近 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "analysis").length))} 条

实拍剪辑

最近 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "real_cut").length))} 条

AI 视频

最近 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "ai_video").length))} 条

内容源同步

最近 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "content_source_sync").length))} 条

分析 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "analysis").length))} 实拍 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "real_cut").length))} AI 视频 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "ai_video").length))}

当前任务

来自 recent_jobs
${(activeJobs.length ? activeJobs : jobs.slice(0, 4)).map((job) => `

${escapeHtml(job.title)}

${escapeHtml(brief(job.style_summary || job.transcript_text || job.error || "暂无摘要", 80))}

${escapeHtml(job.status)} ${escapeHtml(job.line_type || "analysis")} ${canDeriveAiVideo(job) ? renderPipelineJobTag("aiVideo", job, "做 AI 视频") : ""} ${canDeriveRealCut(job) ? renderPipelineJobTag("realCut", job, "做实拍剪辑") : ""} ${actionTag("看详情", "open-job-detail", `data-job-id="${escapeHtml(job.id)}"`)}
`).join("") || `

还没有任务

先去找对标导入内容。

`}

作品与成片

先看真实作品,再补文档与成片
${works.map((video) => `

${escapeHtml(describeVideo(video))}

${escapeHtml(`发布时间 ${formatDateTime(video.published_at)} · 播放 ${formatNumber(video.stats?.play)} · 点赞 ${formatNumber(video.stats?.like)}`)}

${escapeHtml(video.content_type || "video")} 得分 ${escapeHtml(formatNumber(video.score?.performance_score || 0))} ${getVideoLink(video) ? `打开原作品` : ""}
`).join("")} ${recentDocs.map((doc) => `

${escapeHtml(doc.title)}

${escapeHtml(brief(doc.style_summary || doc.combined_text || doc.transcript_text, 92))}

${escapeHtml(doc.source_type || "document")}学习素材
`).join("") || (works.length ? "" : `

还没有作品

先导入内容或跑一次分析任务。

`)}
${renderLastJobDetailCard()}
` ); } function renderReviewScreen() { if (!appState.dashboard) { return screenShell("发布与复盘", "先连接后端。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("复盘未加载", "登录后这里会先用最近任务生成一版复盘入口。")); } if (!backendSupports("/v2/reviews")) { return screenShell( "发布与复盘", "当前 live collector 还没有接入复盘读写接口。", `${button("去生产", "goto-production", "primary")}`, renderEmptyState("复盘能力暂未接入", "这套后端还缺 /v2/reviews,当前可以继续跑生产任务,等 live collector 同步后这里会自动切成真实复盘工作台。") ); } const project = getSelectedProject(); const completed = safeArray(appState.dashboard.recent_jobs).filter((item) => item.status === "completed").slice(0, 4); const reviews = getProjectReviews(project?.id || "").slice(0, 8); return screenShell( "发布与复盘", "先看已保存复盘,再把完成任务转成结构化复盘。", `${button("写复盘", "open-create-review")} ${button("刷新", "refresh-data")} ${button("去生产", "goto-production", "primary")}`, `

已保存复盘

当前项目的真实复盘记录
${escapeHtml(formatNumber(reviews.length))} 条
${reviews.map((review) => `

${escapeHtml(review.title)}

${escapeHtml(brief(review.highlights || review.next_actions || review.notes || "已保存复盘,待继续补充表现数据。", 92))}

${escapeHtml(platformLabel(review.platform || "douyin"))} ${escapeHtml(review.verdict || "已记录")} ${review.publish_url ? `打开链接` : ""} 编辑
`).join("") || `

还没有复盘

可以把最近完成任务直接写成一条复盘。

`}

最近完成

从完成任务继续写复盘或进入下一步生产
${completed.map((job) => `

${escapeHtml(job.title)}

${escapeHtml(brief(job.style_summary || job.transcript_text || "已完成,待补复盘。", 84))}

已完成 ${escapeHtml(job.line_type || "analysis")} ${actionTag("写复盘", "open-review-from-job", `data-job-id="${escapeHtml(job.id)}"`)} ${canDeriveAiVideo(job) ? renderPipelineJobTag("aiVideo", job, "做 AI 视频") : ""} ${canDeriveRealCut(job) ? renderPipelineJobTag("realCut", job, "做实拍剪辑") : ""} ${actionTag("看详情", "open-job-detail", `data-job-id="${escapeHtml(job.id)}"`)}
`).join("") || `

还没有完成任务

先去生产中心跑一条链路。

`}
${renderLastJobDetailCard()} ` ); } function renderCreditsScreen() { if (!appState.dashboard) { return screenShell("额度", "先连接后端。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("额度未加载", "后续接真实计费前,先用任务量做运营看板。")); } const jobs = safeArray(appState.dashboard.recent_jobs); return screenShell( "额度", "在接真实计费前,先按任务量给出运营看板。", `${button("刷新", "refresh-data")}`, `
文案消耗预估${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "analysis").length))}
分析 / 生成链路按任务数估算
封面消耗预估0
封面链待接暂未真实计费
视频消耗预估${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "ai_video" || item.line_type === "real_cut").length))}
AI 视频 / 实拍剪辑可做套餐
` ); } function renderTopbar() { const workspaceStrong = document.querySelector(".workspace-switch strong"); const workspaceSpan = document.querySelector(".workspace-switch span"); const searchInput = document.querySelector(".search input"); const avatar = document.querySelector(".avatar"); const topPills = document.querySelectorAll(".top-pill"); const platforms = document.querySelector(".topbar-left .chip-row"); const project = getSelectedProject(); if (workspaceStrong) { workspaceStrong.textContent = project?.name || (appState.session ? "已连接工作区" : "未连接工作区"); } if (workspaceSpan) { workspaceSpan.textContent = appState.dashboard ? `${safeArray(appState.dashboard.projects).length} 个项目 · ${safeArray(appState.dashboard.assistants).length} 个 Agent` : "连接后加载项目和 Agent"; } if (searchInput) { searchInput.value = ""; searchInput.placeholder = "搜项目、账号、内容、Agent"; } if (avatar) { avatar.textContent = initials(appState.me?.display_name || appState.me?.username || appState.session?.account?.display_name || "SF"); } if (topPills.length >= 3) { topPills[0].textContent = `项目 ${formatNumber(appState.dashboard?.projects?.length || 0)}`; topPills[1].textContent = `对标 ${formatNumber(appState.accounts.length)}`; topPills[2].textContent = `任务 ${formatNumber(appState.dashboard?.recent_jobs?.length || 0)}`; } if (platforms) { platforms.innerHTML = getPlatformChips().map((label, index) => `${escapeHtml(label)}`).join(""); } } function renderAll() { renderTopbar(); renderAuthUi(); screenMap.dashboard.innerHTML = renderDashboardScreen(); screenMap.intake.innerHTML = renderProjectsScreen(); screenMap.discovery.innerHTML = renderDiscoveryScreen(); screenMap.tracking.innerHTML = renderTrackingScreen(); screenMap.automation.innerHTML = renderAutomationScreen(); screenMap.owned.innerHTML = renderOwnedScreen(); screenMap.playbook.innerHTML = renderPlaybookScreen(); screenMap.production.innerHTML = renderProductionScreen(); screenMap.review.innerHTML = renderReviewScreen(); screenMap.credits.innerHTML = renderCreditsScreen(); renderOneLinerUi(); setScreen(screenMap[appState.screen] ? appState.screen : "dashboard"); } async function createProject() { if (!appState.session) { openAuthModal(); return; } const name = window.prompt("输入项目名称"); if (!name) return; const description = window.prompt("输入项目说明(可选)") || ""; setBusy(true, "正在创建项目..."); try { await storyforgeFetch("/v2/projects", { method: "POST", body: { name, description } }); await bootstrap(); } catch (error) { alert("创建项目失败: " + error.message); } finally { setBusy(false, ""); } } function openPreferredModelAction() { const models = getModelOptions(); const currentProfile = getCurrentModelProfile(); const currentId = currentProfile?.id || models[0]?.value || ""; const localCatalog = appState.localModelCatalog || {}; const gatewayModels = safeArray(localCatalog.models).map((item) => item.id).filter(Boolean); openActionModal({ title: "设置分析主模型", description: "后续导入分析、市场调研和风格学习会优先使用这里设置的模型。", submitLabel: "保存模型", fields: [ { type: "html", label: "本机模型网关", html: `

${escapeHtml(localCatalog.reachable ? "网关在线" : "网关离线")}

${escapeHtml(currentProfile ? `当前主模型:${currentProfile.name} · ${currentProfile.model_name || "-"}` : `默认模型:${localCatalog.default_model || "GLM-5"}`)}

${gatewayModels.slice(0, 6).map((model) => `${escapeHtml(model)}`).join("") || `暂未读取到模型目录`} ${localCatalog.management_url ? `打开管理页` : ""}
` }, { name: "modelProfileId", label: "主模型", type: "select", value: currentId, options: models } ], onSubmit: async (values) => { if (!values.modelProfileId) throw new Error("请先选择一个模型"); await storyforgeFetch("/v2/me/preferences/analysis-model", { method: "POST", body: { model_profile_id: values.modelProfileId } }); rememberAction("主模型已更新", "新的分析主模型已经保存。", "green"); await bootstrap(); } }); } function rememberAction(title, summary, tone = "blue", payload = null) { appState.lastAction = { title, summary, tone, payload, createdAt: new Date().toISOString() }; } function extractGeneratedCopy(payload) { const raw = payload?.content || payload?.text || payload?.copy || payload?.result?.content || ""; return brief(raw, 2400); } function renderLastActionCard() { if (!appState.lastAction) return ""; return `

最近动作

${escapeHtml(formatDateTime(appState.lastAction.createdAt))}
${escapeHtml(appState.lastAction.title)}

${escapeHtml(appState.lastAction.title)}

${escapeHtml(appState.lastAction.summary)}

`; } function renderLastJobDetailCard() { const detail = appState.lastJobDetail; if (!detail?.job) return ""; const previewLinks = getJobPreviewLinks(detail.job); return `

最近任务详情

${escapeHtml(formatDateTime(detail.job.created_at))}
${escapeHtml(detail.job.status || "-")}

${escapeHtml(detail.job.title || detail.job.id)}

${escapeHtml(brief(detail.job.style_summary || detail.job.transcript_text || detail.job.error || "暂无摘要", 120))}

${escapeHtml(detail.job.line_type || "-")} ${detail.job.status === "completed" ? actionTag("写复盘", "open-review-from-job", `data-job-id="${escapeHtml(detail.job.id)}"`) : ""} ${canDeriveAiVideo(detail.job) ? renderPipelineJobTag("aiVideo", detail.job, "做 AI 视频") : ""} ${canDeriveRealCut(detail.job) ? renderPipelineJobTag("realCut", detail.job, "做实拍剪辑") : ""} ${actionTag("看详情", "open-job-detail", `data-job-id="${escapeHtml(detail.job.id)}"`)}
${previewLinks.length ? `
${previewLinks.slice(0, 3).map((item) => `

${escapeHtml(item.label.replace(/^result\./, "").replace(/^artifacts\./, ""))}

${escapeHtml(item.url)}

`).join("")}
` : ""}
`; } function requireSelectedProject() { const project = getSelectedProject(); if (!project) throw new Error("请先创建项目"); return project; } function requireSelectedAssistant() { const assistant = getSelectedAssistant(); if (!assistant) throw new Error("请先创建 Agent"); return assistant; } function requireSelectedAccountRow() { const account = getSelectedAccount(); if (!account) throw new Error("请先在“找对标”里选中一个账号"); return account; } function openImportHomepageAction() { const project = requireSelectedProject(); const kb = getProjectKnowledgeBases(project.id)[0]; const assistants = getAssistantOptions(project.id); openActionModal({ title: "导入主页并同步", description: "适合抖音 / 小红书 / B站 / 快手 / 视频号主页。先建内容源,再触发同步与分析。", submitLabel: "开始同步", fields: [ { name: "projectId", label: "归属项目", type: "select", value: project.id, options: getProjectOptions() }, { name: "platform", label: "平台", type: "select", value: "douyin", options: getPlatformOptions() }, { name: "title", label: "标题", placeholder: "例如:创业口播对标账号" }, { name: "handle", label: "账号名 / handle", placeholder: "可选" }, { name: "sourceUrl", label: "主页链接", type: "url", placeholder: "https://..." }, { name: "assistantId", label: "绑定 Agent", type: "select", value: assistants[0]?.value || "", options: [{ value: "", label: "暂不绑定" }, ...assistants] }, { name: "maxItems", label: "最多同步作品数", type: "number", value: 5, min: 1, max: 20 } ], onSubmit: async (values) => { if (!values.sourceUrl?.trim()) throw new Error("请填写主页链接"); const projectId = values.projectId || project.id; const platform = normalizePlatformValue(values.platform, "douyin"); const source = await storyforgeFetch("/v2/content-sources", { method: "POST", body: { project_id: projectId, source_kind: "creator_account", platform, handle: values.handle || "", source_url: values.sourceUrl.trim(), title: values.title || values.handle || "主页对标", metadata: {} } }); const job = await storyforgeFetch("/v2/pipelines/content-source-sync", { method: "POST", body: { project_id: projectId, knowledge_base_id: getProjectKnowledgeBases(projectId)[0]?.id || kb?.id || "", assistant_id: values.assistantId || "", content_source_id: source.id, platform, handle: values.handle || "", source_url: values.sourceUrl.trim(), title: values.title || values.handle || "主页对标", max_items: Number(values.maxItems || 5), skip_existing: true, auto_trigger_analysis: true } }); rememberAction("主页同步已启动", `已把主页加入项目,并创建同步任务 ${job.title || job.id}。`, "blue", job); await bootstrap(); } }); } function openImportSelectedAccountAction() { const account = requireSelectedAccountRow(); const platform = getAccountPlatform(account); const project = requireSelectedProject(); const assistants = getAssistantOptions(project.id); const currentSources = getCurrentProjectSourcesForAccount(account, project.id); const currentSource = currentSources[0]; const kb = getProjectKnowledgeBases(project.id)[0]; openActionModal({ title: currentSource ? "继续同步当前对标" : "导入当前对标", description: currentSource ? "当前项目里已经有这个对标账号,继续触发同步并可切换绑定 Agent。" : "把当前选中的对标账号加入项目,并绑定 Agent 进入持续同步。", submitLabel: currentSource ? "继续同步" : "导入并同步", fields: [ { name: "projectId", label: "归属项目", type: "select", value: project.id, options: getProjectOptions() }, { name: "platform", label: "平台", type: "select", value: normalizePlatformValue(currentSource?.platform || platform), options: getPlatformOptions() }, { name: "title", label: "内容源标题", value: currentSource?.title || `${getAccountName(account)} 对标主页` }, { name: "handle", label: "账号标识", value: currentSource?.handle || getAccountHandle(account) || "" }, { name: "sourceUrl", label: "主页链接", type: "url", value: currentSource?.source_url || getAccountProfileUrl(account) || "", placeholder: "https://..." }, { name: "assistantId", label: "绑定 Agent", type: "select", value: getSelectedAssistant()?.id || assistants[0]?.value || "", options: [{ value: "", label: "暂不绑定" }, ...assistants] }, { name: "maxItems", label: "最多同步作品数", type: "number", value: Number(currentSource?.metadata?.max_items || 6), min: 1, max: 20 }, { name: "skipExisting", label: "跳过已存在作品", type: "checkbox", value: true }, { name: "autoAnalyze", label: "同步后自动分析", type: "checkbox", value: true } ], onSubmit: async (values) => { if (!values.sourceUrl?.trim()) throw new Error("请先填写主页链接"); const projectId = values.projectId || project.id; const platform = normalizePlatformValue(values.platform, "douyin"); const source = currentSource && currentSource.project_id === projectId ? currentSource : await storyforgeFetch("/v2/content-sources", { method: "POST", body: { project_id: projectId, source_kind: "creator_account", platform, handle: values.handle || "", source_url: values.sourceUrl.trim(), title: values.title || values.handle || getAccountName(account) || "对标主页", metadata: { imported_from_account_id: account.id, imported_from_workspace: "discovery" } } }); const job = await storyforgeFetch("/v2/pipelines/content-source-sync", { method: "POST", body: { project_id: projectId, knowledge_base_id: getProjectKnowledgeBases(projectId)[0]?.id || kb?.id || "", assistant_id: values.assistantId || "", content_source_id: source.id, platform, handle: values.handle || getAccountHandle(account) || "", source_url: values.sourceUrl.trim(), title: values.title || getAccountName(account) || values.handle || "对标主页", max_items: Number(values.maxItems || 6), skip_existing: Boolean(values.skipExisting), auto_trigger_analysis: Boolean(values.autoAnalyze) } }); rememberAction("对标已接入项目", `已把「${getAccountName(account) || "当前对标"}」接入项目,并创建同步任务 ${job.title || job.id}。`, "green", { source, job }); await bootstrap(); } }); } function openTrackSelectedAccountAction() { const account = requireSelectedAccountRow(); const platform = getAccountPlatform(account); const trackingAccountsPath = getWorkbenchRoute(platform, "trackingAccounts"); if (!trackingAccountsPath) { rememberAction("当前平台待接入", getPendingWorkbenchReason(platform), "orange"); renderAll(); return; } const project = requireSelectedProject(); const assistants = getAssistantOptions(project.id); const trackedItem = safeArray(appState.trackingAccounts).find((item) => item.tracked_account_id === account.id); openActionModal({ title: trackedItem ? "更新跟踪账号" : "加入跟踪", description: trackedItem ? "这个账号已经在跟踪中,可以切换负责 Agent 或补充备注。" : "把当前对标账号加入每日跟踪,后续自动生成更新日报。", submitLabel: trackedItem ? "保存跟踪" : "开始跟踪", fields: [ { name: "accountName", label: "账号", type: "html", html: `
${escapeHtml(getAccountName(account) || "未命名账号")}

${escapeHtml(getAccountProfileUrl(account) || account.signature || "")}

` }, { name: "assistantId", label: "负责 Agent", type: "select", value: trackedItem?.assistant_id || getSelectedAssistant()?.id || assistants[0]?.value || "", options: [{ value: "", label: "先不绑定" }, ...assistants] }, { name: "note", label: "跟踪备注", value: trackedItem?.note || "", placeholder: "例如:重点观察开头结构、成交句式和更新频率" } ], onSubmit: async (values) => { await storyforgeFetch(trackingAccountsPath, { method: "POST", body: { tracked_account_id: account.id, assistant_id: values.assistantId || "", note: values.note || "" } }); rememberAction(trackedItem ? "跟踪已更新" : "已加入跟踪", `账号「${getAccountName(account) || "当前对标"}」现在会进入更新日报。`, "green"); await bootstrap(); } }); } function openImportVideoLinkAction() { const project = requireSelectedProject(); const assistants = getAssistantOptions(project.id); openActionModal({ title: "导入作品链接", description: "直接把单条视频链接送进分析链。", submitLabel: "开始分析", fields: [ { name: "projectId", label: "归属项目", type: "select", value: project.id, options: getProjectOptions() }, { name: "title", label: "标题", placeholder: "可选,不填则使用默认标题" }, { name: "videoUrl", label: "作品链接", type: "url", placeholder: "https://..." }, { name: "assistantId", label: "绑定 Agent", type: "select", value: assistants[0]?.value || "", options: [{ value: "", label: "暂不绑定" }, ...assistants] }, { name: "language", label: "语言", type: "select", value: "auto", options: [{ value: "auto", label: "自动" }, { value: "zh-CN", label: "中文" }] } ], onSubmit: async (values) => { if (!values.videoUrl?.trim()) throw new Error("请填写作品链接"); const projectId = values.projectId || project.id; const job = await storyforgeFetch("/v2/explore/video-link", { method: "POST", body: { video_url: values.videoUrl.trim(), title: values.title || "", project_id: projectId, knowledge_base_id: getProjectKnowledgeBases(projectId)[0]?.id || "", assistant_id: values.assistantId || "", language: values.language || "auto" } }); rememberAction("作品分析已启动", `已创建分析任务 ${job.title || job.id}。`, "blue", job); await bootstrap(); } }); } function openImportTextAction() { const project = requireSelectedProject(); const assistants = getAssistantOptions(project.id); openActionModal({ title: "导入文本素材", description: "把口播稿、拆解稿或灵感文本直接送进知识与分析链。", submitLabel: "开始分析", fields: [ { name: "projectId", label: "归属项目", type: "select", value: project.id, options: getProjectOptions() }, { name: "title", label: "标题", placeholder: "例如:创业口播拆解" }, { name: "content", label: "正文", type: "textarea", rows: 8, placeholder: "粘贴需要分析的文本" }, { name: "assistantId", label: "绑定 Agent", type: "select", value: assistants[0]?.value || "", options: [{ value: "", label: "暂不绑定" }, ...assistants] } ], onSubmit: async (values) => { if (!values.title?.trim()) throw new Error("请填写标题"); if (!values.content?.trim()) throw new Error("请填写正文"); const projectId = values.projectId || project.id; const job = await storyforgeFetch("/v2/explore/text", { method: "POST", body: { title: values.title.trim(), content: values.content.trim(), project_id: projectId, knowledge_base_id: getProjectKnowledgeBases(projectId)[0]?.id || "", assistant_id: values.assistantId || "" } }); rememberAction("文本分析已启动", `已创建文本分析任务 ${job.title || job.id}。`, "blue", job); await bootstrap(); } }); } function openUploadVideoAction() { const project = requireSelectedProject(); const assistants = getAssistantOptions(project.id); openActionModal({ title: "上传本地视频", description: "上传本地素材,直接进入分析链。", submitLabel: "上传并分析", fields: [ { name: "projectId", label: "归属项目", type: "select", value: project.id, options: getProjectOptions() }, { name: "title", label: "标题", placeholder: "可选,不填则用文件名" }, { name: "assistantId", label: "绑定 Agent", type: "select", value: assistants[0]?.value || "", options: [{ value: "", label: "暂不绑定" }, ...assistants] }, { name: "file", label: "本地视频", type: "file", accept: ".mp4,.mov,.m4v,.avi,.mkv,.webm" } ], onSubmit: async (values) => { if (!values.file) throw new Error("请先选择本地视频"); const projectId = values.projectId || project.id; const form = new FormData(); form.append("file", values.file); form.append("title", values.title || ""); form.append("project_id", projectId); form.append("knowledge_base_id", getProjectKnowledgeBases(projectId)[0]?.id || ""); form.append("assistant_id", values.assistantId || ""); const job = await storyforgeFetch("/v2/explore/upload-video", { method: "POST", body: form }); rememberAction("上传分析已启动", `已上传素材并创建任务 ${job.title || job.id}。`, "blue", job); await bootstrap(); } }); } function openOneLinerProfileAction() { const project = requireSelectedProject(); const assistants = getAssistantOptions(project.id); const profile = appState.onelinerProfile || {}; openActionModal({ title: "配置 OneLiner", description: "绑定总控主 Agent 的默认平台、长期目标和默认执行 Agent。", submitLabel: "保存配置", fields: [ { name: "assistantId", label: "默认执行 Agent", type: "select", value: profile.assistant_id || getSelectedAssistant()?.id || assistants[0]?.value || "", options: [{ value: "", label: "先不绑定" }, ...assistants] }, { name: "displayName", label: "显示名", value: profile.display_name || "OneLiner", placeholder: "例如:增长总控 OneLiner" }, { name: "defaultPlatform", label: "默认平台", type: "select", value: normalizePlatformValue(profile.default_platform || getPreferredPlatform(), "douyin"), options: getPlatformOptions() }, { name: "longTermGoal", label: "长期目标", type: "textarea", rows: 4, value: profile.long_term_goal || "", placeholder: "例如:围绕创业 IP 做跨平台增长与成交转化" }, { name: "notes", label: "补充说明", type: "textarea", rows: 4, value: profile.notes || "", placeholder: "例如:前端没产品化的需求先由 OneLiner 承接,不允许直接改核心代码" } ], onSubmit: async (values) => { const saved = await storyforgeFetch("/v2/oneliner/profile", { method: "PUT", body: { project_id: project.id, assistant_id: values.assistantId || "", display_name: values.displayName || "OneLiner", default_platform: values.defaultPlatform || "douyin", long_term_goal: values.longTermGoal || "", notes: values.notes || "", config: { chat_only_for_unreleased_ui: true, commercial_ready: true, tenant_isolation_required: true } } }); appState.onelinerProfile = saved; rememberAction("OneLiner 已保存", `已更新 OneLiner「${saved.display_name || "OneLiner"}」配置。`, "green", saved); renderAll(); } }); } function openPlatformAgentProfileAction(platform) { const project = requireSelectedProject(); const agents = safeArray(appState.platformAgents); const current = agents.find((item) => item.platform === platform) || {}; const assistants = getAssistantOptions(project.id); openActionModal({ title: `配置 ${platformLabel(platform)} Agent`, description: "给这个平台绑定自己的执行 Agent,并补充任务目标和方法论定位。", submitLabel: "保存平台 Agent", fields: [ { name: "assistantId", label: "绑定执行 Agent", type: "select", value: current.assistant_id || assistants[0]?.value || "", options: [{ value: "", label: "先不绑定" }, ...assistants] }, { name: "name", label: "名称", value: current.name || `${platformLabel(platform)} Agent`, placeholder: "例如:快手增长 Agent" }, { name: "mission", label: "任务目标", type: "textarea", rows: 4, value: current.mission || "", placeholder: "例如:沉淀快手平台的开场结构、停留逻辑和转化方法论" }, { name: "notes", label: "补充说明", type: "textarea", rows: 4, value: current.notes || "", placeholder: "例如:优先观察短句节奏、直播切片和成交句式" }, { name: "status", label: "状态", type: "select", value: current.status || "active", options: [{ value: "active", label: "启用" }, { value: "draft", label: "草稿" }, { value: "paused", label: "暂停" }] } ], onSubmit: async (values) => { const saved = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(platform)}/profile`, { method: "PUT", body: { project_id: project.id, assistant_id: values.assistantId || "", name: values.name || `${platformLabel(platform)} Agent`, mission: values.mission || "", notes: values.notes || "", status: values.status || "active", config: { self_optimize: true, tenant_scoped_memory: true, ui_escalation_via_oneliner: true } } }); appState.platformAgents = safeArray(appState.platformAgents).filter((item) => item.platform !== platform).concat(saved).sort((a, b) => String(a.platform).localeCompare(String(b.platform))); rememberAction("平台 Agent 已保存", `已更新 ${platformLabel(platform)} Agent。`, "green", saved); renderAll(); } }); } function openPlatformAgentMemoryAction(platform) { const project = requireSelectedProject(); openActionModal({ title: `补充 ${platformLabel(platform)} Agent 记忆`, description: "把当前阶段已经验证有效的平台方法、结论或注意事项沉淀为租户级长期记忆。", submitLabel: "保存记忆", fields: [ { name: "memoryKey", label: "记忆键", value: "lesson.current", placeholder: "例如:hook.pattern.v1" }, { name: "title", label: "标题", placeholder: "例如:快手直播切片更吃冲突前置" }, { name: "summary", label: "摘要", type: "textarea", rows: 4, placeholder: "写清楚这条记忆的结论和适用场景" } ], onSubmit: async (values) => { if (!values.memoryKey?.trim()) throw new Error("请填写记忆键"); if (!values.summary?.trim()) throw new Error("请填写记忆摘要"); const saved = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(platform)}/memories`, { method: "POST", body: { project_id: project.id, memory_key: values.memoryKey.trim(), title: values.title || values.memoryKey.trim(), summary: values.summary.trim(), subject_type: "project", subject_id: project.id, details: { source: "manual-ui", platform, captured_at: new Date().toISOString() }, confidence: 0.82 } }); rememberAction("平台记忆已保存", `已把这条方法沉淀到 ${platformLabel(platform)} Agent 记忆中。`, "green", saved); await loadAgentControlSurfaces(project.id); renderAll(); } }); } function openPlatformAgentSkillAction(platform) { const project = requireSelectedProject(); openActionModal({ title: `补充 ${platformLabel(platform)} Agent 技能`, description: "把子 Agent 当前阶段验证过的方法论固化成可复用技能,并保留测试规范。", submitLabel: "保存技能", fields: [ { name: "skillKey", label: "技能键", value: "skill.current", placeholder: "例如:crawler.profile.dom.v2" }, { name: "name", label: "名称", placeholder: "例如:主页结构适配技能" }, { name: "status", label: "状态", type: "select", value: "validated", options: [{ value: "draft", label: "草稿" }, { value: "validated", label: "已验证" }, { value: "paused", label: "暂停" }] }, { name: "method", label: "方法摘要", type: "textarea", rows: 4, placeholder: "写清楚当前方法是怎么拿到结果的" }, { name: "testSpec", label: "验收标准", type: "textarea", rows: 4, placeholder: "例如:主页抓取成功率 >= 95%,作品标题和发布时间都齐全" } ], onSubmit: async (values) => { if (!values.skillKey?.trim()) throw new Error("请填写技能键"); if (!values.name?.trim()) throw new Error("请填写技能名称"); const saved = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(platform)}/skills`, { method: "POST", body: { project_id: project.id, skill_key: values.skillKey.trim(), name: values.name.trim(), status: values.status || "validated", method: { summary: values.method || "" }, test_spec: { summary: values.testSpec || "" }, last_result: { source: "manual-ui" }, success_count: 1, failure_count: 0, last_score: 0.9 } }); rememberAction("平台技能已保存", `已把方法固化到 ${platformLabel(platform)} Agent 技能中。`, "green", saved); await loadAgentControlSurfaces(project.id); renderAll(); } }); } async function openPlatformAgentDetailAction(platform) { const project = requireSelectedProject(); const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform()); const profile = safeArray(appState.platformAgents).find((item) => item.platform === normalizedPlatform) || null; if (!profile) { alert("没有找到这个平台 Agent。"); return; } const [memoriesPayload, skillsPayload] = await Promise.all([ storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/memories?project_id=${encodeURIComponent(project.id)}`).catch(() => ({ items: [] })), storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/skills?project_id=${encodeURIComponent(project.id)}`).catch(() => ({ items: [] })) ]); const memories = safeArray(memoriesPayload?.items || memoriesPayload).slice(0, 6); const skills = safeArray(skillsPayload?.items || skillsPayload).slice(0, 6); const skillVersionEntries = await Promise.all( skills.map(async (item) => { if (!backendSupports("/v2/platform-agents/{platform}/skills/{skill_id}/versions")) { return [item.id, []]; } const payload = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/skills/${encodeURIComponent(item.id)}/versions?project_id=${encodeURIComponent(project.id)}`).catch(() => ({ items: [] })); return [item.id, safeArray(payload?.items || payload).slice(0, 3)]; }) ); const skillVersions = Object.fromEntries(skillVersionEntries); openActionModal({ title: `${platformLabel(normalizedPlatform)} Agent 详情`, description: "查看当前平台 Agent 最近沉淀的记忆、技能和就绪度。", hideSubmit: true, fields: [ { type: "html", label: "详情", html: `

${escapeHtml(profile.name || `${platformLabel(normalizedPlatform)} Agent`)}

${escapeHtml(profile.mission || profile.notes || "暂无任务目标说明")}

${escapeHtml(profile.status || "draft")} ${profile.readiness_label ? `= 50 ? "blue" : "orange"}">${escapeHtml(profile.readiness_label)} ${escapeHtml(formatNumber(profile.readiness_score || 0))}` : ""} ${escapeHtml(profile.assistant?.name || "未绑执行 Agent")}
最近记忆
${memories.map((item) => `

${escapeHtml(item.title || item.memory_key || "未命名")}

${escapeHtml(item.summary || "暂无摘要")}

${escapeHtml(item.memory_key || "memory")}${escapeHtml(formatNumber(item.confidence || 0))}
`).join("") || `

还没有平台记忆

先把这段时间验证有效的方法沉淀进来。

`}
最近技能
${skills.map((item) => `

${escapeHtml(item.name || item.skill_key || "未命名")}

${escapeHtml(item.test_spec?.summary || item.method?.summary || "暂无方法摘要")}

${escapeHtml(item.status || "draft")} 得分 ${escapeHtml(formatNumber(item.last_score || 0))} 验收通过 标记待优化
${safeArray(skillVersions[item.id]).length ? `
${safeArray(skillVersions[item.id]).map((version, index) => ` ${escapeHtml(`v${formatNumber(version.version_no || 0)} · ${version.snapshot_reason || "snapshot"}`)} `).join("")}
` : ""}
`).join("") || `

还没有平台技能

等子 Agent 跑出稳定结果后,把方法固化成技能。

`}
运行平台自检 编辑配置 继续补记忆 继续补技能 让 OneLiner 调度
` } ] }); } function openPlatformSkillReviewAction(platform, skillId, accepted) { const project = requireSelectedProject(); const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform()); const profile = safeArray(appState.platformAgents).find((item) => item.platform === normalizedPlatform) || null; const skill = safeArray(profile?.recent_skill ? [profile.recent_skill] : []) .concat([]) .find((item) => item.id === skillId) || null; openActionModal({ title: accepted ? "验收平台技能" : "标记技能待优化", description: accepted ? `把这条 ${platformLabel(normalizedPlatform)} 技能标记为当前可复用的方法。` : `这条 ${platformLabel(normalizedPlatform)} 技能暂时不通过,要求继续优化。`, submitLabel: accepted ? "确认通过" : "确认待优化", fields: [ { name: "summary", label: "结论摘要", placeholder: accepted ? "例如:当前抓取结果和验收数据一致,可固化成技能" : "例如:账号匹配不稳定,需要继续优化抓取方式" }, { name: "reviewNotes", label: "审计备注", type: "textarea", rows: 4, value: skill?.last_result?.review_notes || "", placeholder: "写清楚为什么通过或退回" }, { name: "score", label: "得分", type: "number", value: accepted ? 0.9 : 0.45, min: 0, max: 1, step: 0.05 } ], onSubmit: async (values) => { if (!backendSupports("/v2/platform-agents/{platform}/skills/{skill_id}/review")) { throw new Error("当前后端还没有接入平台技能验收接口。"); } const saved = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/skills/${encodeURIComponent(skillId)}/review`, { method: "POST", body: { project_id: project.id, accepted, score: Number(values.score || (accepted ? 0.9 : 0.45)), summary: values.summary || "", review_notes: values.reviewNotes || "" } }); rememberAction( accepted ? "平台技能已通过" : "平台技能待优化", `技能「${saved.name || saved.skill_key || skillId}」已更新为 ${saved.status || (accepted ? "validated" : "needs_revision")}。`, accepted ? "green" : "orange", saved ); await loadAgentControlSurfaces(project.id); renderAll(); } }); } function openPlatformSkillRollbackAction(platform, skillId, versionId) { const project = requireSelectedProject(); const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform()); openActionModal({ title: "回滚平台技能", description: "把当前技能回退到旧版本,并保留新的回滚快照,方便继续追踪。", submitLabel: "确认回滚", fields: [ { name: "summary", label: "回滚说明", type: "html", html: `

${escapeHtml(platformLabel(normalizedPlatform))} 技能回滚

${escapeHtml(`将 skill ${skillId} 回滚到版本 ${versionId}。`)}

` } ], onSubmit: async () => { const payload = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/skills/${encodeURIComponent(skillId)}/rollback`, { method: "POST", body: { project_id: project.id, version_id: versionId } }); rememberAction("技能已回滚", `已回滚到版本 ${payload.rollback_from_version?.version_no || "指定版本"}。`, "green", payload); await loadAgentControlSurfaces(project.id); renderAll(); } }); } function openActionRegistryEditAction(actionKey) { const project = requireSelectedProject(); const actionDef = safeArray(appState.onelinerActionRegistry).find((item) => item.action_key === actionKey) || null; if (!actionDef) { alert("没有找到这条动作定义。"); return; } openActionModal({ title: "编辑 OneLiner 动作", description: "在租户范围内控制动作名称、说明、开关和少量配置。", submitLabel: "保存动作", fields: [ { name: "label", label: "动作名称", value: actionDef.label || "" }, { name: "description", label: "动作说明", type: "textarea", rows: 4, value: actionDef.description || "" }, { name: "status", label: "状态", type: "select", value: actionDef.status || "enabled", options: [{ value: "enabled", label: "启用" }, { value: "disabled", label: "禁用" }] }, { name: "configJson", label: "配置 JSON", type: "textarea", rows: 5, value: JSON.stringify(actionDef.config || {}, null, 2) } ], onSubmit: async (values) => { let config = {}; if (String(values.configJson || "").trim()) { config = JSON.parse(values.configJson); } const saved = await storyforgeFetch(`/v2/oneliner/action-registry/${encodeURIComponent(actionKey)}?project_id=${encodeURIComponent(project.id)}`, { method: "PUT", body: { label: values.label || "", description: values.description || "", category: actionDef.category || "custom", status: values.status || "enabled", config } }); rememberAction("动作已更新", `OneLiner 动作「${saved.label || saved.action_key}」已保存。`, "green", saved); await loadAgentControlSurfaces(project.id); renderAll(); } }); } function openTenantQuotaAction() { const project = requireSelectedProject(); const quota = appState.tenantQuota || {}; openActionModal({ title: "编辑租户额度", description: "当前额度按租户 + 项目隔离,用于商业化预算、动作配额和存储保护。", submitLabel: "保存额度", fields: [ { name: "enabled", label: "启用额度保护", type: "checkbox", value: quota.enabled !== false }, { name: "monthlyBudgetCents", label: "月预算(分)", type: "number", value: quota.monthly_budget_cents || 0, min: 0 }, { name: "storageLimitBytes", label: "存储上限(字节)", type: "number", value: quota.storage_limit_bytes || 0, min: 0 }, { name: "analysisQuota", label: "分析配额", type: "number", value: quota.analysis_quota || 0, min: 0 }, { name: "copyQuota", label: "文案配额", type: "number", value: quota.copy_quota || 0, min: 0 }, { name: "aiVideoQuota", label: "AI 视频配额", type: "number", value: quota.ai_video_quota || 0, min: 0 }, { name: "realCutQuota", label: "实拍剪辑配额", type: "number", value: quota.real_cut_quota || 0, min: 0 }, { name: "recorderQuota", label: "录制配额", type: "number", value: quota.recorder_quota || 0, min: 0 } ], onSubmit: async (values) => { const saved = await storyforgeFetch(`/v2/tenant/quota?project_id=${encodeURIComponent(project.id)}`, { method: "PUT", body: { enabled: Boolean(values.enabled), monthly_budget_cents: Number(values.monthlyBudgetCents || 0), storage_limit_bytes: Number(values.storageLimitBytes || 0), analysis_quota: Number(values.analysisQuota || 0), copy_quota: Number(values.copyQuota || 0), ai_video_quota: Number(values.aiVideoQuota || 0), real_cut_quota: Number(values.realCutQuota || 0), recorder_quota: Number(values.recorderQuota || 0), config: quota.config || {} } }); rememberAction("租户额度已更新", "当前项目的预算与配额已经保存。", "green", saved); await loadAgentControlSurfaces(project.id); renderAll(); } }); } function openCreateAssistantAction() { const project = requireSelectedProject(); const kbOptions = getKnowledgeBaseOptions(project.id); const modelOptions = getModelOptions(); openActionModal({ title: "创建 Agent", description: "先定义用途、平台与目标,再让 Agent 学习内容。", submitLabel: "创建 Agent", fields: [ { name: "projectId", label: "归属项目", type: "select", value: project.id, options: getProjectOptions() }, { name: "name", label: "名称", placeholder: "例如:创业成交助手" }, { name: "description", label: "说明", placeholder: "例如:服务创业 IP 与成交型短视频" }, { name: "goal", label: "生成目标", placeholder: "例如:输出创业口播、对标拆解和成交文案" }, { name: "systemPrompt", label: "系统提示词", type: "textarea", rows: 5, placeholder: "可选,不填则后续再补" }, { name: "knowledgeBaseId", label: "默认知识库", type: "select", value: kbOptions[0]?.value || "", options: [{ value: "", label: "暂不绑定" }, ...kbOptions] }, { name: "modelProfileId", label: "主模型", type: "select", value: modelOptions.find((item) => item.value === safeArray(appState.dashboard?.model_profiles).find((m) => m.is_default)?.id)?.value || modelOptions[0]?.value || "", options: modelOptions } ], onSubmit: async (values) => { if (!values.name?.trim()) throw new Error("请填写 Agent 名称"); const projectId = values.projectId || project.id; const assistant = await storyforgeFetch("/v2/assistants", { method: "POST", body: { project_id: projectId, name: values.name.trim(), description: values.description || "", generation_goal: values.goal || "", system_prompt: values.systemPrompt || "", knowledge_base_ids: values.knowledgeBaseId ? [values.knowledgeBaseId] : [], model_profile_id: values.modelProfileId || "" } }); appState.selectedAssistantId = assistant.id; rememberAction("Agent 已创建", `已创建 Agent「${assistant.name}」。`, "green", assistant); await bootstrap(); } }); } function openEditAssistantAction(assistantId = "") { const assistant = safeArray(appState.dashboard?.assistants).find((item) => item.id === assistantId) || getSelectedAssistant(); if (!assistant) { alert("请先选择一个 Agent"); return; } const modelOptions = getModelOptions(); openActionModal({ title: "编辑 Agent", description: "更新当前 Agent 的名称、目标和主模型,不会影响已完成任务。", submitLabel: "保存 Agent", fields: [ { name: "name", label: "名称", value: assistant.name || "", placeholder: "例如:创业成交助手" }, { name: "description", label: "说明", value: assistant.description || "", placeholder: "例如:服务创业 IP 与成交型短视频" }, { name: "goal", label: "生成目标", value: assistant.generation_goal || "", placeholder: "例如:输出创业口播、对标拆解和成交文案" }, { name: "systemPrompt", label: "系统提示词", type: "textarea", rows: 5, value: assistant.system_prompt || "", placeholder: "可选,不填则后续再补" }, { name: "modelProfileId", label: "主模型", type: "select", value: assistant.model_profile_id || modelOptions[0]?.value || "", options: modelOptions } ], onSubmit: async (values) => { if (!values.name?.trim()) throw new Error("请填写 Agent 名称"); const updated = await storyforgeFetch(`/v2/assistants/${encodeURIComponent(assistant.id)}`, { method: "PATCH", body: { name: values.name.trim(), description: values.description || "", generation_goal: values.goal || "", system_prompt: values.systemPrompt || "", model_profile_id: values.modelProfileId || "" } }); appState.selectedAssistantId = updated.id; rememberAction("Agent 已更新", `已更新 Agent「${updated.name}」。`, "green", updated); await bootstrap(); } }); } function openAnalyzeSelectedAccountAction() { const account = requireSelectedAccountRow(); const platform = getAccountPlatform(account); const analyzePath = getWorkbenchRoute(platform, "analyzeAccount", account.id); if (!analyzePath) { rememberAction("当前平台待接入", getPendingWorkbenchReason(platform), "orange"); renderAll(); return; } openActionModal({ title: "分析当前对标账号", description: "从商业化和内容运营角度重跑一次账号分析。", submitLabel: "开始分析", fields: [ { name: "maxVideos", label: "纳入分析作品数", type: "number", value: 6, min: 3, max: 20 }, { name: "extraFocus", label: "额外关注点", type: "textarea", rows: 4, placeholder: "例如:更关注商业化承接与私域转化" }, { name: "autoAnalyzeTopVideos", label: "分析后自动补高分作品", type: "checkbox", value: true }, { name: "topVideoCount", label: "高分作品分析数", type: "number", value: 4, min: 1, max: 10 } ], onSubmit: async (values) => { const result = await storyforgeFetch(analyzePath, { method: "POST", body: { model_profile_ids: [], linked_account_ids: [], include_linked_accounts: true, include_recent_similar_candidates: true, max_videos: Number(values.maxVideos || 6), extra_focus: values.extraFocus || "", temperature: 0.35, auto_analyze_top_videos: Boolean(values.autoAnalyzeTopVideos), top_video_analysis_count: Number(values.topVideoCount || 4) } }); const summary = result?.suggestions?.[0]?.parsed_json?.executive_summary || result?.suggestions?.[0]?.suggestion_text || "已生成新的账号分析。"; rememberAction("对标账号分析完成", brief(summary, 120), "green", result); await loadPlatformAccount(platform, account.id); renderAll(); } }); } function openAnalyzeTopVideosAction() { const account = requireSelectedAccountRow(); const platform = getAccountPlatform(account); const analyzePath = getWorkbenchRoute(platform, "analyzeTopVideos", account.id); if (!analyzePath || !backendSupports(`/v2/${platform}/accounts/{account_id}/videos/analyze-top`)) { rememberAction("当前后端暂不支持", "这套 live collector 还没有接入高分作品批量分析。", "orange"); renderAll(); return; } openActionModal({ title: "分析高分作品", description: "对当前对标账号的高分作品批量补分析。", submitLabel: "开始分析", fields: [ { name: "topVideoCount", label: "分析作品数", type: "number", value: 5, min: 1, max: 12 }, { name: "minScore", label: "最低分阈值", type: "number", value: 45, min: 0, max: 100 } ], onSubmit: async (values) => { const result = await storyforgeFetch(analyzePath, { method: "POST", body: { model_profile_id: "", top_video_count: Number(values.topVideoCount || 5), min_score: Number(values.minScore || 45), temperature: 0.25 } }); rememberAction("高分作品分析完成", `已补分析 ${formatNumber(result.analyzed_count)} 条高分作品。`, "green", result); await loadPlatformAccount(platform, account.id); renderAll(); } }); } function openSimilaritySearchAction() { const account = requireSelectedAccountRow(); const platform = getAccountPlatform(account); const createPath = getWorkbenchRoute(platform, "similarSearches"); if (!createPath) { rememberAction("当前平台待接入", getPendingWorkbenchReason(platform), "orange"); renderAll(); return; } openActionModal({ title: "查相似账号", description: "让 Agent 基于当前账号画像找更多可借鉴对象。", submitLabel: "开始查找", fields: [ { name: "maxCandidates", label: "最多候选数", type: "number", value: 8, min: 3, max: 20 }, { name: "extraRequirements", label: "额外要求", type: "textarea", rows: 4, placeholder: "例如:优先找创业成交类、口播结构强的账号" } ], onSubmit: async (values) => { const created = await storyforgeFetch(createPath, { method: "POST", body: { source_account_id: account.id, candidate_urls: [], seed_linked_accounts: true, search_public_pages: true, model_profile_id: "", max_candidates: Number(values.maxCandidates || 8), extra_requirements: values.extraRequirements || "" } }); const searchId = created.id || created.search_id; const detailPath = searchId ? getWorkbenchRoute(platform, "similarSearchDetail", searchId) : ""; const detail = searchId ? await storyforgeFetch(detailPath) : created; appState.lastSimilaritySearch = detail; rememberAction("相似账号已生成", `已生成 ${formatNumber(safeArray(detail.candidates).length)} 个候选账号。`, "green", detail); await loadPlatformAccount(platform, account.id); renderAll(); } }); } function openBenchmarkLinkAction(defaults = {}) { const account = requireSelectedAccountRow(); const platform = getAccountPlatform(account); const benchmarkPath = getWorkbenchRoute(platform, "benchmarkLinks", account.id); if (!benchmarkPath) { rememberAction("当前平台待接入", getPendingWorkbenchReason(platform), "orange"); renderAll(); return; } const options = safeArray(appState.accounts) .filter((item) => item.id !== account.id) .map((item) => ({ value: item.id, label: getAccountName(item) || item.id })); const candidate = typeof defaults.candidateIndex === "number" ? safeArray(appState.lastSimilaritySearch?.candidates)[defaults.candidateIndex] || null : null; openActionModal({ title: "保存对标关系", description: "把当前账号和另一个账号关联成对标关系,便于后续持续跟踪。", submitLabel: "保存关系", fields: [ { name: "targetAccountId", label: "目标账号", type: "select", value: defaults.targetAccountId || candidate?.candidate_account_id || options[0]?.value || "", options: [{ value: "", label: "仅保存主页链接" }, ...options] }, { name: "targetProfileUrl", label: "目标主页链接", type: "url", value: defaults.targetProfileUrl || candidate?.candidate_profile_url || "", placeholder: "没有本地账号时可直接保存主页链接" }, { name: "relationType", label: "关系类型", type: "select", value: "benchmark", options: [ { value: "benchmark", label: "对标" }, { value: "learn", label: "学习" }, { value: "watch", label: "跟踪" } ] }, { name: "note", label: "备注", value: defaults.note || brief(candidate?.rationale_text || "", 120), placeholder: "例如:开场结构很强,适合持续跟踪" } ], onSubmit: async (values) => { if (!values.targetAccountId && !values.targetProfileUrl?.trim()) throw new Error("请先选择一个目标账号或填写主页链接"); const result = await storyforgeFetch(benchmarkPath, { method: "POST", body: { target_account_ids: values.targetAccountId ? [values.targetAccountId] : [], target_profile_urls: values.targetAccountId ? [] : [values.targetProfileUrl.trim()], relation_type: values.relationType || "benchmark", note: values.note || "", search_id: appState.lastSimilaritySearch?.id || "" } }); if (candidate) { markSavedCandidate(candidate, result.links); } else if (appState.selectedWorkspace) { appState.selectedWorkspace = { ...appState.selectedWorkspace, linked_accounts: safeArray(result.links) }; } rememberAction("对标关系已保存", "当前账号的对标关系已更新。", "green"); renderAll(); } }); } async function scanAdminOpsAction() { if (!isSuperAdmin()) throw new Error("只有平台管理者才能调用运维 Agent。"); setBusy(true, "运维 Agent 正在扫描故障事件..."); try { const payload = await storyforgeFetch("/v2/admin/ops/incidents/scan", { method: "POST", body: {} }); rememberAction("运维扫描已完成", `本轮共归集 ${formatNumber(payload.count)} 条故障事件。`, payload.count ? "orange" : "green", payload); await loadAgentControlSurfaces(getOneLinerProjectId()); } finally { setBusy(false, ""); renderAll(); } } function openAdminIncidentReviewAction(incidentId) { if (!isSuperAdmin()) { alert("只有平台管理者才能审计处理故障事件。"); return; } const incident = safeArray(appState.adminOpsOverview?.incidents).find((item) => item.id === incidentId); if (!incident) { alert("没有找到这条故障事件。"); return; } openActionModal({ title: "审计处理故障事件", description: "这里代表管理员侧审计 Agent 的放行/退回动作。", submitLabel: "保存审计结果", fields: [ { name: "summary", label: "事件摘要", type: "html", html: `

${escapeHtml(incident.title)}

${escapeHtml(incident.summary || "暂无摘要")}

${escapeHtml(incident.severity || "warn")} ${escapeHtml(incident.status || "open")} ${incident.source_type ? `${escapeHtml(incident.source_type)}` : ""} ${incident.tenant_user_id ? `租户 ${escapeHtml(brief(incident.tenant_user_id, 12))}` : ""}
${incident.source_type === "job" ? actionTag("看任务详情", "open-job-detail", `data-job-id="${escapeHtml(incident.source_id || "")}"`) : ""} ${incident.source_type === "integration" ? actionTag("去自动流程", "goto-automation") : ""} ${incident.tenant_project_id ? actionTag("去生产中心", "goto-production") : ""} 重新扫描
` }, { name: "status", label: "处理状态", type: "select", value: incident.status || "reviewed", options: [{ value: "reviewed", label: "已审阅" }, { value: "watching", label: "继续观察" }, { value: "resolved", label: "已解决" }, { value: "rejected", label: "驳回修复方案" }] }, { name: "reviewNotes", label: "审计备注", type: "textarea", rows: 5, value: incident.review_notes || "", placeholder: "写清楚为什么放行、退回或继续观察" } ], onSubmit: async (values) => { const saved = await storyforgeFetch(`/v2/admin/ops/incidents/${encodeURIComponent(incident.id)}`, { method: "PATCH", body: { status: values.status || "reviewed", review_notes: values.reviewNotes || "" } }); rememberAction("审计结果已保存", `事件「${saved.title}」已更新为 ${saved.status}。`, "green", saved); await loadAgentControlSurfaces(getOneLinerProjectId()); renderAll(); } }); } function openAdminRepairPlanAction(incidentId) { if (!isSuperAdmin()) { alert("只有平台管理者才能生成修复计划。"); return; } const incident = safeArray(appState.adminOpsOverview?.incidents).find((item) => item.id === incidentId); if (!incident) { alert("没有找到这条故障事件。"); return; } openActionModal({ title: "生成修复计划", description: "让运维 Agent 先生成一版 repair plan,再由审计 Agent 决定是否放行。", submitLabel: "生成计划", fields: [ { name: "scope", label: "计划范围", type: "select", value: "plan", options: [{ value: "plan", label: "标准计划" }, { value: "hotfix", label: "热修建议" }, { value: "watch", label: "仅观察" }] }, { name: "notes", label: "附加说明", type: "textarea", rows: 4, placeholder: "例如:优先验证 cutvideo 上传链,不要动核心代码" } ], onSubmit: async (values) => { const saved = await storyforgeFetch(`/v2/admin/ops/incidents/${encodeURIComponent(incidentId)}/repair-plan`, { method: "POST", body: { incident_id: incidentId, scope: values.scope || "plan", notes: values.notes || "" } }); rememberAction("修复计划已生成", `已为事件「${incident.title}」生成 repair plan。`, "green", saved); await loadAgentControlSurfaces(getOneLinerProjectId()); renderAll(); } }); } function openAdminFixRunAuditAction(runId) { if (!isSuperAdmin()) { alert("只有平台管理者才能审计修复计划。"); return; } const run = safeArray(appState.adminOpsOverview?.recent_fix_runs).find((item) => item.id === runId); if (!run) { alert("没有找到这条修复计划。"); return; } openActionModal({ title: "审计修复计划", description: "审计 Agent 只做放行、驳回或继续观察,不会直接让用户一句话改核心代码。", submitLabel: "保存审计", fields: [ { name: "summary", label: "计划摘要", type: "html", html: `

${escapeHtml(run.plan?.summary || run.id)}

${escapeHtml((run.plan?.steps || []).join(";") || "暂无步骤")}

` }, { name: "reviewStatus", label: "审计状态", type: "select", value: run.audit_status || "approved", options: [{ value: "approved", label: "通过" }, { value: "watching", label: "继续观察" }, { value: "rejected", label: "驳回" }] }, { name: "reviewNotes", label: "审计备注", type: "textarea", rows: 4, value: run.review_notes || "", placeholder: "写清楚为什么通过、驳回或继续观察" } ], onSubmit: async (values) => { const saved = await storyforgeFetch(`/v2/admin/ops/fix-runs/${encodeURIComponent(runId)}/audit`, { method: "POST", body: { review_status: values.reviewStatus || "approved", review_notes: values.reviewNotes || "" } }); rememberAction("修复计划已审计", `修复计划 ${runId} 已更新为 ${saved.audit_status || values.reviewStatus}。`, "green", saved); await loadAgentControlSurfaces(getOneLinerProjectId()); renderAll(); } }); } function openJobDetailAction(jobId) { if (!jobId) return; setBusy(true, "正在加载任务详情..."); loadJobDetail(jobId) .then(({ job, events, childJobs }) => { const artifacts = JSON.stringify(job.artifacts || {}, null, 2); const result = JSON.stringify(job.result || {}, null, 2); const previewLinks = getJobPreviewLinks(job); openActionModal({ title: job.title || "任务详情", description: `状态:${job.status || "-"} · 类型:${job.line_type || job.source_type || "-"}`, hideSubmit: true, fields: [ { type: "html", label: "任务摘要", html: `
任务 ID${escapeHtml(job.id)}
状态${escapeHtml(job.status || "-")}
链路${escapeHtml(job.line_type || "-")}
创建时间${escapeHtml(formatDateTime(job.created_at))}
` }, { type: "html", label: "事件时间线", html: `
${safeArray(events).slice(-6).map((event) => `

${escapeHtml(event.event_type || "event")}

${escapeHtml(brief(event.message || JSON.stringify(event.payload || {}), 120))}

`).join("") || `

暂无事件

当前任务还没有可显示的事件。

`}
` }, { type: "html", label: "结果预览", html: `
${previewLinks.map((item) => `

${escapeHtml(item.label.replace(/^result\./, "").replace(/^artifacts\./, ""))}

${escapeHtml(item.url)}

`).join("") || `

暂无外部结果链接

当前先保留 artifacts 和 result 原始数据供查看。

`}
` }, { type: "html", label: "下一步动作", html: `
${canDeriveAiVideo(job) ? renderPipelineJobTag("aiVideo", job, "继续做 AI 视频") : ""} ${canDeriveRealCut(job) ? renderPipelineJobTag("realCut", job, "继续做实拍剪辑") : ""} ${actionTag("用摘要写文案", "job-to-generate-copy", `data-job-id="${escapeHtml(job.id)}"`)}
` }, { type: "html", label: "子任务", html: `
${safeArray(childJobs).slice(0, 6).map((item) => `

${escapeHtml(item.title || item.id)}

${escapeHtml(brief(item.style_summary || item.transcript_text || item.error || "暂无摘要", 96))}

${escapeHtml(item.status || "-")} ${escapeHtml(item.line_type || "-")} 看详情
`).join("") || `

暂无子任务

当前任务还没有派生出下一层任务。

`}
` }, { type: "textarea", name: "artifactsReadonly", label: "Artifacts", value: artifacts, rows: 8 }, { type: "textarea", name: "resultReadonly", label: "Result", value: result, rows: 8 } ] }); document.querySelector('[data-action-field="artifactsReadonly"]')?.setAttribute("readonly", "readonly"); document.querySelector('[data-action-field="resultReadonly"]')?.setAttribute("readonly", "readonly"); }) .catch((error) => { alert("加载任务详情失败: " + error.message); }) .finally(() => { setBusy(false, ""); }); } function openGenerateCopyAction(defaults = {}) { const assistant = getSelectedAssistant() || requireSelectedAssistant(); const sourceJob = defaults.sourceJob || null; openActionModal({ title: "生成文案", description: "用当前 Agent 和知识库生成一版短视频文案。", submitLabel: "开始生成", fields: [ { name: "brief", label: "创作需求", type: "textarea", rows: 5, value: defaults.brief || getJobSeedBrief(sourceJob), placeholder: "例如:给创业者写一条 60 字内的短视频开场文案" }, { name: "platform", label: "平台", type: "select", value: normalizePlatformValue(defaults.platform || "douyin"), options: getPlatformOptions() }, { name: "audience", label: "受众", value: "创业者" }, { name: "extraRequirements", label: "额外要求", placeholder: "例如:强结论开头,结尾带 CTA" } ], onSubmit: async (values) => { if (!values.brief?.trim()) throw new Error("请填写创作需求"); const result = await storyforgeFetch(`/v2/assistants/${encodeURIComponent(assistant.id)}/generate`, { method: "POST", body: { brief: values.brief.trim(), platform: platformLabel(values.platform || "douyin"), audience: values.audience || "创业者", extra_requirements: values.extraRequirements || "", knowledge_base_ids: safeArray(assistant.knowledge_base_ids) } }); appState.lastGeneratedCopy = { assistantId: assistant.id, assistantName: assistant.name, prompt: values.brief.trim(), content: extractGeneratedCopy(result), usedDocuments: safeArray(result.used_documents).slice(0, 3) }; rememberAction("文案生成完成", `已用 Agent「${assistant.name}」生成一版文案。`, "green", result); renderAll(); } }); } function openCreateAiVideoAction(defaults = {}) { const guard = getPipelineGuard("aiVideo"); if (!guard.enabled) { alert(guard.reason); return; } const project = requireSelectedProject(); const assistant = getSelectedAssistant(); const kb = getProjectKnowledgeBases(project.id)[0]; const sourceJob = defaults.sourceJob || null; openActionModal({ title: "创建 AI 视频任务", description: "输入 brief 后,直接触发 AI 视频链。", submitLabel: "开始生产", fields: [ { name: "title", label: "任务标题", value: defaults.title || (sourceJob ? `${sourceJob.title} · AI 视频` : ""), placeholder: "例如:创业口播 AI 视频测试" }, { name: "brief", label: "视频 brief", type: "textarea", rows: 5, value: defaults.brief || getJobSeedBrief(sourceJob), placeholder: "写明主题、风格、镜头和目标受众" }, { name: "sourceJobId", label: "关联源任务", type: "select", value: defaults.sourceJobId || sourceJob?.id || "", options: [{ value: "", label: "不关联" }, ...getCompletedJobOptions()] }, { name: "style", label: "风格", value: defaults.style || "realistic" }, { name: "shots", label: "镜头数", type: "number", value: defaults.shots || 4, min: 1, max: 12 }, { name: "duration", label: "单镜头秒数", type: "number", value: defaults.duration || 5, min: 3, max: 12 } ], onSubmit: async (values) => { if (!values.title?.trim()) throw new Error("请填写任务标题"); if (!values.brief?.trim()) throw new Error("请填写视频 brief"); const job = await storyforgeFetch("/v2/pipelines/ai-video", { method: "POST", body: { project_id: project.id, assistant_id: assistant?.id || "", knowledge_base_id: kb?.id || "", source_job_id: values.sourceJobId || "", title: values.title.trim(), brief: values.brief.trim(), style: values.style || "realistic", shots: Number(values.shots || 4), duration: Number(values.duration || 5) } }); rememberAction("AI 视频任务已创建", `已创建任务 ${job.title || job.id}。`, "blue", job); await bootstrap(); } }); } function openCreateRealCutAction(defaults = {}) { const guard = getPipelineGuard("realCut"); if (!guard.enabled) { alert(guard.reason); return; } const project = requireSelectedProject(); const sourceJob = defaults.sourceJob || null; openActionModal({ title: "创建实拍剪辑任务", description: "基于已完成的源任务,把素材发到 cutvideo。", submitLabel: "开始剪辑", fields: [ { name: "title", label: "任务标题", value: defaults.title || (sourceJob ? `${sourceJob.title} · 实拍剪辑` : ""), placeholder: "例如:创业素材粗剪" }, { name: "sourceJobId", label: "源任务", type: "select", value: defaults.sourceJobId || sourceJob?.id || getCompletedJobOptions()[0]?.value || "", options: getCompletedJobOptions() }, { name: "targetDurationSec", label: "目标时长(秒)", type: "number", value: defaults.targetDurationSec || 60, min: 10, max: 300 }, { name: "aspectRatio", label: "画幅", value: defaults.aspectRatio || "9:16" }, { name: "objective", label: "目标", type: "textarea", rows: 4, value: defaults.objective || "", placeholder: "例如:保留高信息密度片段,输出适合短视频平台的粗剪结果" } ], onSubmit: async (values) => { if (!values.title?.trim()) throw new Error("请填写任务标题"); if (!values.sourceJobId) throw new Error("请先选择一个已完成的源任务"); const job = await storyforgeFetch("/v2/pipelines/real-cut", { method: "POST", body: { project_id: project.id, title: values.title.trim(), source_job_id: values.sourceJobId, target_duration_sec: Number(values.targetDurationSec || 60), target_aspect_ratio: values.aspectRatio || "9:16", objective: values.objective || "保留高信息密度片段,输出适合短视频平台的粗剪结果" } }); rememberAction("实拍剪辑任务已创建", `已创建任务 ${job.title || job.id}。`, "blue", job); await bootstrap(); } }); } function openLiveRecorderAction() { const status = getIntegrationDetail("live_recorder"); const project = getSelectedProject() || appState.dashboard?.projects?.[0] || null; const assistants = getAssistantOptions(project?.id || ""); openActionModal({ title: "直播录制控制", description: status.reachable ? "新增的是你当前租户名下的录制源。文件访问和录制状态也只会回到你的账号视图里。" : "当前 NAS 录制服务不可达,先检查集成健康。", submitLabel: "保存录制源", fields: [ { type: "html", label: "当前租户", html: renderLiveRecorderSummaryHtml() }, { name: "projectId", label: "归属项目", type: "select", value: project?.id || "", options: getProjectOptions() }, { name: "assistantId", label: "关联 Agent", type: "select", value: assistants[0]?.value || "", options: [{ value: "", label: "暂不绑定" }, ...assistants] }, { name: "platform", label: "平台", type: "select", value: "kuaishou", options: getPlatformOptions() }, { name: "title", label: "录制名称", placeholder: "例如:A 类目直播跟踪" }, { name: "quality", label: "清晰度", type: "select", value: "原画", options: ["原画", "蓝光", "超清", "高清", "标清", "流畅"].map((item) => ({ value: item, label: item })) }, { name: "sourceUrl", label: "直播源", type: "url", placeholder: "https://..." }, { name: "autoStart", label: "导入后立即开始", type: "checkbox", value: true } ], onSubmit: async (values) => { if (!values.sourceUrl?.trim()) throw new Error("请填写直播源链接"); const saved = await storyforgeFetch("/v2/live-recorder/sources", { method: "POST", body: { project_id: values.projectId || project?.id || "", assistant_id: values.assistantId || "", platform: normalizePlatformValue(values.platform, "kuaishou"), source_url: values.sourceUrl.trim(), title: values.title || "", quality: values.quality || "原画", enabled: true } }); let started = null; if (values.autoStart) { try { started = await storyforgeFetch("/v2/live-recorder/recorder/start", { method: "POST", body: {} }); } catch (error) { started = { ok: false, message: error.message }; } } rememberAction("直播录制已下发", "当前租户的直播源已经保存到服务端并同步到 NAS。", "green", { saved, started }); await bootstrap(); } }); } async function openLiveRecorderFileAction(fileId) { const target = safeArray(appState.liveRecorderFiles).find((item) => item.id === fileId); if (!target?.content_url) { throw new Error("当前录像文件不存在,可能已经被移除"); } const blob = await storyforgeFetchBlob(target.content_url); const blobUrl = URL.createObjectURL(blob); window.open(blobUrl, "_blank", "noopener,noreferrer"); window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60000); } async function openStorageArtifactAction(fileId) { const usage = appState.storageStatus?.tenant_usage || {}; const candidates = [ ...safeArray(usage.recent_download_artifacts), ...safeArray(usage.recent_job_artifacts), ...safeArray(appState.storageStatus?.recent_files), ...safeArray(appState.storageStatus?.recent_artifacts) ]; const target = candidates.find((item) => item.id === fileId); if (!target?.content_url) { throw new Error("当前产物不存在,可能已经被清理"); } const blob = await storyforgeFetchBlob(target.content_url); const blobUrl = URL.createObjectURL(blob); window.open(blobUrl, "_blank", "noopener,noreferrer"); window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60000); } function openReviewAction(defaults = {}) { const project = requireSelectedProject(); const assistants = getAssistantOptions(project.id); const sourceJob = defaults.sourceJob || null; const existingReview = defaults.review || null; const metrics = existingReview?.metrics || {}; openActionModal({ title: existingReview ? "编辑复盘" : "写复盘", description: existingReview ? "补充表现数据、判断和下一步动作,持续迭代项目策略。" : "把完成任务写成一条可追踪复盘,后续可按项目累计。", submitLabel: existingReview ? "保存复盘" : "创建复盘", fields: [ { name: "title", label: "标题", value: existingReview?.title || defaults.title || sourceJob?.title || "", placeholder: "例如:创业口播 3 月 22 日复盘" }, { name: "sourceJobId", label: "关联任务", type: "select", value: existingReview?.source_job_id || defaults.sourceJobId || sourceJob?.id || "", options: [{ value: "", label: "不关联任务" }, ...getCompletedJobOptions()] }, { name: "assistantId", label: "负责 Agent", type: "select", value: existingReview?.assistant_id || getSelectedAssistant()?.id || assistants[0]?.value || "", options: [{ value: "", label: "先不绑定" }, ...assistants] }, { name: "platform", label: "平台", type: "select", value: normalizePlatformValue(existingReview?.platform || defaults.platform || "douyin"), options: getPlatformOptions() }, { name: "contentType", label: "内容类型", type: "select", value: existingReview?.content_type || "video", options: [ { value: "video", label: "视频" }, { value: "image_text", label: "图文" }, { value: "live_clip", label: "直播切片" } ] }, { name: "publishUrl", label: "发布链接", type: "url", value: existingReview?.publish_url || "", placeholder: "https://..." }, { name: "publishedAt", label: "发布时间", value: existingReview?.published_at || "", placeholder: "2026-03-22T20:00:00+08:00" }, { name: "playCount", label: "播放", type: "number", value: metrics.play_count || 0, min: 0 }, { name: "likeCount", label: "点赞", type: "number", value: metrics.like_count || 0, min: 0 }, { name: "commentCount", label: "评论", type: "number", value: metrics.comment_count || 0, min: 0 }, { name: "shareCount", label: "分享", type: "number", value: metrics.share_count || 0, min: 0 }, { name: "verdict", label: "结论", type: "select", value: existingReview?.verdict || "", options: [ { value: "", label: "先不下结论" }, { value: "worth_scaling", label: "值得放大" }, { value: "needs_rework", label: "需要重做" }, { value: "good_reference", label: "适合借鉴" }, { value: "hold", label: "先观察" } ] }, { name: "highlights", label: "亮点", type: "textarea", rows: 4, value: existingReview?.highlights || "", placeholder: "例如:开头 3 秒抓人、评论区问题很集中" }, { name: "nextActions", label: "下一步", type: "textarea", rows: 4, value: existingReview?.next_actions || "", placeholder: "例如:保留结构,换一个细分人群再做一条" }, { name: "notes", label: "备注", type: "textarea", rows: 4, value: existingReview?.notes || "", placeholder: "补充团队讨论、平台环境、发布时间段等信息" } ], onSubmit: async (values) => { if (!values.title?.trim()) throw new Error("请填写复盘标题"); const payload = { project_id: project.id, source_job_id: values.sourceJobId || "", assistant_id: values.assistantId || "", title: values.title.trim(), platform: normalizePlatformValue(values.platform, "douyin"), content_type: values.contentType || "video", publish_url: values.publishUrl || "", published_at: values.publishedAt || "", metrics: { play_count: Number(values.playCount || 0), like_count: Number(values.likeCount || 0), comment_count: Number(values.commentCount || 0), share_count: Number(values.shareCount || 0) }, verdict: values.verdict || "", highlights: values.highlights || "", next_actions: values.nextActions || "", notes: values.notes || "" }; const review = existingReview ? await storyforgeFetch(`/v2/reviews/${encodeURIComponent(existingReview.id)}`, { method: "PATCH", body: payload }) : await storyforgeFetch("/v2/reviews", { method: "POST", body: payload }); rememberAction(existingReview ? "复盘已更新" : "复盘已创建", `已保存「${review.title}」并回写到项目复盘。`, "green", review); await bootstrap(); } }); } document.addEventListener("click", async (event) => { if (event.target instanceof HTMLElement && event.target.classList.contains("oneliner-backdrop")) { closeOneLinerPanel(); return; } const action = event.target.closest("[data-action]"); if (action) { const name = action.dataset.action; if (name === "open-auth") { openAuthModal(); return; } if (name === "close-auth") { closeAuthModal(); return; } if (name === "open-oneliner") { try { setBusy(true, "正在打开 OneLiner..."); if (appState.session) { await loadAgentControlSurfaces(appState.selectedProjectId || ""); if (appState.selectedOnelinerSessionId) { await loadOneLinerMessages(appState.selectedOnelinerSessionId); } else if (backendSupports("/v2/oneliner/sessions")) { await ensureOneLinerSession(); } } openOneLinerPanel(); renderAll(); } finally { setBusy(false, ""); } return; } if (name === "close-oneliner") { closeOneLinerPanel(); return; } if (name === "close-sheet") { closeActionModal(); return; } if (name === "submit-sheet") { await submitActionModal(); return; } if (name === "submit-auth") { // Auth form submission is handled centrally so clicking the submit // button and pressing Enter share the same code path. return; } if (name === "show-disabled-reason") { const reason = action.dataset.disabledReason || action.title || "当前动作暂不可用"; rememberAction("动作已拦截", reason, "orange"); renderAll(); alert(reason); return; } if (name === "auth-refresh" || name === "refresh-data") { await bootstrap(); return; } if (name === "refresh-tracking") { await refreshTrackingAccountsAction(); return; } if (name === "open-live-recorder") { openLiveRecorderAction(); return; } if (name === "open-live-recorder-file") { await openLiveRecorderFileAction(action.dataset.fileId || ""); return; } if (name === "open-storage-artifact") { await openStorageArtifactAction(action.dataset.fileId || ""); return; } if (name === "mark-tracking-read") { await markTrackingDigestRead(); rememberAction("日报已标记", "当前跟踪摘要已更新为已读,下次会从新的时间点继续汇总。", "green"); await bootstrap(); return; } if (name === "logout-session") { await logoutSession(); return; } if (name === "goto-discovery") { setScreen("discovery"); return; } if (name === "goto-intake") { setScreen("intake"); return; } if (name === "goto-automation") { setScreen("automation"); return; } if (name === "goto-playbook") { setScreen("playbook"); return; } if (name === "goto-production") { setScreen("production"); return; } if (name === "goto-review") { setScreen("review"); return; } if (name === "open-import-homepage") { openImportHomepageAction(); return; } if (name === "open-import-selected-account") { openImportSelectedAccountAction(); return; } if (name === "open-track-selected-account") { openTrackSelectedAccountAction(); return; } if (name === "refresh-tracked-account") { await refreshTrackedAccountAction(action.dataset.trackedAccountId || ""); return; } if (name === "open-import-video-link") { openImportVideoLinkAction(); return; } if (name === "open-import-text") { openImportTextAction(); return; } if (name === "open-upload-video") { openUploadVideoAction(); return; } if (name === "open-create-assistant") { openCreateAssistantAction(); return; } if (name === "open-oneliner-profile") { openOneLinerProfileAction(); return; } if (name === "select-oneliner-session") { appState.selectedOnelinerSessionId = action.dataset.sessionId || ""; await loadOneLinerMessages(appState.selectedOnelinerSessionId); renderAll(); return; } if (name === "select-assistant") { appState.selectedAssistantId = action.dataset.assistantId || ""; rememberAction("已切换当前 Agent", `当前默认 Agent 已更新为「${getSelectedAssistant()?.name || "未选择"}」。`, "green"); renderAll(); return; } if (name === "open-edit-assistant") { openEditAssistantAction(action.dataset.assistantId || ""); return; } if (name === "open-platform-agent-profile") { openPlatformAgentProfileAction(action.dataset.platform || ""); return; } if (name === "open-platform-agent-detail") { await openPlatformAgentDetailAction(action.dataset.platform || ""); return; } if (name === "open-action-registry-edit") { openActionRegistryEditAction(action.dataset.actionKey || ""); return; } if (name === "open-tenant-quota") { openTenantQuotaAction(); return; } if (name === "run-oneliner-action") { setBusy(true, "OneLiner 正在执行动作..."); try { await executeOneLinerAction(action.dataset.executorKey || "", { platform: action.dataset.platform || "", sessionId: action.dataset.sessionId || "", payload: collectOneLinerActionPayload(action) }); } catch (error) { alert("执行 OneLiner 动作失败: " + error.message); } finally { setBusy(false, ""); } return; } if (name === "open-platform-agent-memory") { openPlatformAgentMemoryAction(action.dataset.platform || ""); return; } if (name === "open-platform-agent-skill") { openPlatformAgentSkillAction(action.dataset.platform || ""); return; } if (name === "review-platform-skill") { openPlatformSkillReviewAction(action.dataset.platform || "", action.dataset.skillId || "", action.dataset.accepted !== "false"); return; } if (name === "rollback-platform-skill") { openPlatformSkillRollbackAction(action.dataset.platform || "", action.dataset.skillId || "", action.dataset.versionId || ""); return; } if (name === "analyze-selected-account") { openAnalyzeSelectedAccountAction(); return; } if (name === "analyze-top-videos") { openAnalyzeTopVideosAction(); return; } if (name === "open-generate-copy") { openGenerateCopyAction(); return; } if (name === "open-ai-video") { openCreateAiVideoAction(); return; } if (name === "open-real-cut") { openCreateRealCutAction(); return; } if (name === "open-create-review") { openReviewAction(); return; } if (name === "open-preferred-model") { openPreferredModelAction(); return; } if (name === "open-review-from-job") { const jobId = action.dataset.jobId || ""; const fromDashboard = safeArray(appState.dashboard?.recent_jobs).find((item) => item.id === jobId) || null; const fromDetail = appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null; openReviewAction({ sourceJobId: jobId, sourceJob: fromDetail || fromDashboard, title: (fromDetail || fromDashboard)?.title || "" }); return; } if (name === "open-review-edit") { const review = getReviewById(action.dataset.reviewId || ""); if (!review) { alert("复盘记录不存在,请先刷新页面"); return; } openReviewAction({ review }); return; } if (name === "open-similar-search") { openSimilaritySearchAction(); return; } if (name === "open-benchmark-link") { openBenchmarkLinkAction(); return; } if (name === "save-candidate-benchmark") { setBusy(true, "正在保存对标关系..."); try { await saveCandidateAsBenchmark(action.dataset.candidateIndex || ""); } catch (error) { alert("保存候选失败: " + error.message); } finally { setBusy(false, ""); } return; } if (name === "open-job-detail") { openJobDetailAction(action.dataset.jobId || ""); return; } if (name === "scan-admin-ops") { await scanAdminOpsAction(); return; } if (name === "review-admin-incident") { openAdminIncidentReviewAction(action.dataset.incidentId || ""); return; } if (name === "open-admin-repair-plan") { openAdminRepairPlanAction(action.dataset.incidentId || ""); return; } if (name === "open-admin-fix-run-audit") { openAdminFixRunAuditAction(action.dataset.runId || ""); return; } if (name === "job-to-ai-video") { const jobId = action.dataset.jobId || ""; const detail = appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null; closeActionModal(); openCreateAiVideoAction({ sourceJobId: jobId, sourceJob: detail }); return; } if (name === "job-to-real-cut") { const jobId = action.dataset.jobId || ""; const detail = appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null; closeActionModal(); openCreateRealCutAction({ sourceJobId: jobId, sourceJob: detail, objective: detail ? `基于任务「${detail.title}」保留高信息密度片段,输出适合短视频平台的粗剪结果。` : "" }); return; } if (name === "job-to-generate-copy") { const jobId = action.dataset.jobId || ""; const detail = appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null; closeActionModal(); openGenerateCopyAction({ sourceJob: detail, brief: detail ? `基于任务「${detail.title}」的结果,生成一版可发布的短视频文案。参考摘要:${getJobSeedBrief(detail)}` : "" }); return; } if (name === "create-project") { await createProject(); return; } if (name === "select-project") { appState.selectedProjectId = action.dataset.projectId || ""; setBusy(true, "正在切换项目视图..."); try { if (backendSupports("/v2/storage/status")) { await loadStorageStatus(appState.selectedProjectId || ""); } else { appState.storageStatus = null; } await loadAgentControlSurfaces(appState.selectedProjectId || ""); if (appState.selectedOnelinerSessionId) { await loadOneLinerMessages(appState.selectedOnelinerSessionId); } else { appState.onelinerMessages = []; } } finally { setBusy(false, ""); } renderAll(); return; } if (name === "select-account") { const accountId = action.dataset.accountId; if (!accountId) return; setBusy(true, "正在加载对标详情..."); try { const account = safeArray(appState.accounts).find((item) => item.id === accountId) || null; await loadPlatformAccount(getAccountPlatform(account), accountId); renderAll(); } catch (error) { alert("加载对标详情失败: " + error.message); } finally { setBusy(false, ""); } return; } if (name === "submit-oneliner") { return; } if (name === "scroll-selected") { document.getElementById("selected-account-anchor")?.scrollIntoView({ behavior: "smooth", block: "start" }); return; } rememberAction("动作待接入", `前端还没有处理动作「${name}」,当前仍可继续通过 OneLiner 对话承接。`, "orange"); renderAll(); return; } }); document.addEventListener("input", (event) => { const target = event.target; if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) return; if (target.dataset.action === "discovery-query") { appState.discoveryQuery = target.value.trim(); screenMap.discovery.innerHTML = renderDiscoveryScreen(); } }); document.addEventListener("submit", async (event) => { const form = event.target; if (!(form instanceof HTMLFormElement)) return; if (form.dataset.role === "auth-form") { event.preventDefault(); setBusy(true, "正在登录并加载..."); try { await loginWithForm(); closeAuthModal(); await bootstrap(); } catch (error) { const message = document.querySelector('[data-role="auth-message"]'); if (message) message.textContent = error.message; } finally { setBusy(false, ""); } return; } if (form.dataset.role === "oneliner-form") { event.preventDefault(); const input = form.querySelector('[data-role="oneliner-input"]'); const value = input instanceof HTMLTextAreaElement ? input.value.trim() : ""; if (!value) return; setBusy(true, "OneLiner 正在拆解任务..."); try { await submitOneLinerMessage(value); if (input instanceof HTMLTextAreaElement) input.value = ""; openOneLinerPanel(); renderAll(); } catch (error) { alert("OneLiner 调度失败: " + error.message); } finally { setBusy(false, ""); } } }); navButtons.forEach((button) => { button.addEventListener("click", () => { const next = button.dataset.screenTarget; setScreen(next); }); }); ensureAuthUi(); renderAll(); bootstrap();