From 4f3ca3f20f5fd3f1a16f144bb7ce801e91406737 Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 7 Apr 2026 16:13:12 +0800 Subject: [PATCH] feat: surface video recorder and recent ai video engine --- CHANGELOG.md | 2 ++ web/storyforge-web-v4/assets/app.js | 31 ++++++++++++++++++ .../assets/storyforge-dashboard-home.js | 32 +++++++++++++------ .../tests/dashboard-home.test.mjs | 6 ++-- .../tests/workbench-pages.test.mjs | 4 +++ 5 files changed, 63 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a218a2..779562d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -686,3 +686,5 @@ - 顶层 `AI 视频 / 实拍剪辑` 主按钮改回“先开配置表单”,会自动承接最近完成任务作为默认来源,但不再直接跳过配置页;只有任务上下文里的 `做 AI 视频 / 做实拍剪辑` 仍保持 direct-execute。 - `AI 视频` 表单新增 `Seedance 配置` 提示,明确说明当前 `Seedance 2.0` 走火山视频配置,默认应在 `Huobao /settings/ai-config -> 视频 -> 火山引擎` 配置;如果不用页面配置,也支持通过 `HUOBAO_VIDEO_BASE_URL / HUOBAO_VIDEO_API_KEY / HUOBAO_VIDEO_MODELS` 环境变量覆盖。 - `integrations/health` 新增 `huobao` 视频配置摘要,能直接看出当前 `Huobao` 视频配置页是否已经录入视频引擎配置,以及对应的配置页路径,减少排查 `Seedance` 任务为什么只建单不出片的歧义。 +- 首页 `1 主 2 次` 动作里把 `视频录制` 抬成了高频次级动作,当前项目有生产任务时能更快进入录制维护入口。 +- `AI 视频` 表单开始直接显示“当前项目最近使用的视频引擎”,像 `Seedance 2.0 · seedance-2.0-pro` 这类信息会在打开表单时直接可见,并保留跳到火山配置状态的入口。 diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 709e45e..dd9b77e 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -12158,6 +12158,36 @@ function renderAiVideoProviderHintHtml(provider = "doubao") { `; } +function renderAiVideoProviderMemoryHtml(projectId = "", preferences = {}) { + const normalizedProjectId = String(projectId || "").trim(); + const normalizedProvider = String(preferences.videoProvider || "").trim(); + const normalizedModel = String(preferences.videoModel || "").trim(); + if (!normalizedProjectId || !normalizedProvider) { + return ` +
+
+

当前项目最近视频引擎

+

这个项目还没有记录最近一次视频引擎选择,当前会先沿用默认引擎。

+
+
+ `; + } + const providerLabel = normalizedProvider === "seedance2" ? "Seedance 2.0" : "当前默认引擎"; + return ` +
+
+

当前项目最近视频引擎

+

${escapeHtml(`上次创建 AI 视频时使用的是 ${providerLabel}${normalizedModel ? ` · ${normalizedModel}` : ""}。`)}

+
+ ${escapeHtml(providerLabel)} + ${normalizedModel ? `${escapeHtml(normalizedModel)}` : ""} + 查看火山配置状态 +
+
+
+ `; +} + function bindAiVideoProviderRecommendations(fields, options = {}) { const providerSelect = fields.querySelector('[data-action-field="videoProvider"]'); const modelInput = fields.querySelector('[data-action-field="videoModel"]'); @@ -12215,6 +12245,7 @@ function openCreateAiVideoAction(defaults = {}) { submitLabel: "开始生产", fields: [ { name: "context", label: "当前上下文", type: "html", html: renderIntakeActionContextHtml(project.id, defaultAssistantId) }, + { name: "recentVideoProvider", label: "最近使用", type: "html", html: renderAiVideoProviderMemoryHtml(project.id, persistedDefaults) }, ...(sourceJob ? [{ name: "sourceJobContext", label: "来源任务", type: "html", html: renderSourceJobContextHtml(sourceJob) }] : []), { name: "title", label: "任务标题", value: defaults.title || recommendDerivativeJobTitle(sourceJob, "AI 视频", ""), placeholder: "例如:创业口播 AI 视频测试" }, { name: "brief", label: "视频 brief", type: "textarea", rows: 5, value: defaults.brief || getJobSeedBrief(sourceJob), placeholder: "写明主题、风格、镜头和目标受众" }, diff --git a/web/storyforge-web-v4/assets/storyforge-dashboard-home.js b/web/storyforge-web-v4/assets/storyforge-dashboard-home.js index 7bc1bc3..a67fff4 100644 --- a/web/storyforge-web-v4/assets/storyforge-dashboard-home.js +++ b/web/storyforge-web-v4/assets/storyforge-dashboard-home.js @@ -130,18 +130,30 @@ intentKey: "custom", planSteps: ["读取当前待执行生产任务", "确认素材和结论完整性", "给出执行确认建议"] }); + actions.push({ + title: "继续处理视频录制", + reason: "录制源、导入配置和运行状态都集中在这里,适合先补录或维护。", + goAction: "focus-live-recorder-maintenance", + goLabel: "去录制", + agentLabel: "交给主 Agent", + sourceActionKey: "homepage-secondary-action-recorder", + intentKey: "custom", + planSteps: ["读取视频录制状态", "识别录制阻塞项", "生成下一步处理动作"] + }); } - actions.push({ - title: "更新重点账号的跟踪摘要", - reason: "有新动态,但不值得占据大块首页空间。", - goAction: "goto-tracking", - goLabel: "去处理", - agentLabel: "交给主 Agent", - sourceActionKey: `homepage-secondary-action-${Math.max(actions.length, 1)}`, - intentKey: "track_account", - planSteps: ["读取重点账号近 7 天动态", "更新跟踪摘要", "给出是否继续跟进建议"] - }); + if (actions.length < 3) { + actions.push({ + title: "更新重点账号的跟踪摘要", + reason: "有新动态,但不值得占据大块首页空间。", + goAction: "goto-tracking", + goLabel: "去处理", + agentLabel: "交给主 Agent", + sourceActionKey: `homepage-secondary-action-${Math.max(actions.length, 1)}`, + intentKey: "track_account", + planSteps: ["读取重点账号近 7 天动态", "更新跟踪摘要", "给出是否继续跟进建议"] + }); + } while (actions.length < 3) { actions.push({ diff --git a/web/storyforge-web-v4/tests/dashboard-home.test.mjs b/web/storyforge-web-v4/tests/dashboard-home.test.mjs index 6b8e677..3a6bedc 100644 --- a/web/storyforge-web-v4/tests/dashboard-home.test.mjs +++ b/web/storyforge-web-v4/tests/dashboard-home.test.mjs @@ -46,7 +46,7 @@ test("homepage v6 puts actions before overview and uses 1-primary-2-secondary st }, secondaryActions: [ { title: "确认一个待执行的生产计划", reason: "素材和结论都在,只差最后确认。", goAction: "goto-production", goLabel: "去处理" }, - { title: "更新重点账号的跟踪摘要", reason: "有新动态,但不值得占据大块首页空间。", goAction: "goto-tracking", goLabel: "去处理" } + { title: "继续处理视频录制", reason: "录制源、导入配置和运行状态都集中在这里,适合先补录或维护。", goAction: "focus-live-recorder-maintenance", goLabel: "去录制" } ], overviewBodyHtml: "
这里只展示当前 tab 的核心状态。
" }); @@ -56,7 +56,7 @@ test("homepage v6 puts actions before overview and uses 1-primary-2-secondary st assert.ok(html.indexOf("今天先做什么") < html.indexOf("项目概览")); assert.match(html, /先补抖音重点对标的高分作品分析/); assert.match(html, /确认一个待执行的生产计划/); - assert.match(html, /更新重点账号的跟踪摘要/); + assert.match(html, /继续处理视频录制/); assert.match(html, /data-action="handoff-to-main-agent"/); }); @@ -77,6 +77,8 @@ test("homepage model builds one primary action, two secondary actions, and a rul assert.equal(model.actionSourceLabel, "规则推荐"); assert.equal(model.secondaryActions.length, 2); assert.match(model.primaryAction.title, /高分作品分析|继续补高分对标/); + assert.match(model.secondaryActions.map((item) => item.title).join(" "), /确认一个待执行的生产计划/); + assert.match(model.secondaryActions.map((item) => item.title).join(" "), /继续处理视频录制/); }); test("homepage overview uses tab buttons and does not render legacy repeated sections", () => { diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 3c9d4d7..2fad29c 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -1129,7 +1129,11 @@ test("ai video form explains where Seedance 火山配置 lives", () => { assert.match(APP, /const AI_VIDEO_DEFAULTS_KEY = STORAGE_KEY \+ ":ai-video-defaults"/); assert.match(APP, /function getAiVideoPreferences\(projectId = ""\)/); assert.match(APP, /function saveAiVideoPreferences\(projectId = "", values = \{\}\)/); + assert.match(APP, /function renderAiVideoProviderMemoryHtml\(projectId = "", preferences = \{\}\)/); assert.match(APP, /const persistedDefaults = getAiVideoPreferences\(project\.id\)/); + assert.match(APP, /label: "最近使用", type: "html", html: renderAiVideoProviderMemoryHtml\(project\.id, persistedDefaults\)/); + assert.match(APP, /当前项目最近视频引擎/); + assert.match(APP, /上次创建 AI 视频时使用的是/); assert.match(APP, /modelInput\.placeholder = provider === "seedance2" \? "例如:seedance-2\.0-pro" : "留空则沿用当前默认视频模型"/); assert.match(APP, /bindAiVideoProviderRecommendations\(fields, \{ seedanceModel: defaultVideoModel \|\| "seedance-2\.0-pro" \}\)/); assert.match(APP, /saveAiVideoPreferences\(project\.id, \{/);