From 76affab96ba762629be91d37cbec4604f692ce1a Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 7 Apr 2026 12:44:50 +0800 Subject: [PATCH] feat: refresh live recorder sheet defaults --- CHANGELOG.md | 6 ++ web/storyforge-web-v4/assets/app.js | 82 +++++++++++++++++++ .../tests/workbench-pages.test.mjs | 4 + 3 files changed, 92 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b6a1f2..6a41ae2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ - `来源任务` 摘要区也会跟着联动更新,切换任务后第一眼就能看到当前承接的是哪条任务。 - 为了支持这层联动,输入型表单里的 HTML 字段现在也带了稳定的 `data-action-field` 标记,后续继续做表单智能化和回归锁定会更稳。 +### 直播录制表单开始跟随项目和平台动态刷新 + +- `新增录制源 / 编辑录制源` 现在会在切换项目或平台时动态刷新录制名称占位,并同步更新可选 Agent 列表,不再停留在打开表单时的默认值。 +- `导入 URL 配置` 现在会在切换平台时实时刷新说明文案和样例配置,抖音/快手两种场景可以直接在同一张表单里切换预设。 +- 这套联动同样保留“手动改过就不再覆盖”的原则,避免自动推荐把用户已经输入的内容冲掉。 + ## 2026-04-06 ### 主 Agent 高注意图动作统一切到直执行入口 diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 61e314a..db38f82 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -593,6 +593,67 @@ function bindCreativeSourceJobRecommendations(fields, options = {}) { sync(); } +function bindLiveRecorderSheetRecommendations(fields, options = {}) { + const projectSelect = fields.querySelector('[data-action-field="projectId"]'); + const platformSelect = fields.querySelector('[data-action-field="platform"]'); + const titleInput = fields.querySelector('[data-action-field="title"]'); + const assistantSelect = fields.querySelector('[data-action-field="assistantId"]'); + const rawTextarea = fields.querySelector('[data-action-field="raw"]'); + const defaultPlatform = options.defaultPlatform || "kuaishou"; + const syncAssistants = () => { + if (!(assistantSelect instanceof HTMLSelectElement) || !(projectSelect instanceof HTMLSelectElement)) return; + const projectId = projectSelect.value || ""; + const assistants = getAssistantOptions(projectId); + const currentValue = assistantSelect.value || ""; + assistantSelect.innerHTML = [ + { value: "", label: "暂不绑定" }, + ...assistants + ].map((item) => ` + + `).join(""); + const stillExists = safeArray(assistants).some((item) => item.value === currentValue); + if (!stillExists && currentValue) { + assistantSelect.value = ""; + } + }; + const syncTitle = () => { + if (!(titleInput instanceof HTMLInputElement)) return; + const projectId = projectSelect instanceof HTMLSelectElement ? projectSelect.value : ""; + const project = safeArray(appState.dashboard?.projects).find((item) => item.id === projectId) + || getSelectedProject() + || null; + const platform = normalizePlatformValue( + platformSelect instanceof HTMLSelectElement ? platformSelect.value : getPreferredPlatform(), + defaultPlatform + ); + titleInput.placeholder = recommendLiveRecorderTitle(project, platform); + }; + const syncRawSamples = () => { + if (!(rawTextarea instanceof HTMLTextAreaElement)) return; + if (rawTextarea.dataset.recommendationMode === "manual") return; + const platform = normalizePlatformValue( + platformSelect instanceof HTMLSelectElement ? platformSelect.value : getPreferredPlatform(), + defaultPlatform + ); + rawTextarea.dataset.recommendationApplying = "1"; + rawTextarea.value = recommendLiveRecorderImportSamples(platform); + rawTextarea.dataset.recommendationMode = "auto"; + delete rawTextarea.dataset.recommendationApplying; + }; + rawTextarea?.addEventListener("input", () => { + if (rawTextarea.dataset.recommendationApplying === "1") return; + rawTextarea.dataset.recommendationMode = "manual"; + }); + const sync = () => { + syncAssistants(); + syncTitle(); + syncRawSamples(); + }; + projectSelect?.addEventListener("change", sync); + platformSelect?.addEventListener("change", sync); + sync(); +} + function formatDateTime(value) { if (!value) return "-"; const date = new Date(value); @@ -11887,6 +11948,9 @@ function openLiveRecorderCreateAction() { { name: "sourceUrl", label: "直播源", type: "url", placeholder: "https://..." }, { name: "autoStart", label: "导入后立即开始", type: "checkbox", value: true } ], + onOpen: ({ fields }) => { + bindLiveRecorderSheetRecommendations(fields, { defaultPlatform }); + }, onSubmit: async (values) => { if (!values.sourceUrl?.trim()) throw new Error("请填写直播源链接"); const saved = await storyforgeFetch("/v2/live-recorder/sources", { @@ -11956,6 +12020,9 @@ function openLiveRecorderSourceAction(sourceId) { { name: "quality", label: "清晰度", type: "select", value: source.quality || "原画", options: ["原画", "蓝光", "超清", "高清", "标清", "流畅"].map((item) => ({ value: item, label: item })) }, { name: "enabled", label: "启用录制源", type: "checkbox", value: Boolean(source.enabled) } ], + onOpen: ({ fields }) => { + bindLiveRecorderSheetRecommendations(fields, { defaultPlatform: source.platform || "kuaishou" }); + }, onSubmit: async (values) => { const saved = await storyforgeFetch(`/v2/live-recorder/sources/${encodeURIComponent(source.id)}`, { method: "PATCH", @@ -11986,6 +12053,7 @@ function openLiveRecorderImportAction() { submitLabel: "导入并同步", fields: [ { name: "context", label: "当前上下文", type: "html", html: renderIntakeActionContextHtml(project?.id || "", defaultAssistantId) }, + { name: "platform", label: "平台", type: "select", value: defaultPlatform, options: getPlatformOptions() }, { name: "raw", label: "配置文本", @@ -11995,6 +12063,20 @@ function openLiveRecorderImportAction() { placeholder: "一行一个 URL,支持 # 注释和 逗号分隔的清晰度/标题" } ], + onOpen: ({ fields, description }) => { + bindLiveRecorderSheetRecommendations(fields, { defaultPlatform }); + const platformSelect = fields.querySelector('[data-action-field="platform"]'); + const syncDescription = () => { + if (!(description instanceof HTMLElement)) return; + const platform = normalizePlatformValue( + platformSelect instanceof HTMLSelectElement ? platformSelect.value : getPreferredPlatform(), + defaultPlatform + ); + description.textContent = `按行粘贴${platformLabel(platform)}直播源,支持用逗号附带清晰度和标题,注释行会被视为停用源。`; + }; + platformSelect?.addEventListener("change", syncDescription); + syncDescription(); + }, onSubmit: async (values) => { if (!String(values.raw || "").trim()) throw new Error("请先粘贴配置文本"); const saved = await storyforgeFetch("/v2/live-recorder/url-config/import", { diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index b200ff5..f1a3b87 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -1197,6 +1197,7 @@ test("input-heavy intake sheets surface current context and smarter defaults", ( assert.match(APP, /function recommendManualIntakeTitle\(project, platform, kind\)/); assert.match(APP, /function bindManualIntakeTitleRecommendation\(fields, kind, options = \{\}\)/); assert.match(APP, /function bindCreativeSourceJobRecommendations\(fields, options = \{\}\)/); + assert.match(APP, /function bindLiveRecorderSheetRecommendations\(fields, options = \{\}\)/); assert.match(importHomepage, /label: "当前上下文", type: "html"/); assert.match(importHomepage, /const defaultAssistantId = getSelectedAssistant\(\)\?\.id \|\| assistants\[0\]\?\.value \|\| ""/); assert.match(importHomepage, /const defaultPlatform = normalizePlatformValue\(getPreferredPlatform\(\), "douyin"\)/); @@ -1263,17 +1264,20 @@ test("input-heavy intake sheets surface current context and smarter defaults", ( assert.match(liveRecorderCreate, /const defaultTitle = recommendLiveRecorderTitle\(project, defaultPlatform\)/); assert.match(liveRecorderCreate, /renderIntakeActionContextHtml\(project\?\.id \|\| "", defaultAssistantId\)/); assert.match(liveRecorderCreate, /value: defaultTitle, placeholder: defaultTitle/); + assert.match(liveRecorderCreate, /onOpen:\s*\(\{ fields \}\) => \{\s*bindLiveRecorderSheetRecommendations\(fields, \{/); assert.match(liveRecorderSource, /label: "当前上下文", type: "html"/); assert.match(liveRecorderSource, /const defaultAssistantId = source\.assistant_id \|\| getSelectedAssistant\(\)\?\.id \|\| assistants\[0\]\?\.value \|\| ""/); assert.match(liveRecorderSource, /const titlePlaceholder = recommendLiveRecorderTitle\(currentProject, source\.platform \|\| "kuaishou"\)/); assert.match(liveRecorderSource, /renderIntakeActionContextHtml\(currentProject\?\.id \|\| source\.project_id \|\| "", defaultAssistantId\)/); assert.match(liveRecorderSource, /placeholder: titlePlaceholder/); + assert.match(liveRecorderSource, /onOpen:\s*\(\{ fields \}\) => \{\s*bindLiveRecorderSheetRecommendations\(fields, \{/); assert.match(liveRecorderImport, /label: "当前上下文", type: "html"/); assert.match(liveRecorderImport, /const defaultAssistantId = getSelectedAssistant\(\)\?\.id \|\| assistants\[0\]\?\.value \|\| ""/); assert.match(liveRecorderImport, /const defaultPlatform = normalizePlatformValue\(getPreferredPlatform\(\), "kuaishou"\)/); assert.match(liveRecorderImport, /const samples = recommendLiveRecorderImportSamples\(defaultPlatform\)/); assert.match(liveRecorderImport, /renderIntakeActionContextHtml\(project\?\.id \|\| "", defaultAssistantId\)/); assert.match(liveRecorderImport, /按行粘贴\$\{platformLabel\(defaultPlatform\)\}直播源/); + assert.match(liveRecorderImport, /onOpen:\s*\(\{ fields, description \}\) => \{\s*bindLiveRecorderSheetRecommendations\(fields, \{/); assert.match(reviewAction, /label: "当前上下文", type: "html"/); assert.match(reviewAction, /const defaultAssistantId = getSelectedAssistant\(\)\?\.id \|\| assistants\[0\]\?\.value \|\| ""/); assert.match(reviewAction, /renderIntakeActionContextHtml\(project\.id, defaultAssistantId\)/);