diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 9c01dab..be1836c 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -35,7 +35,7 @@ const appState = { lastJobDetail: null }; -const INTEGRATION_ORDER = ["local_model", "cutvideo", "huobao", "n8n", "asr"]; +const INTEGRATION_ORDER = ["local_model", "live_recorder", "cutvideo", "huobao", "n8n", "asr"]; const ACTIVE_PLATFORMS = [ { value: "douyin", label: "抖音" }, { value: "xiaohongshu", label: "小红书" }, @@ -44,50 +44,54 @@ const ACTIVE_PLATFORMS = [ { value: "wechat_video", label: "微信视频号" } ]; const ACTIVE_PLATFORM_CHIPS = ["全平台", "抖音", "小红书", "B站", "快手", "视频号"]; +function makePlatformRoutes(platform) { + return { + accounts: `/v2/${platform}/accounts`, + workspace: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/workspace`, + videos: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/videos?limit=80`, + analyzeAccount: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/analysis`, + analyzeTopVideos: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/videos/analyze-top`, + similarSearches: `/v2/${platform}/similar-searches`, + similarSearchDetail: (searchId) => `/v2/${platform}/similar-searches/${encodeURIComponent(searchId)}`, + benchmarkLinks: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/benchmark-links`, + trackingAccounts: `/v2/${platform}/tracking/accounts`, + trackingDigest: `/v2/${platform}/tracking/digest`, + trackingRefresh: `/v2/${platform}/tracking/refresh`, + trackingCursor: `/v2/${platform}/tracking/cursor`, + trackingAccountRefresh: (trackedAccountId) => `/v2/${platform}/tracking/accounts/${encodeURIComponent(trackedAccountId)}/refresh` + }; +} + const PLATFORM_REGISTRY = { douyin: { label: "抖音", shortLabel: "抖音", workbenchReady: true, - routes: { - 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` - } + routes: makePlatformRoutes("douyin") }, xiaohongshu: { label: "小红书", shortLabel: "小红书", - workbenchReady: false, - pendingText: "小红书工作台待接入" + workbenchReady: true, + routes: makePlatformRoutes("xiaohongshu") }, bilibili: { label: "哔哩哔哩", shortLabel: "B站", - workbenchReady: false, - pendingText: "B站工作台待接入" + workbenchReady: true, + routes: makePlatformRoutes("bilibili") }, kuaishou: { label: "快手", shortLabel: "快手", - workbenchReady: false, - pendingText: "快手工作台待接入" + workbenchReady: true, + routes: makePlatformRoutes("kuaishou") }, wechat_video: { label: "微信视频号", shortLabel: "视频号", - workbenchReady: false, - pendingText: "视频号工作台待接入" + workbenchReady: true, + routes: makePlatformRoutes("wechat_video") } }; const INTEGRATION_META = { @@ -96,6 +100,11 @@ const INTEGRATION_META = { hint: "OpenAI-compatible", impacts: ["账号分析", "高分分析", "文案生成"] }, + live_recorder: { + label: "直播录制", + hint: "fnOS NAS", + impacts: ["直播源导入", "录制控制"] + }, cutvideo: { label: "自动剪辑", hint: "Windows cutvideo", @@ -1251,6 +1260,10 @@ function getIntegrationCards() { `设主模型` ].filter(Boolean).join(""); } + if (key === "live_recorder") { + extra = detail.baseUrl ? `服务地址:${detail.baseUrl}` : "当前未配置 NAS 录制服务"; + actions = `录制控制`; + } return { key, meta, @@ -1286,7 +1299,7 @@ function getIntegrationOverview() { ? `自动链路受阻:${blockedActions.length} 项` : `${reachableCount}/${cards.length} 项依赖在线`; const subtitle = !availableCount - ? "刷新后会显示 cutvideo / huobao / n8n / ASR 的真实状态。" + ? "刷新后会显示直播录制 / cutvideo / huobao / n8n / ASR 的真实状态。" : blockedActions.length ? blockedActions.join(";") : "AI 视频与实拍剪辑链路当前可直接发起。"; @@ -3333,6 +3346,38 @@ function openCreateRealCutAction(defaults = {}) { }); } +function openLiveRecorderAction() { + const status = getIntegrationDetail("live_recorder"); + openActionModal({ + title: "直播录制控制", + description: status.reachable + ? "把直播间链接导入到 NAS 录制服务,必要时直接触发开始录制。" + : "当前 NAS 录制服务不可达,先检查集成健康。", + submitLabel: "提交到录制服务", + fields: [ + { name: "raw", label: "直播源", type: "textarea", rows: 4, value: "原画,https://live.kuaishou.com/u/storyforge_anchor", placeholder: "一行一条,例如:原画,https://live.kuaishou.com/u/anchor" }, + { name: "autoStart", label: "导入后立即开始", type: "checkbox", value: true } + ], + onSubmit: async (values) => { + if (!values.raw?.trim()) throw new Error("请填写直播源链接"); + const imported = await storyforgeFetch("/v2/live-recorder/url-config/import", { + method: "POST", + body: { raw: values.raw.trim() } + }); + let started = null; + if (values.autoStart) { + try { + started = await storyforgeFetch("/v2/live-recorder/recorder/start", { method: "POST", body: {} }); + } catch (error) { + started = { ok: false, message: error.message }; + } + } + rememberAction("直播录制已下发", "NAS 录制服务已接收最新直播源。", "green", { imported, started }); + await bootstrap(); + } + }); +} + function openReviewAction(defaults = {}) { const project = requireSelectedProject(); const assistants = getAssistantOptions(project.id); @@ -3449,6 +3494,10 @@ document.addEventListener("click", async (event) => { await refreshTrackingAccountsAction(); return; } + if (name === "open-live-recorder") { + openLiveRecorderAction(); + return; + } if (name === "mark-tracking-read") { await markTrackingDigestRead(); rememberAction("日报已标记", "当前跟踪摘要已更新为已读,下次会从新的时间点继续汇总。", "green");