diff --git a/web/storyforge-web-v4/README.md b/web/storyforge-web-v4/README.md index a8a03da..0c23314 100644 --- a/web/storyforge-web-v4/README.md +++ b/web/storyforge-web-v4/README.md @@ -48,6 +48,8 @@ - 导入文本素材并触发分析 - 上传本地视频并触发分析 - 创建 Agent +- 选择当前 Agent +- 编辑 Agent 的名称、目标、系统提示词和主模型 - 对当前 Douyin 对标账号重跑分析 - 批量分析高分作品 - 查找相似对标账号 @@ -62,6 +64,7 @@ - 在生产中心 / 发布与复盘常驻最近一次任务详情摘要 - 在 Web 中直接创建和编辑复盘 - 在页面里直接看到 `本机模型 / cutvideo / huobao / n8n / ASR` 的真实健康状态 +- 会先识别后端是否具备 `tracking / reviews / integrations` 路由,再决定是否请求,避免不同版本 live collector 刷 404 - 依赖不可达时,自动拦住 AI 视频 / 实拍剪辑动作并展示原因 - 使用 Agent 生成文案 - 创建 AI 视频任务 diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index aab5987..d3ba72f 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -25,6 +25,7 @@ const appState = { reviews: [], integrationHealth: null, localModelCatalog: null, + backendCapabilities: null, busy: false, message: "", lastAction: null, @@ -488,6 +489,21 @@ async function storyforgeFetch(path, options = {}) { return payload; } +async function loadBackendCapabilities(backendUrl) { + const normalizedUrl = (backendUrl || DEFAULT_BACKEND_URL).replace(/\/$/, ""); + const response = await fetch(`${normalizedUrl}/openapi.json`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const payload = await response.json(); + return new Set(Object.keys(payload.paths || {})); +} + +function backendSupports(path) { + if (!(appState.backendCapabilities instanceof Set)) return true; + return appState.backendCapabilities.has(path); +} + async function loginWithForm() { const auth = readAuthForm(); if (!auth.backendUrl) { @@ -540,6 +556,7 @@ async function logoutSession() { appState.trackingDigest = null; appState.reviews = []; appState.integrationHealth = null; + appState.backendCapabilities = null; appState.lastAction = null; appState.lastGeneratedCopy = null; appState.lastSimilaritySearch = null; @@ -598,25 +615,37 @@ async function bootstrap() { renderAll(); return; } + appState.backendCapabilities = await loadBackendCapabilities(appState.session.backendUrl).catch(() => null); + const supportsTracking = backendSupports("/v2/douyin/tracking/accounts"); + const supportsTrackingDigest = backendSupports("/v2/douyin/tracking/digest"); + const supportsReviews = backendSupports("/v2/reviews"); + const supportsIntegrationHealth = backendSupports("/v2/integrations/health"); + const supportsLocalModels = backendSupports("/v2/integrations/local-models"); const [dashboard, contentSources, accounts, trackingAccountsPayload, reviews, integrationHealth, localModelCatalog] = await Promise.all([ storyforgeFetch("/v2/me/dashboard"), storyforgeFetch("/v2/content-sources").catch(() => []), storyforgeFetch("/v2/douyin/accounts").catch(() => []), - storyforgeFetch("/v2/douyin/tracking/accounts").catch(() => ({ items: [], cursor_last_seen_at: "" })), - storyforgeFetch("/v2/reviews").catch(() => []), - storyforgeFetch("/v2/integrations/health").catch(() => null), - storyforgeFetch("/v2/integrations/local-models").catch(() => null) + supportsTracking ? storyforgeFetch("/v2/douyin/tracking/accounts").catch(() => ({ items: [], cursor_last_seen_at: "" })) : Promise.resolve({ items: [], cursor_last_seen_at: "" }), + supportsReviews ? storyforgeFetch("/v2/reviews").catch(() => []) : Promise.resolve([]), + supportsIntegrationHealth ? storyforgeFetch("/v2/integrations/health").catch(() => null) : Promise.resolve(null), + supportsLocalModels ? storyforgeFetch("/v2/integrations/local-models").catch(() => null) : Promise.resolve(null) ]); const trackingCursorLastSeenAt = trackingAccountsPayload?.cursor_last_seen_at || ""; if (trackingCursorLastSeenAt) { setLastSeenAt(trackingCursorLastSeenAt); } const trackingSince = trackingCursorLastSeenAt || getTrackingSinceIso(); - const trackingDigest = await storyforgeFetch(`/v2/douyin/tracking/digest?since=${encodeURIComponent(trackingSince)}&limit=24`).catch(() => ({ + const trackingDigest = supportsTrackingDigest + ? await storyforgeFetch(`/v2/douyin/tracking/digest?since=${encodeURIComponent(trackingSince)}&limit=24`).catch(() => ({ + items: [], + tracked_accounts: [], + cursor_last_seen_at: trackingCursorLastSeenAt + })) + : ({ items: [], tracked_accounts: [], cursor_last_seen_at: trackingCursorLastSeenAt - })); + }); appState.dashboard = dashboard; appState.contentSources = safeArray(contentSources); appState.accounts = safeArray(accounts); @@ -650,6 +679,11 @@ async function bootstrap() { } async function markTrackingDigestRead() { + if (!backendSupports("/v2/douyin/tracking/cursor")) { + rememberAction("当前后端暂不支持", "这套 live collector 还没有接入跟踪已读游标。", "orange"); + renderAll(); + return; + } const nextSeenAt = new Date().toISOString(); await storyforgeFetch("/v2/douyin/tracking/cursor", { method: "POST", @@ -659,6 +693,11 @@ async function markTrackingDigestRead() { } async function refreshTrackingAccountsAction() { + if (!backendSupports("/v2/douyin/tracking/refresh")) { + rememberAction("当前后端暂不支持", "这套 live collector 还没有接入批量跟踪同步。", "orange"); + renderAll(); + return; + } setBusy(true, "正在同步跟踪账号..."); try { const payload = await storyforgeFetch("/v2/douyin/tracking/refresh", { @@ -680,6 +719,11 @@ async function refreshTrackedAccountAction(trackedAccountId) { if (!trackedAccountId) { throw new Error("trackedAccountId is required"); } + if (!backendSupports("/v2/douyin/tracking/accounts/{tracked_account_id}/refresh")) { + rememberAction("当前后端暂不支持", "这套 live collector 还没有接入单账号跟踪同步。", "orange"); + renderAll(); + return; + } setBusy(true, "正在同步该跟踪账号..."); try { const payload = await storyforgeFetch(`/v2/douyin/tracking/accounts/${encodeURIComponent(trackedAccountId)}/refresh`, { @@ -1689,6 +1733,14 @@ function renderTrackingScreen() { if (!appState.dashboard) { return screenShell("跟踪账号", "登录后才能生成真实日报。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("日报未加载", "当前还没有可用的对标账号数据。")); } + if (!backendSupports("/v2/douyin/tracking/accounts")) { + return screenShell( + "跟踪账号", + "当前 live collector 还没有接入跟踪日报接口。", + `${button("跳到找对标", "goto-discovery", "primary")}`, + renderEmptyState("跟踪能力暂未接入", "这套后端还缺 /v2/douyin/tracking/*,等 live collector 同步后这里会自动切成真实日报。") + ); + } const trackedAccounts = safeArray(appState.trackingAccounts); const digestItems = getTrackingDigestItems(12); const cursorLabel = appState.lastSeenAt ? formatDateTime(appState.lastSeenAt) : "尚未记录"; @@ -1845,11 +1897,12 @@ function renderPlaybookScreen() { const assistants = safeArray(appState.dashboard.assistants); const models = safeArray(appState.dashboard.model_profiles); const currentModel = getCurrentModelProfile(); + const currentAssistant = getSelectedAssistant(); const localCatalog = appState.localModelCatalog || {}; const gatewayModels = safeArray(localCatalog.models).map((item) => item.id).filter(Boolean); return screenShell( "Agent", - "这里接真实 Agent 列表,后面再继续补创建和编辑动作。", + "这里接真实 Agent 列表,当前已经支持切换和编辑 Agent。", `${button("设主模型", "open-preferred-model")} ${button("新建 Agent", "open-create-assistant")} ${button("生成文案", "open-generate-copy")} ${button("去生产", "goto-production", "primary")}`, `
${escapeHtml(currentAssistant.generation_goal || currentAssistant.description || "先补齐这个 Agent 的目标和说明。")}
+ +先创建一个 Agent,再把当前项目的内容都交给它学习。
${escapeHtml(assistant.description || assistant.generation_goal || "暂无说明")}
下一步可以直接把创建动作接进来。