feat: generalize web workbench platform routing

This commit is contained in:
kris
2026-03-23 08:47:24 +08:00
parent 3fe01d2f23
commit ac7fe786f3

View File

@@ -17,6 +17,7 @@ const appState = {
selectedVideos: { items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 }, selectedVideos: { items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 },
documents: [], documents: [],
discoveryQuery: "", discoveryQuery: "",
currentPlatform: localStorage.getItem(STORAGE_KEY + ":currentPlatform") || "",
selectedProjectId: "", selectedProjectId: "",
selectedAssistantId: "", selectedAssistantId: "",
lastSeenAt: Number(localStorage.getItem(STORAGE_KEY + ":lastSeenAt") || Date.now()), lastSeenAt: Number(localStorage.getItem(STORAGE_KEY + ":lastSeenAt") || Date.now()),
@@ -43,6 +44,52 @@ const ACTIVE_PLATFORMS = [
{ value: "wechat_video", label: "微信视频号" } { value: "wechat_video", label: "微信视频号" }
]; ];
const ACTIVE_PLATFORM_CHIPS = ["全平台", "抖音", "小红书", "B站", "快手", "视频号"]; 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 = { const INTEGRATION_META = {
local_model: { local_model: {
label: "本机模型", label: "本机模型",
@@ -107,6 +154,90 @@ function platformLabel(value) {
return matched?.label || String(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) { function escapeHtml(value) {
return String(value ?? "") return String(value ?? "")
.replaceAll("&", "&") .replaceAll("&", "&")
@@ -574,6 +705,7 @@ async function logoutSession() {
appState.contentSources = []; appState.contentSources = [];
appState.accounts = []; appState.accounts = [];
appState.selectedAccountId = ""; appState.selectedAccountId = "";
appState.currentPlatform = "";
appState.selectedAssistantId = ""; appState.selectedAssistantId = "";
appState.selectedWorkspace = null; appState.selectedWorkspace = null;
appState.selectedVideos = { items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 }; 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.lastGeneratedCopy = null;
appState.lastSimilaritySearch = null; appState.lastSimilaritySearch = null;
appState.lastJobDetail = null; appState.lastJobDetail = null;
localStorage.removeItem(STORAGE_KEY + ":currentPlatform");
renderAll(); renderAll();
} }
@@ -601,14 +734,23 @@ async function loadKnowledgeDocuments(knowledgeBases) {
return groups.flat().slice(0, 12); return groups.flat().slice(0, 12);
} }
async function loadDouyinAccount(accountId) { async function loadPlatformAccount(platform, accountId) {
if (!accountId) return; if (!accountId) return;
const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform());
appState.selectedAccountId = accountId; 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([ const [workspace, videos] = await Promise.all([
storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(accountId)}/workspace`), storyforgeFetch(workspacePath),
supportsAccountVideos supportsAccountVideos
? storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(accountId)}/videos?limit=80`).catch(() => ({ ? storyforgeFetch(videosPath).catch(() => ({
items: [], items: [],
meta: {}, meta: {},
top_scored_video_ids: [], top_scored_video_ids: [],
@@ -651,16 +793,20 @@ async function bootstrap() {
return; return;
} }
appState.backendCapabilities = await loadBackendCapabilities(appState.session.backendUrl).catch(() => null); appState.backendCapabilities = await loadBackendCapabilities(appState.session.backendUrl).catch(() => null);
const supportsTracking = backendSupports("/v2/douyin/tracking/accounts"); const preferredPlatform = getPreferredPlatform();
const supportsTrackingDigest = backendSupports("/v2/douyin/tracking/digest"); 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 supportsReviews = backendSupports("/v2/reviews");
const supportsIntegrationHealth = backendSupports("/v2/integrations/health"); const supportsIntegrationHealth = backendSupports("/v2/integrations/health");
const supportsLocalModels = backendSupports("/v2/integrations/local-models"); const supportsLocalModels = backendSupports("/v2/integrations/local-models");
const [dashboard, contentSources, accounts, trackingAccountsPayload, reviews, integrationHealth, localModelCatalog] = await Promise.all([ const [dashboard, contentSources, accounts, trackingAccountsPayload, reviews, integrationHealth, localModelCatalog] = await Promise.all([
storyforgeFetch("/v2/me/dashboard"), storyforgeFetch("/v2/me/dashboard"),
storyforgeFetch("/v2/content-sources").catch(() => []), storyforgeFetch("/v2/content-sources").catch(() => []),
storyforgeFetch("/v2/douyin/accounts").catch(() => []), accountListPath ? storyforgeFetch(accountListPath).catch(() => []) : Promise.resolve([]),
supportsTracking ? storyforgeFetch("/v2/douyin/tracking/accounts").catch(() => ({ items: [], cursor_last_seen_at: "" })) : Promise.resolve({ items: [], cursor_last_seen_at: "" }), trackingAccountsPath ? storyforgeFetch(trackingAccountsPath).catch(() => ({ items: [], cursor_last_seen_at: "" })) : Promise.resolve({ items: [], cursor_last_seen_at: "" }),
supportsReviews ? storyforgeFetch("/v2/reviews").catch(() => []) : Promise.resolve([]), supportsReviews ? storyforgeFetch("/v2/reviews").catch(() => []) : Promise.resolve([]),
supportsIntegrationHealth ? storyforgeFetch("/v2/integrations/health").catch(() => null) : Promise.resolve(null), supportsIntegrationHealth ? storyforgeFetch("/v2/integrations/health").catch(() => null) : Promise.resolve(null),
supportsLocalModels ? storyforgeFetch("/v2/integrations/local-models").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); setLastSeenAt(trackingCursorLastSeenAt);
} }
const trackingSince = trackingCursorLastSeenAt || getTrackingSinceIso(); const trackingSince = trackingCursorLastSeenAt || getTrackingSinceIso();
const trackingDigest = supportsTrackingDigest const trackingDigest = trackingDigestPath
? await storyforgeFetch(`/v2/douyin/tracking/digest?since=${encodeURIComponent(trackingSince)}&limit=24`).catch(() => ({ ? await storyforgeFetch(`${trackingDigestPath}?since=${encodeURIComponent(trackingSince)}&limit=24`).catch(() => ({
items: [], items: [],
tracked_accounts: [], tracked_accounts: [],
cursor_last_seen_at: trackingCursorLastSeenAt cursor_last_seen_at: trackingCursorLastSeenAt
@@ -696,7 +842,8 @@ async function bootstrap() {
const selectedAccountExists = appState.accounts.some((item) => item.id === appState.selectedAccountId); const selectedAccountExists = appState.accounts.some((item) => item.id === appState.selectedAccountId);
const nextAccountId = selectedAccountExists ? appState.selectedAccountId : appState.accounts[0]?.id || ""; const nextAccountId = selectedAccountExists ? appState.selectedAccountId : appState.accounts[0]?.id || "";
if (nextAccountId) { if (nextAccountId) {
await loadDouyinAccount(nextAccountId); const nextAccount = appState.accounts.find((item) => item.id === nextAccountId) || null;
await loadPlatformAccount(getAccountPlatform(nextAccount), nextAccountId);
} else { } else {
appState.selectedAccountId = ""; appState.selectedAccountId = "";
appState.selectedWorkspace = null; appState.selectedWorkspace = null;
@@ -714,13 +861,15 @@ async function bootstrap() {
} }
async function markTrackingDigestRead() { 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"); rememberAction("当前后端暂不支持", "这套 live collector 还没有接入跟踪已读游标。", "orange");
renderAll(); renderAll();
return; return;
} }
const nextSeenAt = new Date().toISOString(); const nextSeenAt = new Date().toISOString();
await storyforgeFetch("/v2/douyin/tracking/cursor", { await storyforgeFetch(trackingCursorPath, {
method: "POST", method: "POST",
body: { last_seen_at: nextSeenAt } body: { last_seen_at: nextSeenAt }
}); });
@@ -728,14 +877,16 @@ async function markTrackingDigestRead() {
} }
async function refreshTrackingAccountsAction() { 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"); rememberAction("当前后端暂不支持", "这套 live collector 还没有接入批量跟踪同步。", "orange");
renderAll(); renderAll();
return; return;
} }
setBusy(true, "正在同步跟踪账号..."); setBusy(true, "正在同步跟踪账号...");
try { try {
const payload = await storyforgeFetch("/v2/douyin/tracking/refresh", { const payload = await storyforgeFetch(trackingRefreshPath, {
method: "POST" method: "POST"
}); });
rememberAction( rememberAction(
@@ -754,14 +905,16 @@ async function refreshTrackedAccountAction(trackedAccountId) {
if (!trackedAccountId) { if (!trackedAccountId) {
throw new Error("trackedAccountId is required"); 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"); rememberAction("当前后端暂不支持", "这套 live collector 还没有接入单账号跟踪同步。", "orange");
renderAll(); renderAll();
return; return;
} }
setBusy(true, "正在同步该跟踪账号..."); setBusy(true, "正在同步该跟踪账号...");
try { try {
const payload = await storyforgeFetch(`/v2/douyin/tracking/accounts/${encodeURIComponent(trackedAccountId)}/refresh`, { const payload = await storyforgeFetch(trackingRefreshPath, {
method: "POST" method: "POST"
}); });
const success = payload.success !== false; const success = payload.success !== false;
@@ -849,17 +1002,21 @@ function getReviewById(reviewId) {
function getContentSourcesForAccount(account) { function getContentSourcesForAccount(account) {
if (!account) return []; if (!account) return [];
const profileUrl = String(account.profile_url || "").trim(); const platform = getAccountPlatform(account);
const douyinId = String(account.douyin_id || "").trim(); const profileUrl = getAccountProfileUrl(account);
const nickname = String(account.nickname || "").trim(); const handle = getAccountHandle(account);
const nickname = getAccountName(account);
return safeArray(appState.contentSources).filter((source) => { return safeArray(appState.contentSources).filter((source) => {
const sourceUrl = String(source.source_url || "").trim(); 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 title = String(source.title || "").trim();
const sourcePlatform = normalizePlatformValue(source.platform || "", platform);
return ( return (
(profileUrl && sourceUrl === profileUrl) || sourcePlatform === platform && (
(douyinId && handle === douyinId) || (profileUrl && sourceUrl === profileUrl) ||
(nickname && title.includes(nickname)) (handle && sourceHandle === handle) ||
(nickname && title.includes(nickname))
)
); );
}); });
} }
@@ -1195,6 +1352,9 @@ function markSavedCandidate(candidate, links) {
async function saveCandidateAsBenchmark(candidateIndex, relationType = "benchmark") { async function saveCandidateAsBenchmark(candidateIndex, relationType = "benchmark") {
const account = requireSelectedAccountRow(); 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)]; const candidate = safeArray(appState.lastSimilaritySearch?.candidates)[Number(candidateIndex)];
if (!candidate) throw new Error("当前候选不存在,请先重新查相似"); if (!candidate) throw new Error("当前候选不存在,请先重新查相似");
const payload = { const payload = {
@@ -1207,7 +1367,7 @@ async function saveCandidateAsBenchmark(candidateIndex, relationType = "benchmar
if (!payload.target_account_ids.length && !payload.target_profile_urls.length) { if (!payload.target_account_ids.length && !payload.target_profile_urls.length) {
throw new Error("当前候选没有可保存的账号或主页链接"); throw new Error("当前候选没有可保存的账号或主页链接");
} }
const result = await storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(account.id)}/benchmark-links`, { const result = await storyforgeFetch(benchmarkPath, {
method: "POST", method: "POST",
body: payload body: payload
}); });
@@ -1411,10 +1571,10 @@ function renderDashboardScreen() {
${accounts.slice(0, 3).map((account) => ` ${accounts.slice(0, 3).map((account) => `
<div class="entity-card pad"> <div class="entity-card pad">
<div class="entity-cell"> <div class="entity-cell">
<div class="avatar-lg">${escapeHtml(initials(account.nickname || account.douyin_id))}</div> <div class="avatar-lg">${escapeHtml(initials(getAccountName(account)))}</div>
<div> <div>
<div class="cell-title">${escapeHtml(account.nickname || "未命名账号")}</div> <div class="cell-title">${escapeHtml(getAccountName(account))}</div>
<div class="cell-desc">${escapeHtml(account.signature || account.profile_url || "已同步抖音账号")}</div> <div class="cell-desc">${escapeHtml(account.signature || getAccountProfileUrl(account) || `已同步${platformLabel(getAccountPlatform(account))}账号`)}</div>
</div> </div>
</div> </div>
<div class="entity-meta"> <div class="entity-meta">
@@ -1536,17 +1696,20 @@ function renderProjectsScreen() {
function renderDiscoveryScreen() { function renderDiscoveryScreen() {
if (!appState.dashboard) { if (!appState.dashboard) {
return screenShell("找对标", "连接后端后才能加载真实对标账号。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("对标库未加载", "登录后这里会直接显示抖音账号列表和详情。")); return screenShell("找对标", "连接后端后才能加载真实对标账号。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("对标库未加载", "登录后这里会显示当前平台的账号列表和详情。"));
} }
const query = appState.discoveryQuery.toLowerCase(); const query = appState.discoveryQuery.toLowerCase();
const accounts = safeArray(appState.accounts).filter((account) => { const accounts = safeArray(appState.accounts).filter((account) => {
if (!query) return true; 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(" ") .join(" ")
.toLowerCase() .toLowerCase()
.includes(query); .includes(query);
}); });
const selected = getSelectedAccount(); 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 reports = safeArray(appState.selectedWorkspace?.recent_reports);
const linkedAccounts = safeArray(appState.selectedWorkspace?.linked_accounts); const linkedAccounts = safeArray(appState.selectedWorkspace?.linked_accounts);
const videos = safeArray(appState.selectedVideos?.items); const videos = safeArray(appState.selectedVideos?.items);
@@ -1560,18 +1723,20 @@ function renderDiscoveryScreen() {
const tracked = selected?.id ? isTrackedAccount(selected.id) : false; const tracked = selected?.id ? isTrackedAccount(selected.id) : false;
return screenShell( return screenShell(
"找对标", "找对标",
"这里已经接入真实抖音账号列表和单账号详情。", isWorkbenchPlatform(currentPlatform)
`${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")}`, ? `这里已经接入真实${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 || "" })}`,
` `
<div class="panel"> <div class="panel">
<div class="toolbar"> <div class="toolbar">
<div class="toolbar-stack"> <div class="toolbar-stack">
<label class="search search-inline"> <label class="search search-inline">
<span></span> <span></span>
<input data-action="discovery-query" value="${escapeHtml(appState.discoveryQuery)}" placeholder="搜账号名、主页链接、关键词" /> <input data-action="discovery-query" value="${escapeHtml(appState.discoveryQuery)}" placeholder="搜账号名、账号标识、主页链接、关键词" />
</label> </label>
<div class="filters"> <div class="filters">
<div class="filter">平台抖音</div> <div class="filter">平台${escapeHtml(currentPlatformLabel)}</div>
<div class="filter">账号数${escapeHtml(formatNumber(accounts.length))}</div> <div class="filter">账号数${escapeHtml(formatNumber(accounts.length))}</div>
<div class="filter">报告${escapeHtml(formatNumber(reports.length))}</div> <div class="filter">报告${escapeHtml(formatNumber(reports.length))}</div>
<div class="filter">作品${escapeHtml(formatNumber(effectiveVideos.length))}</div> <div class="filter">作品${escapeHtml(formatNumber(effectiveVideos.length))}</div>
@@ -1592,10 +1757,10 @@ function renderDiscoveryScreen() {
return ` return `
<button class="account-select-card ${active ? "is-active" : ""}" type="button" data-action="select-account" data-account-id="${escapeHtml(account.id)}"> <button class="account-select-card ${active ? "is-active" : ""}" type="button" data-action="select-account" data-account-id="${escapeHtml(account.id)}">
<div class="entity-cell"> <div class="entity-cell">
<div class="avatar-lg">${escapeHtml(initials(account.nickname || account.douyin_id))}</div> <div class="avatar-lg">${escapeHtml(initials(getAccountName(account)))}</div>
<div> <div>
<div class="cell-title">${escapeHtml(account.nickname || "未命名账号")}</div> <div class="cell-title">${escapeHtml(getAccountName(account))}</div>
<div class="cell-desc">${escapeHtml(account.douyin_id || "未填抖音号")}</div> <div class="cell-desc">${escapeHtml(getAccountSubtitle(account) || "未填账号标识")}</div>
</div> </div>
</div> </div>
<div class="kpi-inline"> <div class="kpi-inline">
@@ -1609,7 +1774,7 @@ function renderDiscoveryScreen() {
</div> </div>
</button> </button>
`; `;
}).join("") || `<div class="empty-state">当前没有抖音账号数据。</div>`} }).join("") || `<div class="empty-state">当前平台没有账号数据。</div>`}
</div> </div>
<div class="table-wrap"> <div class="table-wrap">
<table class="account-table"> <table class="account-table">
@@ -1628,10 +1793,10 @@ function renderDiscoveryScreen() {
<tr> <tr>
<td> <td>
<div class="entity-cell"> <div class="entity-cell">
<div class="avatar-lg">${escapeHtml(initials(account.nickname || account.douyin_id))}</div> <div class="avatar-lg">${escapeHtml(initials(getAccountName(account)))}</div>
<div> <div>
<div class="cell-title">${escapeHtml(account.nickname || "未命名账号")}</div> <div class="cell-title">${escapeHtml(getAccountName(account))}</div>
<div class="cell-desc">${escapeHtml(account.douyin_id || account.profile_url || "-")}</div> <div class="cell-desc">${escapeHtml(getAccountSubtitle(account) || "-")}</div>
</div> </div>
</div> </div>
</td> </td>
@@ -1641,20 +1806,20 @@ function renderDiscoveryScreen() {
<td>${escapeHtml(formatNumber(account.video_summary?.avg_like))}</td> <td>${escapeHtml(formatNumber(account.video_summary?.avg_like))}</td>
<td><div class="row-meta"><span class="tag" data-action="select-account" data-account-id="${escapeHtml(account.id)}">查看</span></div></td> <td><div class="row-meta"><span class="tag" data-action="select-account" data-account-id="${escapeHtml(account.id)}">查看</span></div></td>
</tr> </tr>
`).join("") || `<tr><td colspan="6">当前没有抖音账号数据。</td></tr>`} `).join("") || `<tr><td colspan="6">当前平台没有账号数据。</td></tr>`}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="layout-grid grid-main" style="padding:18px; border-top:1px solid var(--line);"> <div class="layout-grid grid-main" style="padding:18px; border-top:1px solid var(--line);">
<div class="side-stack"> <div class="side-stack">
<div class="panel pad" style="box-shadow:none;" id="selected-account-anchor"> <div class="panel pad" style="box-shadow:none;" id="selected-account-anchor">
<div class="panel-head"><div><h3>当前选中对标</h3><div class="panel-subtitle"></div></div><span class="tag blue">${escapeHtml(selected?.nickname || "")}</span></div> <div class="panel-head"><div><h3>当前选中对标</h3><div class="panel-subtitle"></div></div><span class="tag blue">${escapeHtml(getAccountName(selected) || "")}</span></div>
<div class="hero-card" style="padding:18px;"> <div class="hero-card" style="padding:18px;">
<div class="entity-cell"> <div class="entity-cell">
<div class="avatar-lg">${escapeHtml(initials(selected?.nickname || "SF"))}</div> <div class="avatar-lg">${escapeHtml(initials(getAccountName(selected) || "SF"))}</div>
<div> <div>
<h3>${escapeHtml(selected?.nickname || "还没有选中账号")}</h3> <h3>${escapeHtml(getAccountName(selected) || "还没有选中账号")}</h3>
<p>${escapeHtml(selected?.profile_url || selected?.signature || "左侧点一个账号,这里会展示详情。")}</p> <p>${escapeHtml(getAccountProfileUrl(selected) || selected?.signature || "左侧点一个账号,这里会展示详情。")}</p>
</div> </div>
</div> </div>
<div class="mobile-only compact-summary-row" style="margin-top:14px;"> <div class="mobile-only compact-summary-row" style="margin-top:14px;">
@@ -1686,13 +1851,14 @@ function renderDiscoveryScreen() {
` : `<div class="task-item"><h4>还没有选中账号</h4><p>先从左侧列表选一个对标账号,再决定是否导入到当前项目。</p></div>`} ` : `<div class="task-item"><h4>还没有选中账号</h4><p>先从左侧列表选一个对标账号,再决定是否导入到当前项目。</p></div>`}
</div> </div>
<div class="three-col"> <div class="three-col">
<div class="insight-card"> <div class="insight-card">
<h4>账号画像</h4> <h4>账号画像</h4>
<ul> <ul>
<li>${escapeHtml(selected?.signature || "暂无签名")}</li> <li>${escapeHtml(selected?.signature || "暂无签名")}</li>
<li>${escapeHtml("标签" + safeArray(selected?.tags).slice(0, 4).join(" / ") || "暂无标签")}</li> <li>${escapeHtml("平台" + currentPlatformLabel)}</li>
<li>${escapeHtml("同步状态" + (selected?.sync_status || "-"))}</li> <li>${escapeHtml("标签" + safeArray(selected?.tags).slice(0, 4).join(" / ") || "暂无标签")}</li>
</ul> <li>${escapeHtml("同步状态:" + (selected?.sync_status || "-"))}</li>
</ul>
</div> </div>
<div class="insight-card"> <div class="insight-card">
<h4>高分作品</h4> <h4>高分作品</h4>
@@ -1774,12 +1940,14 @@ function renderTrackingScreen() {
if (!appState.dashboard) { if (!appState.dashboard) {
return screenShell("跟踪账号", "登录后才能生成真实日报。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("日报未加载", "当前还没有可用的对标账号数据。")); 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( return screenShell(
"跟踪账号", "跟踪账号",
"当前 live collector 还没有接入跟踪日报接口。", `${getPendingWorkbenchReason(currentPlatform)}`,
`${button("跳到找对标", "goto-discovery", "primary")}`, `${button("跳到找对标", "goto-discovery", "primary")}`,
renderEmptyState("跟踪能力暂未接入", "这套后端还缺 /v2/douyin/tracking/*,等 live collector 同步后这里会自动切成真实日报。") renderEmptyState("跟踪能力暂未接入", `这套后端还没有接入 ${platformLabel(currentPlatform)} 跟踪接口 live collector 同步后这里会自动切成真实日报`)
); );
} }
const trackedAccounts = safeArray(appState.trackingAccounts); const trackedAccounts = safeArray(appState.trackingAccounts);
@@ -1787,7 +1955,7 @@ function renderTrackingScreen() {
const cursorLabel = appState.lastSeenAt ? formatDateTime(appState.lastSeenAt) : "尚未记录"; const cursorLabel = appState.lastSeenAt ? formatDateTime(appState.lastSeenAt) : "尚未记录";
return screenShell( return screenShell(
"跟踪账号", "跟踪账号",
"这里已经接上真实跟踪对象和按上次打开后的更新日报。", `这里已经接上真实${getPlatformShortLabel(currentPlatform)}跟踪对象和按上次打开后的更新日报`,
`${button("同步全部", "refresh-tracking")} ${button("标记已读", "mark-tracking-read")} ${button("跳到找对标", "goto-discovery", "primary")}`, `${button("同步全部", "refresh-tracking")} ${button("标记已读", "mark-tracking-read")} ${button("跳到找对标", "goto-discovery", "primary")}`,
` `
<div class="hero-card"> <div class="hero-card">
@@ -2475,6 +2643,7 @@ function openImportHomepageAction() {
function openImportSelectedAccountAction() { function openImportSelectedAccountAction() {
const account = requireSelectedAccountRow(); const account = requireSelectedAccountRow();
const platform = getAccountPlatform(account);
const project = requireSelectedProject(); const project = requireSelectedProject();
const assistants = getAssistantOptions(project.id); const assistants = getAssistantOptions(project.id);
const currentSources = getCurrentProjectSourcesForAccount(account, project.id); const currentSources = getCurrentProjectSourcesForAccount(account, project.id);
@@ -2488,10 +2657,10 @@ function openImportSelectedAccountAction() {
submitLabel: currentSource ? "继续同步" : "导入并同步", submitLabel: currentSource ? "继续同步" : "导入并同步",
fields: [ fields: [
{ name: "projectId", label: "归属项目", type: "select", value: project.id, options: getProjectOptions() }, { name: "projectId", label: "归属项目", type: "select", value: project.id, options: getProjectOptions() },
{ name: "platform", label: "平台", type: "select", value: normalizePlatformValue(currentSource?.platform || "douyin"), options: getPlatformOptions() }, { name: "platform", label: "平台", type: "select", value: normalizePlatformValue(currentSource?.platform || platform), options: getPlatformOptions() },
{ name: "title", label: "内容源标题", value: currentSource?.title || `${account.nickname || account.douyin_id || "对标账号"} 对标主页` }, { name: "title", label: "内容源标题", value: currentSource?.title || `${getAccountName(account)} 对标主页` },
{ name: "handle", label: "账号标识", value: currentSource?.handle || account.douyin_id || "" }, { name: "handle", label: "账号标识", value: currentSource?.handle || getAccountHandle(account) || "" },
{ name: "sourceUrl", label: "主页链接", type: "url", value: currentSource?.source_url || account.profile_url || "", placeholder: "https://..." }, { 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: "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: "maxItems", label: "最多同步作品数", type: "number", value: Number(currentSource?.metadata?.max_items || 6), min: 1, max: 20 },
{ name: "skipExisting", label: "跳过已存在作品", type: "checkbox", value: true }, { name: "skipExisting", label: "跳过已存在作品", type: "checkbox", value: true },
@@ -2511,7 +2680,7 @@ function openImportSelectedAccountAction() {
platform, platform,
handle: values.handle || "", handle: values.handle || "",
source_url: values.sourceUrl.trim(), source_url: values.sourceUrl.trim(),
title: values.title || values.handle || account.nickname || "对标主页", title: values.title || values.handle || getAccountName(account) || "对标主页",
metadata: { metadata: {
imported_from_account_id: account.id, imported_from_account_id: account.id,
imported_from_workspace: "discovery" imported_from_workspace: "discovery"
@@ -2526,15 +2695,15 @@ function openImportSelectedAccountAction() {
assistant_id: values.assistantId || "", assistant_id: values.assistantId || "",
content_source_id: source.id, content_source_id: source.id,
platform, platform,
handle: values.handle || account.douyin_id || "", handle: values.handle || getAccountHandle(account) || "",
source_url: values.sourceUrl.trim(), 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), max_items: Number(values.maxItems || 6),
skip_existing: Boolean(values.skipExisting), skip_existing: Boolean(values.skipExisting),
auto_trigger_analysis: Boolean(values.autoAnalyze) 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(); await bootstrap();
} }
}); });
@@ -2542,6 +2711,13 @@ function openImportSelectedAccountAction() {
function openTrackSelectedAccountAction() { function openTrackSelectedAccountAction() {
const account = requireSelectedAccountRow(); const account = requireSelectedAccountRow();
const platform = getAccountPlatform(account);
const trackingAccountsPath = getWorkbenchRoute(platform, "trackingAccounts");
if (!trackingAccountsPath) {
rememberAction("当前平台待接入", getPendingWorkbenchReason(platform), "orange");
renderAll();
return;
}
const project = requireSelectedProject(); const project = requireSelectedProject();
const assistants = getAssistantOptions(project.id); const assistants = getAssistantOptions(project.id);
const trackedItem = safeArray(appState.trackingAccounts).find((item) => item.tracked_account_id === account.id); const trackedItem = safeArray(appState.trackingAccounts).find((item) => item.tracked_account_id === account.id);
@@ -2552,12 +2728,12 @@ function openTrackSelectedAccountAction() {
: "把当前对标账号加入每日跟踪,后续自动生成更新日报。", : "把当前对标账号加入每日跟踪,后续自动生成更新日报。",
submitLabel: trackedItem ? "保存跟踪" : "开始跟踪", submitLabel: trackedItem ? "保存跟踪" : "开始跟踪",
fields: [ fields: [
{ name: "accountName", label: "账号", type: "html", html: `<div class="sheet-html"><strong>${escapeHtml(account.nickname || account.douyin_id || "未命名账号")}</strong><p>${escapeHtml(account.profile_url || account.signature || "")}</p></div>` }, { name: "accountName", label: "账号", type: "html", html: `<div class="sheet-html"><strong>${escapeHtml(getAccountName(account) || "未命名账号")}</strong><p>${escapeHtml(getAccountProfileUrl(account) || account.signature || "")}</p></div>` },
{ name: "assistantId", label: "负责 Agent", type: "select", value: trackedItem?.assistant_id || getSelectedAssistant()?.id || assistants[0]?.value || "", options: [{ value: "", label: "先不绑定" }, ...assistants] }, { 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: "例如:重点观察开头结构、成交句式和更新频率" } { name: "note", label: "跟踪备注", value: trackedItem?.note || "", placeholder: "例如:重点观察开头结构、成交句式和更新频率" }
], ],
onSubmit: async (values) => { onSubmit: async (values) => {
await storyforgeFetch("/v2/douyin/tracking/accounts", { await storyforgeFetch(trackingAccountsPath, {
method: "POST", method: "POST",
body: { body: {
tracked_account_id: account.id, tracked_account_id: account.id,
@@ -2565,7 +2741,7 @@ function openTrackSelectedAccountAction() {
note: values.note || "" note: values.note || ""
} }
}); });
rememberAction(trackedItem ? "跟踪已更新" : "已加入跟踪", `账号「${account.nickname || account.douyin_id || "当前对标"}」现在会进入更新日报。`, "green"); rememberAction(trackedItem ? "跟踪已更新" : "已加入跟踪", `账号「${getAccountName(account) || "当前对标"}」现在会进入更新日报。`, "green");
await bootstrap(); await bootstrap();
} }
}); });
@@ -2748,6 +2924,13 @@ function openEditAssistantAction(assistantId = "") {
function openAnalyzeSelectedAccountAction() { function openAnalyzeSelectedAccountAction() {
const account = requireSelectedAccountRow(); const account = requireSelectedAccountRow();
const platform = getAccountPlatform(account);
const analyzePath = getWorkbenchRoute(platform, "analyzeAccount", account.id);
if (!analyzePath) {
rememberAction("当前平台待接入", getPendingWorkbenchReason(platform), "orange");
renderAll();
return;
}
openActionModal({ openActionModal({
title: "分析当前对标账号", title: "分析当前对标账号",
description: "从商业化和内容运营角度重跑一次账号分析。", description: "从商业化和内容运营角度重跑一次账号分析。",
@@ -2759,7 +2942,7 @@ function openAnalyzeSelectedAccountAction() {
{ name: "topVideoCount", label: "高分作品分析数", type: "number", value: 4, min: 1, max: 10 } { name: "topVideoCount", label: "高分作品分析数", type: "number", value: 4, min: 1, max: 10 }
], ],
onSubmit: async (values) => { onSubmit: async (values) => {
const result = await storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(account.id)}/analysis`, { const result = await storyforgeFetch(analyzePath, {
method: "POST", method: "POST",
body: { body: {
model_profile_ids: [], model_profile_ids: [],
@@ -2775,7 +2958,7 @@ function openAnalyzeSelectedAccountAction() {
}); });
const summary = result?.suggestions?.[0]?.parsed_json?.executive_summary || result?.suggestions?.[0]?.suggestion_text || "已生成新的账号分析。"; const summary = result?.suggestions?.[0]?.parsed_json?.executive_summary || result?.suggestions?.[0]?.suggestion_text || "已生成新的账号分析。";
rememberAction("对标账号分析完成", brief(summary, 120), "green", result); rememberAction("对标账号分析完成", brief(summary, 120), "green", result);
await loadDouyinAccount(account.id); await loadPlatformAccount(platform, account.id);
renderAll(); renderAll();
} }
}); });
@@ -2783,7 +2966,9 @@ function openAnalyzeSelectedAccountAction() {
function openAnalyzeTopVideosAction() { function openAnalyzeTopVideosAction() {
const account = requireSelectedAccountRow(); 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"); rememberAction("当前后端暂不支持", "这套 live collector 还没有接入高分作品批量分析。", "orange");
renderAll(); renderAll();
return; return;
@@ -2797,7 +2982,7 @@ function openAnalyzeTopVideosAction() {
{ name: "minScore", label: "最低分阈值", type: "number", value: 45, min: 0, max: 100 } { name: "minScore", label: "最低分阈值", type: "number", value: 45, min: 0, max: 100 }
], ],
onSubmit: async (values) => { onSubmit: async (values) => {
const result = await storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(account.id)}/videos/analyze-top`, { const result = await storyforgeFetch(analyzePath, {
method: "POST", method: "POST",
body: { body: {
model_profile_id: "", model_profile_id: "",
@@ -2807,7 +2992,7 @@ function openAnalyzeTopVideosAction() {
} }
}); });
rememberAction("高分作品分析完成", `已补分析 ${formatNumber(result.analyzed_count)} 条高分作品。`, "green", result); rememberAction("高分作品分析完成", `已补分析 ${formatNumber(result.analyzed_count)} 条高分作品。`, "green", result);
await loadDouyinAccount(account.id); await loadPlatformAccount(platform, account.id);
renderAll(); renderAll();
} }
}); });
@@ -2815,6 +3000,13 @@ function openAnalyzeTopVideosAction() {
function openSimilaritySearchAction() { function openSimilaritySearchAction() {
const account = requireSelectedAccountRow(); const account = requireSelectedAccountRow();
const platform = getAccountPlatform(account);
const createPath = getWorkbenchRoute(platform, "similarSearches");
if (!createPath) {
rememberAction("当前平台待接入", getPendingWorkbenchReason(platform), "orange");
renderAll();
return;
}
openActionModal({ openActionModal({
title: "查相似账号", title: "查相似账号",
description: "让 Agent 基于当前账号画像找更多可借鉴对象。", description: "让 Agent 基于当前账号画像找更多可借鉴对象。",
@@ -2824,7 +3016,7 @@ function openSimilaritySearchAction() {
{ name: "extraRequirements", label: "额外要求", type: "textarea", rows: 4, placeholder: "例如:优先找创业成交类、口播结构强的账号" } { name: "extraRequirements", label: "额外要求", type: "textarea", rows: 4, placeholder: "例如:优先找创业成交类、口播结构强的账号" }
], ],
onSubmit: async (values) => { onSubmit: async (values) => {
const created = await storyforgeFetch("/v2/douyin/similar-searches", { const created = await storyforgeFetch(createPath, {
method: "POST", method: "POST",
body: { body: {
source_account_id: account.id, source_account_id: account.id,
@@ -2837,12 +3029,13 @@ function openSimilaritySearchAction() {
} }
}); });
const searchId = created.id || created.search_id; const searchId = created.id || created.search_id;
const detailPath = searchId ? getWorkbenchRoute(platform, "similarSearchDetail", searchId) : "";
const detail = searchId const detail = searchId
? await storyforgeFetch(`/v2/douyin/similar-searches/${encodeURIComponent(searchId)}`) ? await storyforgeFetch(detailPath)
: created; : created;
appState.lastSimilaritySearch = detail; appState.lastSimilaritySearch = detail;
rememberAction("相似账号已生成", `已生成 ${formatNumber(safeArray(detail.candidates).length)} 个候选账号。`, "green", detail); rememberAction("相似账号已生成", `已生成 ${formatNumber(safeArray(detail.candidates).length)} 个候选账号。`, "green", detail);
await loadDouyinAccount(account.id); await loadPlatformAccount(platform, account.id);
renderAll(); renderAll();
} }
}); });
@@ -2850,9 +3043,16 @@ function openSimilaritySearchAction() {
function openBenchmarkLinkAction(defaults = {}) { function openBenchmarkLinkAction(defaults = {}) {
const account = requireSelectedAccountRow(); 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) const options = safeArray(appState.accounts)
.filter((item) => item.id !== account.id) .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" const candidate = typeof defaults.candidateIndex === "number"
? safeArray(appState.lastSimilaritySearch?.candidates)[defaults.candidateIndex] || null ? safeArray(appState.lastSimilaritySearch?.candidates)[defaults.candidateIndex] || null
: null; : null;
@@ -2872,7 +3072,7 @@ function openBenchmarkLinkAction(defaults = {}) {
], ],
onSubmit: async (values) => { onSubmit: async (values) => {
if (!values.targetAccountId && !values.targetProfileUrl?.trim()) throw new Error("请先选择一个目标账号或填写主页链接"); 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", method: "POST",
body: { body: {
target_account_ids: values.targetAccountId ? [values.targetAccountId] : [], target_account_ids: values.targetAccountId ? [values.targetAccountId] : [],
@@ -3409,7 +3609,8 @@ document.addEventListener("click", async (event) => {
if (!accountId) return; if (!accountId) return;
setBusy(true, "正在加载对标详情..."); setBusy(true, "正在加载对标详情...");
try { try {
await loadDouyinAccount(accountId); const account = safeArray(appState.accounts).find((item) => item.id === accountId) || null;
await loadPlatformAccount(getAccountPlatform(account), accountId);
renderAll(); renderAll();
} catch (error) { } catch (error) {
alert("加载对标详情失败: " + error.message); alert("加载对标详情失败: " + error.message);