feat: remember ai video provider per project
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:08:18 +08:00
parent 8dce288e3a
commit f0ce9ed80c
3 changed files with 47 additions and 2 deletions

View File

@@ -16,6 +16,12 @@
- `Seedance 配置` 提示也会随引擎切换即时更新,表单第一眼就能看出这次走的是默认视频链,还是 `Seedance 2.0 -> 火山视频配置` - `Seedance 配置` 提示也会随引擎切换即时更新,表单第一眼就能看出这次走的是默认视频链,还是 `Seedance 2.0 -> 火山视频配置`
- 这套联动同样保留“手动改过就不再自动覆盖”的原则,避免把用户已经输入的模型名冲掉。 - 这套联动同样保留“手动改过就不再自动覆盖”的原则,避免把用户已经输入的模型名冲掉。
### AI 视频开始按项目记忆最近一次视频引擎
- `创建 AI 视频任务` 现在会按项目记住你最近一次使用的 `视频引擎 / 引擎模型`
- 如果某个项目最近一次就是用 `Seedance 2.0 + seedance-2.0-pro`,下次再打开这张表单时会优先带出这套组合,不用每次重新选。
- 这套记忆只在当前项目内生效,不会把一个项目的视频引擎偏好串到别的项目上。
### 修复额度页套餐建议引起的全局渲染报错 ### 修复额度页套餐建议引起的全局渲染报错
- `额度` 页面现在会先初始化 `packageRecommendation` 再渲染套餐建议,不再因为变量未定义把整个工作台渲染链打断。 - `额度` 页面现在会先初始化 `packageRecommendation` 再渲染套餐建议,不再因为变量未定义把整个工作台渲染链打断。

View File

@@ -2,6 +2,7 @@ const STORAGE_KEY = "storyforge-web-v4-session";
const SESSION_STORE = StoryForgeSessionStore.create(STORAGE_KEY); const SESSION_STORE = StoryForgeSessionStore.create(STORAGE_KEY);
const DEFAULT_BACKEND_URL = StoryForgeApiClient.detectDefaultBackendUrl(); const DEFAULT_BACKEND_URL = StoryForgeApiClient.detectDefaultBackendUrl();
const RECOVERY_HISTORY_KEY = STORAGE_KEY + ":recovery-history"; const RECOVERY_HISTORY_KEY = STORAGE_KEY + ":recovery-history";
const AI_VIDEO_DEFAULTS_KEY = STORAGE_KEY + ":ai-video-defaults";
const navButtons = document.querySelectorAll("[data-screen-target]"); const navButtons = document.querySelectorAll("[data-screen-target]");
const screens = Array.from(document.querySelectorAll("[data-screen]")); const screens = Array.from(document.querySelectorAll("[data-screen]"));
@@ -578,6 +579,34 @@ function getCompletedJobById(jobId = "") {
|| null; || null;
} }
function getAiVideoPreferences(projectId = "") {
const normalizedProjectId = String(projectId || "").trim();
if (!normalizedProjectId) return {};
try {
const payload = JSON.parse(localStorage.getItem(AI_VIDEO_DEFAULTS_KEY) || "{}");
const saved = payload && typeof payload === "object" ? payload[normalizedProjectId] : null;
return saved && typeof saved === "object" ? saved : {};
} catch {
return {};
}
}
function saveAiVideoPreferences(projectId = "", values = {}) {
const normalizedProjectId = String(projectId || "").trim();
if (!normalizedProjectId) return;
try {
const payload = JSON.parse(localStorage.getItem(AI_VIDEO_DEFAULTS_KEY) || "{}");
const nextPayload = payload && typeof payload === "object" ? payload : {};
nextPayload[normalizedProjectId] = {
videoProvider: String(values.videoProvider || "doubao").trim() || "doubao",
videoModel: String(values.videoModel || "").trim()
};
localStorage.setItem(AI_VIDEO_DEFAULTS_KEY, JSON.stringify(nextPayload));
} catch {
// ignore local preference persistence failures
}
}
function bindCreativeSourceJobRecommendations(fields, options = {}) { function bindCreativeSourceJobRecommendations(fields, options = {}) {
const sourceJobSelect = fields.querySelector('[data-action-field="sourceJobId"]'); const sourceJobSelect = fields.querySelector('[data-action-field="sourceJobId"]');
if (!(sourceJobSelect instanceof HTMLSelectElement)) return; if (!(sourceJobSelect instanceof HTMLSelectElement)) return;
@@ -12173,11 +12202,12 @@ function openCreateAiVideoAction(defaults = {}) {
const defaultAssistantId = assistant?.id || ""; const defaultAssistantId = assistant?.id || "";
const sourceJob = defaults.sourceJob || null; const sourceJob = defaults.sourceJob || null;
const defaultPlatform = normalizePlatformValue(defaults.platform || sourceJob?.platform || "douyin"); const defaultPlatform = normalizePlatformValue(defaults.platform || sourceJob?.platform || "douyin");
const persistedDefaults = getAiVideoPreferences(project.id);
const defaultVideoProvider = String( const defaultVideoProvider = String(
defaults.videoProvider || defaults.video_provider || sourceJob?.artifacts?.video_provider || "doubao" defaults.videoProvider || defaults.video_provider || sourceJob?.artifacts?.video_provider || persistedDefaults.videoProvider || "doubao"
).trim() || "doubao"; ).trim() || "doubao";
const defaultVideoModel = String( const defaultVideoModel = String(
defaults.videoModel || defaults.video_model || sourceJob?.artifacts?.video_model || "" defaults.videoModel || defaults.video_model || sourceJob?.artifacts?.video_model || persistedDefaults.videoModel || ""
).trim() || (defaultVideoProvider === "seedance2" ? "seedance-2.0-pro" : ""); ).trim() || (defaultVideoProvider === "seedance2" ? "seedance-2.0-pro" : "");
openActionModal({ openActionModal({
title: "创建 AI 视频任务", title: "创建 AI 视频任务",
@@ -12282,6 +12312,10 @@ function openCreateAiVideoAction(defaults = {}) {
video_model: values.videoModel || "", video_model: values.videoModel || "",
} }
}); });
saveAiVideoPreferences(project.id, {
videoProvider: values.videoProvider || "doubao",
videoModel: values.videoModel || ""
});
rememberAction( rememberAction(
"AI 视频任务已创建", "AI 视频任务已创建",
`已创建任务 ${job.title || job.id},引擎 ${normalizedProvider === "seedance2" ? "Seedance 2.0" : "当前默认引擎"}`, `已创建任务 ${job.title || job.id},引擎 ${normalizedProvider === "seedance2" ? "Seedance 2.0" : "当前默认引擎"}`,

View File

@@ -1126,8 +1126,13 @@ test("ai video form explains where Seedance 火山配置 lives", () => {
assert.match(APP, /id="integration-\$\{escapeHtml\(item\.key\)\}-anchor"/); assert.match(APP, /id="integration-\$\{escapeHtml\(item\.key\)\}-anchor"/);
assert.match(APP, /function focusAutomationHealthWorkspace\(anchorId = "integration-huobao-anchor"\)/); assert.match(APP, /function focusAutomationHealthWorkspace\(anchorId = "integration-huobao-anchor"\)/);
assert.match(APP, /function bindAiVideoProviderRecommendations\(fields, options = \{\}\)/); assert.match(APP, /function bindAiVideoProviderRecommendations\(fields, options = \{\}\)/);
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, /const persistedDefaults = getAiVideoPreferences\(project\.id\)/);
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(clickActions, /name === "focus-huobao-video-config"[\s\S]*focusAutomationHealthWorkspace\("integration-huobao-anchor"\)/); assert.match(clickActions, /name === "focus-huobao-video-config"[\s\S]*focusAutomationHealthWorkspace\("integration-huobao-anchor"\)/);
}); });