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");