From 3cf56a4db60a0d4846c9c2c4c60ea76ac55b7bca Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 7 Apr 2026 12:59:13 +0800 Subject: [PATCH] feat: sync intake context on project changes --- CHANGELOG.md | 6 +++ web/storyforge-web-v4/assets/app.js | 42 +++++++++++++++++++ .../tests/workbench-pages.test.mjs | 7 ++++ 3 files changed, 55 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a41ae2..fc21b8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,12 @@ - `导入 URL 配置` 现在会在切换平台时实时刷新说明文案和样例配置,抖音/快手两种场景可以直接在同一张表单里切换预设。 - 这套联动同样保留“手动改过就不再覆盖”的原则,避免自动推荐把用户已经输入的内容冲掉。 +### 输入型表单切项目时会同步刷新 Agent 和上下文 + +- `导入主页 / 导入当前对标 / 加入跟踪 / 导入作品链接 / 导入文本 / 上传本地视频` 这几张输入型表单,现在在切换项目后会一起刷新可选 Agent 列表和顶部“当前上下文”摘要。 +- 这样不会再出现“项目已经换了,但表单里还是上一项目的 Agent 和上下文”的错位。 +- `加入跟踪` 虽然没有项目切换,但现在在切换负责 Agent 时,顶部上下文摘要也会实时更新。 + ## 2026-04-06 ### 主 Agent 高注意图动作统一切到直执行入口 diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index b7fcac6..1dddf9f 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -503,6 +503,38 @@ function bindManualIntakeTitleRecommendation(fields, kind, options = {}) { sync(); } +function bindActionContextRecommendation(fields, options = {}) { + const contextHtml = fields.querySelector('[data-action-field="context"] .sheet-html'); + const assistantSelect = fields.querySelector('[data-action-field="assistantId"]'); + const projectField = options.projectField === undefined ? "projectId" : options.projectField; + const projectSelect = projectField ? fields.querySelector(`[data-action-field="${projectField}"]`) : null; + const defaultAssistantId = options.defaultAssistantId || ""; + const syncAssistantOptions = () => { + if (!(assistantSelect instanceof HTMLSelectElement) || !(projectSelect instanceof HTMLSelectElement)) return; + const assistants = getAssistantOptions(projectSelect.value || ""); + const currentValue = assistantSelect.value || ""; + const fallbackValue = assistants.some((item) => item.value === currentValue) + ? currentValue + : assistants.some((item) => item.value === defaultAssistantId) + ? defaultAssistantId + : assistants[0]?.value || ""; + assistantSelect.innerHTML = [{ value: "", label: "暂不绑定" }, ...assistants].map((item) => ` + + `).join(""); + assistantSelect.value = fallbackValue; + }; + const sync = () => { + syncAssistantOptions(); + if (!(contextHtml instanceof HTMLElement)) return; + const projectId = projectSelect instanceof HTMLSelectElement ? projectSelect.value : getSelectedProject()?.id || ""; + const assistantId = assistantSelect instanceof HTMLSelectElement ? assistantSelect.value : defaultAssistantId; + contextHtml.innerHTML = renderIntakeActionContextHtml(projectId, assistantId); + }; + projectSelect?.addEventListener("change", sync); + assistantSelect?.addEventListener("change", sync); + sync(); +} + function getCompletedJobById(jobId = "") { const normalizedId = String(jobId || "").trim(); if (!normalizedId) return null; @@ -9534,6 +9566,7 @@ function openImportHomepageAction() { submitLabel: "开始同步", onOpen: ({ fields }) => { bindManualIntakeTitleRecommendation(fields, "主页对标", { defaultPlatform: "douyin" }); + bindActionContextRecommendation(fields, { defaultAssistantId }); }, fields: [ { name: "context", label: "当前上下文", type: "html", html: renderIntakeActionContextHtml(project.id, defaultAssistantId) }, @@ -9596,6 +9629,9 @@ function openImportSelectedAccountAction() { { name: "skipExisting", label: "跳过已存在作品", type: "checkbox", value: true }, { name: "autoAnalyze", label: "同步后自动分析", type: "checkbox", value: true } ], + onOpen: ({ fields }) => { + bindActionContextRecommendation(fields, { defaultAssistantId }); + }, onSubmit: async (values) => { if (!values.sourceUrl?.trim()) throw new Error("请先填写主页链接"); const projectId = values.projectId || project.id; @@ -9638,6 +9674,9 @@ function openTrackSelectedAccountAction() { { name: "assistantId", label: "负责 Agent", type: "select", value: defaultAssistantId, options: [{ value: "", label: "先不绑定" }, ...assistants] }, { name: "note", label: "跟踪备注", value: trackedItem?.note || "", placeholder: "例如:重点观察开头结构、成交句式和更新频率" } ], + onOpen: ({ fields }) => { + bindActionContextRecommendation(fields, { defaultAssistantId, projectField: "" }); + }, onSubmit: async (values) => { await runDirectDiscoveryAction("track-account", { target_account_id: account.id, @@ -9666,6 +9705,7 @@ function openImportVideoLinkAction() { submitLabel: "开始分析", onOpen: ({ fields }) => { bindManualIntakeTitleRecommendation(fields, "单条作品", { defaultPlatform: "douyin" }); + bindActionContextRecommendation(fields, { defaultAssistantId }); }, fields: [ { name: "context", label: "当前上下文", type: "html", html: renderIntakeActionContextHtml(project.id, defaultAssistantId) }, @@ -9706,6 +9746,7 @@ function openImportTextAction() { submitLabel: "开始分析", onOpen: ({ fields }) => { bindManualIntakeTitleRecommendation(fields, "文本素材", { defaultPlatform: "douyin" }); + bindActionContextRecommendation(fields, { defaultAssistantId }); }, fields: [ { name: "context", label: "当前上下文", type: "html", html: renderIntakeActionContextHtml(project.id, defaultAssistantId) }, @@ -9745,6 +9786,7 @@ function openUploadVideoAction() { submitLabel: "上传并分析", onOpen: ({ fields }) => { bindManualIntakeTitleRecommendation(fields, "本地视频", { defaultPlatform: "douyin" }); + bindActionContextRecommendation(fields, { defaultAssistantId }); }, fields: [ { name: "context", label: "当前上下文", type: "html", html: renderIntakeActionContextHtml(project.id, defaultAssistantId) }, diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index f1a3b87..5ced9b9 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -1196,6 +1196,7 @@ test("input-heavy intake sheets surface current context and smarter defaults", ( assert.match(APP, /function recommendLiveRecorderImportSamples\(platform\)/); assert.match(APP, /function recommendManualIntakeTitle\(project, platform, kind\)/); assert.match(APP, /function bindManualIntakeTitleRecommendation\(fields, kind, options = \{\}\)/); + assert.match(APP, /function bindActionContextRecommendation\(fields, options = \{\}\)/); assert.match(APP, /function bindCreativeSourceJobRecommendations\(fields, options = \{\}\)/); assert.match(APP, /function bindLiveRecorderSheetRecommendations\(fields, options = \{\}\)/); assert.match(importHomepage, /label: "当前上下文", type: "html"/); @@ -1205,19 +1206,23 @@ test("input-heavy intake sheets surface current context and smarter defaults", ( assert.match(importHomepage, /renderIntakeActionContextHtml\(project\.id, defaultAssistantId\)/); assert.match(importHomepage, /value: defaultPlatform/); assert.match(importHomepage, /placeholder: titlePlaceholder/); + assert.match(importHomepage, /bindActionContextRecommendation\(fields, \{ defaultAssistantId \}\);/); assert.match(importHomepage, /onOpen:\s*\(\{ fields \}\) => \{\s*bindManualIntakeTitleRecommendation\(fields, "主页对标", \{ defaultPlatform: "douyin" \}\);/); assert.match(importSelected, /label: "当前上下文", type: "html"/); assert.match(importSelected, /const defaultAssistantId = getSelectedAssistant\(\)\?\.id \|\| assistants\[0\]\?\.value \|\| ""/); assert.match(importSelected, /renderIntakeActionContextHtml\(project\.id, defaultAssistantId\)/); + assert.match(importSelected, /bindActionContextRecommendation\(fields, \{ defaultAssistantId \}\);/); assert.match(trackSelected, /label: "当前上下文", type: "html"/); assert.match(trackSelected, /const defaultAssistantId = trackedItem\?\.assistant_id \|\| getSelectedAssistant\(\)\?\.id \|\| assistants\[0\]\?\.value \|\| ""/); assert.match(trackSelected, /renderIntakeActionContextHtml\(project\.id, defaultAssistantId\)/); + assert.match(trackSelected, /bindActionContextRecommendation\(fields, \{ defaultAssistantId, projectField: "" \}\);/); assert.match(importVideo, /label: "当前上下文", type: "html"/); assert.match(importVideo, /const defaultAssistantId = getSelectedAssistant\(\)\?\.id \|\| assistants\[0\]\?\.value \|\| ""/); assert.match(importVideo, /const defaultPlatform = normalizePlatformValue\(getPreferredPlatform\(\), "douyin"\)/); assert.match(importVideo, /const titlePlaceholder = recommendManualIntakeTitle\(project, defaultPlatform, "单条作品"\)/); assert.match(importVideo, /renderIntakeActionContextHtml\(project\.id, defaultAssistantId\)/); assert.match(importVideo, /placeholder: titlePlaceholder/); + assert.match(importVideo, /bindActionContextRecommendation\(fields, \{ defaultAssistantId \}\);/); assert.match(importVideo, /onOpen:\s*\(\{ fields \}\) => \{\s*bindManualIntakeTitleRecommendation\(fields, "单条作品", \{ defaultPlatform: "douyin" \}\);/); assert.match(importText, /label: "当前上下文", type: "html"/); assert.match(importText, /const defaultAssistantId = getSelectedAssistant\(\)\?\.id \|\| assistants\[0\]\?\.value \|\| ""/); @@ -1225,6 +1230,7 @@ test("input-heavy intake sheets surface current context and smarter defaults", ( assert.match(importText, /const titlePlaceholder = recommendManualIntakeTitle\(project, defaultPlatform, "文本素材"\)/); assert.match(importText, /renderIntakeActionContextHtml\(project\.id, defaultAssistantId\)/); assert.match(importText, /placeholder: titlePlaceholder/); + assert.match(importText, /bindActionContextRecommendation\(fields, \{ defaultAssistantId \}\);/); assert.match(importText, /onOpen:\s*\(\{ fields \}\) => \{\s*bindManualIntakeTitleRecommendation\(fields, "文本素材", \{ defaultPlatform: "douyin" \}\);/); assert.match(uploadVideo, /label: "当前上下文", type: "html"/); assert.match(uploadVideo, /const defaultAssistantId = getSelectedAssistant\(\)\?\.id \|\| assistants\[0\]\?\.value \|\| ""/); @@ -1232,6 +1238,7 @@ test("input-heavy intake sheets surface current context and smarter defaults", ( assert.match(uploadVideo, /const titlePlaceholder = recommendManualIntakeTitle\(project, defaultPlatform, "本地视频"\)/); assert.match(uploadVideo, /renderIntakeActionContextHtml\(project\.id, defaultAssistantId\)/); assert.match(uploadVideo, /placeholder: titlePlaceholder/); + assert.match(uploadVideo, /bindActionContextRecommendation\(fields, \{ defaultAssistantId \}\);/); assert.match(uploadVideo, /onOpen:\s*\(\{ fields \}\) => \{\s*bindManualIntakeTitleRecommendation\(fields, "本地视频", \{ defaultPlatform: "douyin" \}\);/); assert.match(generateCopy, /label: "当前上下文", type: "html"/); assert.match(generateCopy, /renderIntakeActionContextHtml\(project\?\.id \|\| "", assistant\.id\)/);