feat: generalize web workbench platform routing
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user