diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 7457157..b82754f 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -17,6 +17,7 @@ const appState = { 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()), @@ -43,6 +44,52 @@ const ACTIVE_PLATFORMS = [ { value: "wechat_video", label: "微信视频号" } ]; const ACTIVE_PLATFORM_CHIPS = ["全平台", "抖音", "小红书", "B站", "快手", "视频号"]; +const PLATFORM_REGISTRY = { + douyin: { + label: "抖音", + shortLabel: "抖音", + workbenchReady: true, + routes: { + accounts: "/v2/douyin/accounts", + workspace: (accountId) => `/v2/douyin/accounts/${encodeURIComponent(accountId)}/workspace`, + videos: (accountId) => `/v2/douyin/accounts/${encodeURIComponent(accountId)}/videos?limit=80`, + analyzeAccount: (accountId) => `/v2/douyin/accounts/${encodeURIComponent(accountId)}/analysis`, + analyzeTopVideos: (accountId) => `/v2/douyin/accounts/${encodeURIComponent(accountId)}/videos/analyze-top`, + similarSearches: "/v2/douyin/similar-searches", + similarSearchDetail: (searchId) => `/v2/douyin/similar-searches/${encodeURIComponent(searchId)}`, + benchmarkLinks: (accountId) => `/v2/douyin/accounts/${encodeURIComponent(accountId)}/benchmark-links`, + trackingAccounts: "/v2/douyin/tracking/accounts", + trackingDigest: "/v2/douyin/tracking/digest", + trackingRefresh: "/v2/douyin/tracking/refresh", + trackingCursor: "/v2/douyin/tracking/cursor", + trackingAccountRefresh: (trackedAccountId) => `/v2/douyin/tracking/accounts/${encodeURIComponent(trackedAccountId)}/refresh` + } + }, + xiaohongshu: { + label: "小红书", + shortLabel: "小红书", + workbenchReady: false, + pendingText: "小红书工作台待接入" + }, + bilibili: { + label: "哔哩哔哩", + shortLabel: "B站", + workbenchReady: false, + pendingText: "B站工作台待接入" + }, + kuaishou: { + label: "快手", + shortLabel: "快手", + workbenchReady: false, + pendingText: "快手工作台待接入" + }, + wechat_video: { + label: "微信视频号", + shortLabel: "视频号", + workbenchReady: false, + pendingText: "视频号工作台待接入" + } +}; const INTEGRATION_META = { local_model: { label: "本机模型", @@ -107,6 +154,90 @@ function platformLabel(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 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("&", "&") @@ -574,6 +705,7 @@ async function logoutSession() { 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 }; @@ -587,6 +719,7 @@ async function logoutSession() { appState.lastGeneratedCopy = null; appState.lastSimilaritySearch = null; appState.lastJobDetail = null; + localStorage.removeItem(STORAGE_KEY + ":currentPlatform"); renderAll(); } @@ -601,14 +734,23 @@ async function loadKnowledgeDocuments(knowledgeBases) { return groups.flat().slice(0, 12); } -async function loadDouyinAccount(accountId) { +async function loadPlatformAccount(platform, accountId) { if (!accountId) return; + const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform()); appState.selectedAccountId = accountId; - const supportsAccountVideos = backendSupports("/v2/douyin/accounts/{account_id}/videos"); + 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(`/v2/douyin/accounts/${encodeURIComponent(accountId)}/workspace`), + storyforgeFetch(workspacePath), supportsAccountVideos - ? storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(accountId)}/videos?limit=80`).catch(() => ({ + ? storyforgeFetch(videosPath).catch(() => ({ items: [], meta: {}, top_scored_video_ids: [], @@ -651,16 +793,20 @@ async function bootstrap() { return; } appState.backendCapabilities = await loadBackendCapabilities(appState.session.backendUrl).catch(() => null); - const supportsTracking = backendSupports("/v2/douyin/tracking/accounts"); - const supportsTrackingDigest = backendSupports("/v2/douyin/tracking/digest"); + 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 [dashboard, contentSources, accounts, trackingAccountsPayload, reviews, integrationHealth, localModelCatalog] = await Promise.all([ storyforgeFetch("/v2/me/dashboard"), storyforgeFetch("/v2/content-sources").catch(() => []), - storyforgeFetch("/v2/douyin/accounts").catch(() => []), - supportsTracking ? storyforgeFetch("/v2/douyin/tracking/accounts").catch(() => ({ items: [], cursor_last_seen_at: "" })) : Promise.resolve({ items: [], cursor_last_seen_at: "" }), + 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) @@ -670,8 +816,8 @@ async function bootstrap() { setLastSeenAt(trackingCursorLastSeenAt); } const trackingSince = trackingCursorLastSeenAt || getTrackingSinceIso(); - const trackingDigest = supportsTrackingDigest - ? await storyforgeFetch(`/v2/douyin/tracking/digest?since=${encodeURIComponent(trackingSince)}&limit=24`).catch(() => ({ + const trackingDigest = trackingDigestPath + ? await storyforgeFetch(`${trackingDigestPath}?since=${encodeURIComponent(trackingSince)}&limit=24`).catch(() => ({ items: [], tracked_accounts: [], cursor_last_seen_at: trackingCursorLastSeenAt @@ -696,7 +842,8 @@ async function bootstrap() { const selectedAccountExists = appState.accounts.some((item) => item.id === appState.selectedAccountId); const nextAccountId = selectedAccountExists ? appState.selectedAccountId : appState.accounts[0]?.id || ""; if (nextAccountId) { - await loadDouyinAccount(nextAccountId); + const nextAccount = appState.accounts.find((item) => item.id === nextAccountId) || null; + await loadPlatformAccount(getAccountPlatform(nextAccount), nextAccountId); } else { appState.selectedAccountId = ""; appState.selectedWorkspace = null; @@ -714,13 +861,15 @@ async function bootstrap() { } async function markTrackingDigestRead() { - if (!backendSupports("/v2/douyin/tracking/cursor")) { + 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("/v2/douyin/tracking/cursor", { + await storyforgeFetch(trackingCursorPath, { method: "POST", body: { last_seen_at: nextSeenAt } }); @@ -728,14 +877,16 @@ async function markTrackingDigestRead() { } async function refreshTrackingAccountsAction() { - if (!backendSupports("/v2/douyin/tracking/refresh")) { + 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("/v2/douyin/tracking/refresh", { + const payload = await storyforgeFetch(trackingRefreshPath, { method: "POST" }); rememberAction( @@ -754,14 +905,16 @@ async function refreshTrackedAccountAction(trackedAccountId) { if (!trackedAccountId) { throw new Error("trackedAccountId is required"); } - if (!backendSupports("/v2/douyin/tracking/accounts/{tracked_account_id}/refresh")) { + 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(`/v2/douyin/tracking/accounts/${encodeURIComponent(trackedAccountId)}/refresh`, { + const payload = await storyforgeFetch(trackingRefreshPath, { method: "POST" }); const success = payload.success !== false; @@ -849,17 +1002,21 @@ function getReviewById(reviewId) { function getContentSourcesForAccount(account) { if (!account) return []; - const profileUrl = String(account.profile_url || "").trim(); - const douyinId = String(account.douyin_id || "").trim(); - const nickname = String(account.nickname || "").trim(); + 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 handle = String(source.handle || "").trim(); + const sourceHandle = String(source.handle || "").trim(); const title = String(source.title || "").trim(); + const sourcePlatform = normalizePlatformValue(source.platform || "", platform); return ( - (profileUrl && sourceUrl === profileUrl) || - (douyinId && handle === douyinId) || - (nickname && title.includes(nickname)) + sourcePlatform === platform && ( + (profileUrl && sourceUrl === profileUrl) || + (handle && sourceHandle === handle) || + (nickname && title.includes(nickname)) + ) ); }); } @@ -1195,6 +1352,9 @@ function markSavedCandidate(candidate, 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 = { @@ -1207,7 +1367,7 @@ async function saveCandidateAsBenchmark(candidateIndex, relationType = "benchmar if (!payload.target_account_ids.length && !payload.target_profile_urls.length) { throw new Error("当前候选没有可保存的账号或主页链接"); } - const result = await storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(account.id)}/benchmark-links`, { + const result = await storyforgeFetch(benchmarkPath, { method: "POST", body: payload }); @@ -1411,10 +1571,10 @@ function renderDashboardScreen() { ${accounts.slice(0, 3).map((account) => `
-
${escapeHtml(initials(account.nickname || account.douyin_id))}
+
${escapeHtml(initials(getAccountName(account)))}
-
${escapeHtml(account.nickname || "未命名账号")}
-
${escapeHtml(account.signature || account.profile_url || "已同步抖音账号")}
+
${escapeHtml(getAccountName(account))}
+
${escapeHtml(account.signature || getAccountProfileUrl(account) || `已同步${platformLabel(getAccountPlatform(account))}账号`)}
@@ -1536,17 +1696,20 @@ function renderProjectsScreen() { function renderDiscoveryScreen() { if (!appState.dashboard) { - return screenShell("找对标", "连接后端后才能加载真实对标账号。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("对标库未加载", "登录后这里会直接显示抖音账号列表和详情。")); + return screenShell("找对标", "连接后端后才能加载真实对标账号。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("对标库未加载", "登录后这里会显示当前平台的账号列表和详情。")); } const query = appState.discoveryQuery.toLowerCase(); const accounts = safeArray(appState.accounts).filter((account) => { if (!query) return true; - return [account.nickname, account.signature, account.profile_url, account.douyin_id, ...safeArray(account.tags), ...safeArray(account.keywords)] + 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); @@ -1560,18 +1723,20 @@ function renderDiscoveryScreen() { const tracked = selected?.id ? isTrackedAccount(selected.id) : false; return screenShell( "找对标", - "这里已经接入真实抖音账号列表和单账号详情。", - `${button("导入主页", "open-import-homepage")} ${button("导入当前对标", "open-import-selected-account")} ${button(tracked ? "已在跟踪" : "加入跟踪", "open-track-selected-account")} ${button("账号分析", "analyze-selected-account")} ${button("高分分析", "analyze-top-videos")} ${button("查相似", "open-similar-search")} ${button("存对标", "open-benchmark-link", "primary")}`, + 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))}
@@ -1592,10 +1757,10 @@ function renderDiscoveryScreen() { return ` `; - }).join("") || `
当前没有抖音账号数据。
`} + }).join("") || `
当前平台没有账号数据。
`}
@@ -1628,10 +1793,10 @@ function renderDiscoveryScreen() { @@ -1641,20 +1806,20 @@ function renderDiscoveryScreen() { - `).join("") || ``} + `).join("") || ``}
-

当前选中对标

直接来自工作台接口
${escapeHtml(selected?.nickname || "未选中")}
+

当前选中对标

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

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

-

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

+

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

+

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

@@ -1686,13 +1851,14 @@ function renderDiscoveryScreen() { ` : `

还没有选中账号

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

`}
-
-

账号画像

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

账号画像

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

高分作品

@@ -1774,12 +1940,14 @@ function renderTrackingScreen() { if (!appState.dashboard) { return screenShell("跟踪账号", "登录后才能生成真实日报。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("日报未加载", "当前还没有可用的对标账号数据。")); } - if (!backendSupports("/v2/douyin/tracking/accounts")) { + const currentPlatform = getPreferredPlatform(); + const trackingAccountsPath = getWorkbenchRoute(currentPlatform, "trackingAccounts"); + if (!trackingAccountsPath || !backendSupports(trackingAccountsPath)) { return screenShell( "跟踪账号", - "当前 live collector 还没有接入跟踪日报接口。", + `${getPendingWorkbenchReason(currentPlatform)}。`, `${button("跳到找对标", "goto-discovery", "primary")}`, - renderEmptyState("跟踪能力暂未接入", "这套后端还缺 /v2/douyin/tracking/*,等 live collector 同步后这里会自动切成真实日报。") + renderEmptyState("跟踪能力暂未接入", `这套后端还没有接入 ${platformLabel(currentPlatform)} 跟踪接口,等 live collector 同步后这里会自动切成真实日报。`) ); } const trackedAccounts = safeArray(appState.trackingAccounts); @@ -1787,7 +1955,7 @@ function renderTrackingScreen() { const cursorLabel = appState.lastSeenAt ? formatDateTime(appState.lastSeenAt) : "尚未记录"; return screenShell( "跟踪账号", - "这里已经接上真实跟踪对象和按上次打开后的更新日报。", + `这里已经接上真实${getPlatformShortLabel(currentPlatform)}跟踪对象和按上次打开后的更新日报。`, `${button("同步全部", "refresh-tracking")} ${button("标记已读", "mark-tracking-read")} ${button("跳到找对标", "goto-discovery", "primary")}`, `
@@ -2475,6 +2643,7 @@ function openImportHomepageAction() { function openImportSelectedAccountAction() { const account = requireSelectedAccountRow(); + const platform = getAccountPlatform(account); const project = requireSelectedProject(); const assistants = getAssistantOptions(project.id); const currentSources = getCurrentProjectSourcesForAccount(account, project.id); @@ -2488,10 +2657,10 @@ function openImportSelectedAccountAction() { submitLabel: currentSource ? "继续同步" : "导入并同步", fields: [ { name: "projectId", label: "归属项目", type: "select", value: project.id, options: getProjectOptions() }, - { name: "platform", label: "平台", type: "select", value: normalizePlatformValue(currentSource?.platform || "douyin"), options: getPlatformOptions() }, - { name: "title", label: "内容源标题", value: currentSource?.title || `${account.nickname || account.douyin_id || "对标账号"} 对标主页` }, - { name: "handle", label: "账号标识", value: currentSource?.handle || account.douyin_id || "" }, - { name: "sourceUrl", label: "主页链接", type: "url", value: currentSource?.source_url || account.profile_url || "", placeholder: "https://..." }, + { 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 }, @@ -2511,7 +2680,7 @@ function openImportSelectedAccountAction() { platform, handle: values.handle || "", source_url: values.sourceUrl.trim(), - title: values.title || values.handle || account.nickname || "对标主页", + title: values.title || values.handle || getAccountName(account) || "对标主页", metadata: { imported_from_account_id: account.id, imported_from_workspace: "discovery" @@ -2526,15 +2695,15 @@ function openImportSelectedAccountAction() { assistant_id: values.assistantId || "", content_source_id: source.id, platform, - handle: values.handle || account.douyin_id || "", + handle: values.handle || getAccountHandle(account) || "", source_url: values.sourceUrl.trim(), - title: values.title || account.nickname || values.handle || "对标主页", + 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("对标已接入项目", `已把「${account.nickname || account.douyin_id || "当前对标"}」接入项目,并创建同步任务 ${job.title || job.id}。`, "green", { source, job }); + rememberAction("对标已接入项目", `已把「${getAccountName(account) || "当前对标"}」接入项目,并创建同步任务 ${job.title || job.id}。`, "green", { source, job }); await bootstrap(); } }); @@ -2542,6 +2711,13 @@ function openImportSelectedAccountAction() { 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); @@ -2552,12 +2728,12 @@ function openTrackSelectedAccountAction() { : "把当前对标账号加入每日跟踪,后续自动生成更新日报。", submitLabel: trackedItem ? "保存跟踪" : "开始跟踪", fields: [ - { name: "accountName", label: "账号", type: "html", html: `
${escapeHtml(account.nickname || account.douyin_id || "未命名账号")}

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

` }, + { 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("/v2/douyin/tracking/accounts", { + await storyforgeFetch(trackingAccountsPath, { method: "POST", body: { tracked_account_id: account.id, @@ -2565,7 +2741,7 @@ function openTrackSelectedAccountAction() { note: values.note || "" } }); - rememberAction(trackedItem ? "跟踪已更新" : "已加入跟踪", `账号「${account.nickname || account.douyin_id || "当前对标"}」现在会进入更新日报。`, "green"); + rememberAction(trackedItem ? "跟踪已更新" : "已加入跟踪", `账号「${getAccountName(account) || "当前对标"}」现在会进入更新日报。`, "green"); await bootstrap(); } }); @@ -2748,6 +2924,13 @@ function openEditAssistantAction(assistantId = "") { 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: "从商业化和内容运营角度重跑一次账号分析。", @@ -2759,7 +2942,7 @@ function openAnalyzeSelectedAccountAction() { { name: "topVideoCount", label: "高分作品分析数", type: "number", value: 4, min: 1, max: 10 } ], onSubmit: async (values) => { - const result = await storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(account.id)}/analysis`, { + const result = await storyforgeFetch(analyzePath, { method: "POST", body: { model_profile_ids: [], @@ -2775,7 +2958,7 @@ function openAnalyzeSelectedAccountAction() { }); const summary = result?.suggestions?.[0]?.parsed_json?.executive_summary || result?.suggestions?.[0]?.suggestion_text || "已生成新的账号分析。"; rememberAction("对标账号分析完成", brief(summary, 120), "green", result); - await loadDouyinAccount(account.id); + await loadPlatformAccount(platform, account.id); renderAll(); } }); @@ -2783,7 +2966,9 @@ function openAnalyzeSelectedAccountAction() { function openAnalyzeTopVideosAction() { const account = requireSelectedAccountRow(); - if (!backendSupports("/v2/douyin/accounts/{account_id}/videos/analyze-top")) { + 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; @@ -2797,7 +2982,7 @@ function openAnalyzeTopVideosAction() { { name: "minScore", label: "最低分阈值", type: "number", value: 45, min: 0, max: 100 } ], onSubmit: async (values) => { - const result = await storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(account.id)}/videos/analyze-top`, { + const result = await storyforgeFetch(analyzePath, { method: "POST", body: { model_profile_id: "", @@ -2807,7 +2992,7 @@ function openAnalyzeTopVideosAction() { } }); rememberAction("高分作品分析完成", `已补分析 ${formatNumber(result.analyzed_count)} 条高分作品。`, "green", result); - await loadDouyinAccount(account.id); + await loadPlatformAccount(platform, account.id); renderAll(); } }); @@ -2815,6 +3000,13 @@ function openAnalyzeTopVideosAction() { 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 基于当前账号画像找更多可借鉴对象。", @@ -2824,7 +3016,7 @@ function openSimilaritySearchAction() { { name: "extraRequirements", label: "额外要求", type: "textarea", rows: 4, placeholder: "例如:优先找创业成交类、口播结构强的账号" } ], onSubmit: async (values) => { - const created = await storyforgeFetch("/v2/douyin/similar-searches", { + const created = await storyforgeFetch(createPath, { method: "POST", body: { source_account_id: account.id, @@ -2837,12 +3029,13 @@ function openSimilaritySearchAction() { } }); const searchId = created.id || created.search_id; + const detailPath = searchId ? getWorkbenchRoute(platform, "similarSearchDetail", searchId) : ""; const detail = searchId - ? await storyforgeFetch(`/v2/douyin/similar-searches/${encodeURIComponent(searchId)}`) + ? await storyforgeFetch(detailPath) : created; appState.lastSimilaritySearch = detail; rememberAction("相似账号已生成", `已生成 ${formatNumber(safeArray(detail.candidates).length)} 个候选账号。`, "green", detail); - await loadDouyinAccount(account.id); + await loadPlatformAccount(platform, account.id); renderAll(); } }); @@ -2850,9 +3043,16 @@ function openSimilaritySearchAction() { 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: item.nickname || item.douyin_id || item.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; @@ -2872,7 +3072,7 @@ function openBenchmarkLinkAction(defaults = {}) { ], onSubmit: async (values) => { if (!values.targetAccountId && !values.targetProfileUrl?.trim()) throw new Error("请先选择一个目标账号或填写主页链接"); - const result = await storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(account.id)}/benchmark-links`, { + const result = await storyforgeFetch(benchmarkPath, { method: "POST", body: { target_account_ids: values.targetAccountId ? [values.targetAccountId] : [], @@ -3409,7 +3609,8 @@ document.addEventListener("click", async (event) => { if (!accountId) return; setBusy(true, "正在加载对标详情..."); try { - await loadDouyinAccount(accountId); + const account = safeArray(appState.accounts).find((item) => item.id === accountId) || null; + await loadPlatformAccount(getAccountPlatform(account), accountId); renderAll(); } catch (error) { alert("加载对标详情失败: " + error.message);