feat: surface video recorder and recent ai video engine
Some checks failed
StoryForge CI / Baseline checks (push) Has been cancelled
StoryForge CI / Backend tests (push) Has been cancelled
StoryForge CI / Web tests (push) Has been cancelled

This commit is contained in:
kris
2026-04-07 16:13:12 +08:00
parent f0ce9ed80c
commit 4f3ca3f20f
5 changed files with 63 additions and 12 deletions

View File

@@ -686,3 +686,5 @@
- 顶层 `AI 视频 / 实拍剪辑` 主按钮改回“先开配置表单”,会自动承接最近完成任务作为默认来源,但不再直接跳过配置页;只有任务上下文里的 `做 AI 视频 / 做实拍剪辑` 仍保持 direct-execute。 - 顶层 `AI 视频 / 实拍剪辑` 主按钮改回“先开配置表单”,会自动承接最近完成任务作为默认来源,但不再直接跳过配置页;只有任务上下文里的 `做 AI 视频 / 做实拍剪辑` 仍保持 direct-execute。
- `AI 视频` 表单新增 `Seedance 配置` 提示,明确说明当前 `Seedance 2.0` 走火山视频配置,默认应在 `Huobao /settings/ai-config -> 视频 -> 火山引擎` 配置;如果不用页面配置,也支持通过 `HUOBAO_VIDEO_BASE_URL / HUOBAO_VIDEO_API_KEY / HUOBAO_VIDEO_MODELS` 环境变量覆盖。 - `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` 任务为什么只建单不出片的歧义。 - `integrations/health` 新增 `huobao` 视频配置摘要,能直接看出当前 `Huobao` 视频配置页是否已经录入视频引擎配置,以及对应的配置页路径,减少排查 `Seedance` 任务为什么只建单不出片的歧义。
- 首页 `1 主 2 次` 动作里把 `视频录制` 抬成了高频次级动作,当前项目有生产任务时能更快进入录制维护入口。
- `AI 视频` 表单开始直接显示“当前项目最近使用的视频引擎”,像 `Seedance 2.0 · seedance-2.0-pro` 这类信息会在打开表单时直接可见,并保留跳到火山配置状态的入口。

View File

@@ -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 `
<div class="sheet-html">
<div class="task-item compact">
<h4>当前项目最近视频引擎</h4>
<p>这个项目还没有记录最近一次视频引擎选择,当前会先沿用默认引擎。</p>
</div>
</div>
`;
}
const providerLabel = normalizedProvider === "seedance2" ? "Seedance 2.0" : "当前默认引擎";
return `
<div class="sheet-html">
<div class="task-item compact">
<h4>当前项目最近视频引擎</h4>
<p>${escapeHtml(`上次创建 AI 视频时使用的是 ${providerLabel}${normalizedModel ? ` · ${normalizedModel}` : ""}`)}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(providerLabel)}</span>
${normalizedModel ? `<span class="tag">${escapeHtml(normalizedModel)}</span>` : ""}
<span class="tag clickable-tag" data-action="focus-huobao-video-config">查看火山配置状态</span>
</div>
</div>
</div>
`;
}
function bindAiVideoProviderRecommendations(fields, options = {}) { function bindAiVideoProviderRecommendations(fields, options = {}) {
const providerSelect = fields.querySelector('[data-action-field="videoProvider"]'); const providerSelect = fields.querySelector('[data-action-field="videoProvider"]');
const modelInput = fields.querySelector('[data-action-field="videoModel"]'); const modelInput = fields.querySelector('[data-action-field="videoModel"]');
@@ -12215,6 +12245,7 @@ function openCreateAiVideoAction(defaults = {}) {
submitLabel: "开始生产", submitLabel: "开始生产",
fields: [ fields: [
{ name: "context", label: "当前上下文", type: "html", html: renderIntakeActionContextHtml(project.id, defaultAssistantId) }, { 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) }] : []), ...(sourceJob ? [{ name: "sourceJobContext", label: "来源任务", type: "html", html: renderSourceJobContextHtml(sourceJob) }] : []),
{ name: "title", label: "任务标题", value: defaults.title || recommendDerivativeJobTitle(sourceJob, "AI 视频", ""), placeholder: "例如:创业口播 AI 视频测试" }, { 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: "写明主题、风格、镜头和目标受众" }, { name: "brief", label: "视频 brief", type: "textarea", rows: 5, value: defaults.brief || getJobSeedBrief(sourceJob), placeholder: "写明主题、风格、镜头和目标受众" },

View File

@@ -130,18 +130,30 @@
intentKey: "custom", intentKey: "custom",
planSteps: ["读取当前待执行生产任务", "确认素材和结论完整性", "给出执行确认建议"] planSteps: ["读取当前待执行生产任务", "确认素材和结论完整性", "给出执行确认建议"]
}); });
actions.push({
title: "继续处理视频录制",
reason: "录制源、导入配置和运行状态都集中在这里,适合先补录或维护。",
goAction: "focus-live-recorder-maintenance",
goLabel: "去录制",
agentLabel: "交给主 Agent",
sourceActionKey: "homepage-secondary-action-recorder",
intentKey: "custom",
planSteps: ["读取视频录制状态", "识别录制阻塞项", "生成下一步处理动作"]
});
} }
actions.push({ if (actions.length < 3) {
title: "更新重点账号的跟踪摘要", actions.push({
reason: "有新动态,但不值得占据大块首页空间。", title: "更新重点账号的跟踪摘要",
goAction: "goto-tracking", reason: "有新动态,但不值得占据大块首页空间。",
goLabel: "去处理", goAction: "goto-tracking",
agentLabel: "交给主 Agent", goLabel: "去处理",
sourceActionKey: `homepage-secondary-action-${Math.max(actions.length, 1)}`, agentLabel: "交给主 Agent",
intentKey: "track_account", sourceActionKey: `homepage-secondary-action-${Math.max(actions.length, 1)}`,
planSteps: ["读取重点账号近 7 天动态", "更新跟踪摘要", "给出是否继续跟进建议"] intentKey: "track_account",
}); planSteps: ["读取重点账号近 7 天动态", "更新跟踪摘要", "给出是否继续跟进建议"]
});
}
while (actions.length < 3) { while (actions.length < 3) {
actions.push({ actions.push({

View File

@@ -46,7 +46,7 @@ test("homepage v6 puts actions before overview and uses 1-primary-2-secondary st
}, },
secondaryActions: [ secondaryActions: [
{ title: "确认一个待执行的生产计划", reason: "素材和结论都在,只差最后确认。", goAction: "goto-production", goLabel: "去处理" }, { title: "确认一个待执行的生产计划", reason: "素材和结论都在,只差最后确认。", goAction: "goto-production", goLabel: "去处理" },
{ title: "更新重点账号的跟踪摘要", reason: "有新动态,但不值得占据大块首页空间。", goAction: "goto-tracking", goLabel: "去处理" } { title: "继续处理视频录制", reason: "录制源、导入配置和运行状态都集中在这里,适合先补录或维护。", goAction: "focus-live-recorder-maintenance", goLabel: "去录制" }
], ],
overviewBodyHtml: "<section>这里只展示当前 tab 的核心状态。</section>" overviewBodyHtml: "<section>这里只展示当前 tab 的核心状态。</section>"
}); });
@@ -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.ok(html.indexOf("今天先做什么") < html.indexOf("项目概览"));
assert.match(html, /先补抖音重点对标的高分作品分析/); assert.match(html, /先补抖音重点对标的高分作品分析/);
assert.match(html, /确认一个待执行的生产计划/); assert.match(html, /确认一个待执行的生产计划/);
assert.match(html, /更新重点账号的跟踪摘要/); assert.match(html, /继续处理视频录制/);
assert.match(html, /data-action="handoff-to-main-agent"/); 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.actionSourceLabel, "规则推荐");
assert.equal(model.secondaryActions.length, 2); assert.equal(model.secondaryActions.length, 2);
assert.match(model.primaryAction.title, /高分作品分析|继续补高分对标/); 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", () => { test("homepage overview uses tab buttons and does not render legacy repeated sections", () => {

View File

@@ -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, /const AI_VIDEO_DEFAULTS_KEY = STORAGE_KEY \+ ":ai-video-defaults"/);
assert.match(APP, /function getAiVideoPreferences\(projectId = ""\)/); assert.match(APP, /function getAiVideoPreferences\(projectId = ""\)/);
assert.match(APP, /function saveAiVideoPreferences\(projectId = "", values = \{\}\)/); 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, /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, /modelInput\.placeholder = provider === "seedance2" \? "例如seedance-2\.0-pro" : "留空则沿用当前默认视频模型"/);
assert.match(APP, /bindAiVideoProviderRecommendations\(fields, \{ seedanceModel: defaultVideoModel \|\| "seedance-2\.0-pro" \}\)/); assert.match(APP, /bindAiVideoProviderRecommendations\(fields, \{ seedanceModel: defaultVideoModel \|\| "seedance-2\.0-pro" \}\)/);
assert.match(APP, /saveAiVideoPreferences\(project\.id, \{/); assert.match(APP, /saveAiVideoPreferences\(project\.id, \{/);