feat: sync intake context on project changes
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 12:59:13 +08:00
parent cc58e2f03d
commit 3cf56a4db6
3 changed files with 55 additions and 0 deletions

View File

@@ -17,6 +17,12 @@
- `导入 URL 配置` 现在会在切换平台时实时刷新说明文案和样例配置,抖音/快手两种场景可以直接在同一张表单里切换预设。
- 这套联动同样保留“手动改过就不再覆盖”的原则,避免自动推荐把用户已经输入的内容冲掉。
### 输入型表单切项目时会同步刷新 Agent 和上下文
- `导入主页 / 导入当前对标 / 加入跟踪 / 导入作品链接 / 导入文本 / 上传本地视频` 这几张输入型表单,现在在切换项目后会一起刷新可选 Agent 列表和顶部“当前上下文”摘要。
- 这样不会再出现“项目已经换了,但表单里还是上一项目的 Agent 和上下文”的错位。
- `加入跟踪` 虽然没有项目切换,但现在在切换负责 Agent 时,顶部上下文摘要也会实时更新。
## 2026-04-06
### 主 Agent 高注意图动作统一切到直执行入口

View File

@@ -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) => `
<option value="${escapeHtml(item.value)}">${escapeHtml(item.label)}</option>
`).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) },

View File

@@ -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\)/);