diff --git a/CHANGELOG.md b/CHANGELOG.md index 318ad12..04b83a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 这个文件用于给 Gitea 仓库保留阶段性版本更新记录,方便直接查看每一轮里程碑,不用只依赖零散 commit。 +## 2026-04-04 + +### 主 Agent 配置与执行落点继续收口 + +- 发现页里三类关键动作现在会落到更精确的业务区域:账号分析会直接切到快照/字段/报告区域,高分作品分析会直接滚到“最近高分拆解”,相似账号生成会直接滚到“相似对标 / 已绑关系”。 +- 复盘创建/更新完成后,不再只停留在通用成功提示,而是会自动回到“发布与复盘”,并把刚保存的复盘项聚焦出来。 +- 同一类“保存对标关系”动作也统一改成精确落到关系区域,避免成功后仍让用户自己再找结果在哪。 + ## 2026-03-30 ### 主 Agent 治理与运行闭环 @@ -186,3 +194,30 @@ - 新增“查看执行结果”,会直接打开对应主 Agent run 的结果卡。 - 新增“回到主 Agent 查看”,会切到对应 run 的上下文并打开主 Agent 悬浮窗口,方便顺着同一轮执行继续处理。 - 前端回归也补上了这两个动作入口和事件处理器,避免后续又退回成只能展示、不能继续操作。 + +### 真实动作成功后的落点继续收口 + +- `加入跟踪 / 更新跟踪` 成功后,现在会直接切到 `跟踪账号` 工作区,不再只留一条成功提示。 +- `存对标 / 保存对标关系` 成功后,会直接把找对标详情切到 `关系` 视图,便于继续看刚保存的关系和候选。 +- `单任务恢复 / 批量恢复` 成功后,会优先打开新恢复出来的任务详情;如果没有拿到新任务 id,也会回到 `生产中心 -> 失败恢复`。 +- `生成文案` 成功后,会直接回到 `Agent` 工作区的“最近生成”结果区,而不是让用户自己找。 + +### 平台 Agent 最近执行字段补齐 + +- `recent_execution` 现在除了版本号和摘要,还会带: + - `oneliner_profile_version_id` + - `platform_agent_profile_version_id` + - `recommended_action` + - `workstream_key / workstream_label` +- 平台 Agent 总览卡和详情弹层会直接利用这些字段渲染“回到业务页”动作,不需要先打开 run 详情再猜下一步。 + +### 回归护栏继续加固 + +- 前端工作台回归新增了: + - 跟踪/对标成功后的页面落点校验 + - 恢复任务和文案生成的结果落点校验 + - 平台 Agent 最近执行 `recommended_action / workstream` 渲染校验 +- 后端治理回归新增了平台 Agent `recent_execution` 新字段断言,锁住: + - 精确版本 id + - 推荐业务动作 + - 工作流标签 diff --git a/collector-service/app/oneliner_features.py b/collector-service/app/oneliner_features.py index 8d51f2f..dee2547 100644 --- a/collector-service/app/oneliner_features.py +++ b/collector-service/app/oneliner_features.py @@ -1482,13 +1482,48 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: readiness_label = "可用" else: readiness_label = "待补全" + + def build_recent_execution_payload(base_payload: dict[str, Any], run_row: dict[str, Any] | None) -> dict[str, Any]: + if not run_row: + return base_payload + latest_result = _parse_json(run_row.get("result_json"), {}) + latest_governance = _parse_json(run_row.get("governance_json"), {}) + execution_card = (latest_result.get("execution_card") or {}) if isinstance(latest_result, dict) else {} + result_sections = (latest_result.get("result_sections") or {}) if isinstance(latest_result, dict) else {} + recommended_action = {} + if isinstance(latest_result, dict): + recommended_action = latest_result.get("recommended_action") or latest_result.get("recommended_preview_action") or {} + oneliner_profile_version = ( + execution_card.get("oneliner_profile_version") + or latest_governance.get("oneliner_profile_version") + or (latest_governance.get("oneliner_profile") or {}).get("current_version") + or {} + ) + platform_profile_version = ( + execution_card.get("platform_agent_profile") + or ((latest_governance.get("platform_agent_profile") or {}).get("current_version") or {}) + ) + return { + **base_payload, + "oneliner_profile_version_id": str(oneliner_profile_version.get("version_id") or oneliner_profile_version.get("id") or "").strip(), + "platform_agent_profile_version_id": str(platform_profile_version.get("version_id") or platform_profile_version.get("id") or "").strip(), + "recommended_action": { + "action": str(recommended_action.get("action") or "").strip(), + "screen": str(recommended_action.get("screen") or "").strip(), + "label": str(recommended_action.get("label") or "").strip(), + "summary": str(recommended_action.get("summary") or "").strip(), + }, + "workstream_key": str(result_sections.get("workstream_key") or "").strip(), + "workstream_label": str(result_sections.get("workstream_label") or "").strip(), + } + recent_execution = None current_version = None if row: _ensure_platform_agent_profile_version_seed(row, actor_user_id=account.get("id", "")) current_version = _platform_agent_profile_version_payload(_current_platform_agent_profile_version_row(row)) if row and str(row.get("last_run_id") or "").strip(): - recent_execution = { + recent_execution = build_recent_execution_payload({ "run_id": str(row.get("last_run_id") or "").strip(), "run_status": str(row.get("last_run_status") or "").strip(), "used_at": str(row.get("last_used_at") or "").strip(), @@ -1498,7 +1533,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "platform_agent_profile_version_no": int(row.get("last_platform_profile_version_no") or 0), "summary": str(row.get("last_execution_summary") or "").strip(), "source_screen": str(row.get("last_source_screen") or "").strip(), - } + }, legacy.db.fetch_one("SELECT * FROM agent_runs WHERE id = ?", (str(row.get("last_run_id") or "").strip(),))) if recent_execution is None and str(project_id or "").strip() and str(platform or "").strip(): latest_run_row = legacy.db.fetch_one( """ @@ -1517,7 +1552,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: or latest_governance.get("oneliner_profile_version") or {} ) - recent_execution = { + recent_execution = build_recent_execution_payload({ "run_id": str(latest_run_row.get("id") or "").strip(), "run_status": str(latest_run_row.get("run_status") or "").strip(), "used_at": str(latest_run_row.get("finished_at") or latest_run_row.get("updated_at") or "").strip(), @@ -1531,7 +1566,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: ), "summary": str(latest_run_row.get("status_summary") or "").strip(), "source_screen": str(latest_run_row.get("source_screen") or "").strip(), - } + }, latest_run_row) return { "id": row["id"] if row else "", "user_id": account["id"], diff --git a/tests/test_main_agent_governance.py b/tests/test_main_agent_governance.py index d747e80..cdb1e98 100644 --- a/tests/test_main_agent_governance.py +++ b/tests/test_main_agent_governance.py @@ -928,10 +928,16 @@ class MainAgentGovernanceTests(unittest.TestCase): self.assertEqual(refreshed_douyin["recent_execution"]["run_id"], run_payload["id"]) self.assertEqual(refreshed_douyin["recent_execution"]["intent_key"], "governance_review") self.assertGreaterEqual(refreshed_douyin["recent_execution"]["oneliner_profile_version_no"], 1) + self.assertTrue(refreshed_douyin["recent_execution"]["oneliner_profile_version_id"]) + self.assertTrue(refreshed_douyin["recent_execution"]["platform_agent_profile_version_id"]) self.assertEqual( refreshed_douyin["recent_execution"]["platform_agent_profile_version_no"], rollback_profile_payload["current_version"]["version_no"], ) + self.assertEqual(refreshed_douyin["recent_execution"]["recommended_action"]["action"], "goto-playbook") + self.assertEqual(refreshed_douyin["recent_execution"]["recommended_action"]["screen"], "playbook") + self.assertEqual(refreshed_douyin["recent_execution"]["workstream_key"], "playbook") + self.assertEqual(refreshed_douyin["recent_execution"]["workstream_label"], "Agent 治理") def test_admin_ops_routes_are_live(self) -> None: now = self.db_module.utc_now() diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 9205985..5620e38 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -61,6 +61,7 @@ const appState = { trackingDigest: null, trackingRefreshNotice: null, reviews: [], + reviewFocusId: "", liveRecorderSources: [], liveRecorderStatus: null, liveRecorderFiles: [], @@ -4467,11 +4468,13 @@ function renderPlatformAgentPanel() {
${escapeHtml(item.recent_execution.intent_label || "主 Agent 任务")} ${escapeHtml(item.recent_execution.run_status || "done")} + ${item.recent_execution.workstream_label ? `${escapeHtml(item.recent_execution.workstream_label)}` : ""} ${item.recent_execution.oneliner_profile_version_no ? `配置 v${escapeHtml(formatNumber(item.recent_execution.oneliner_profile_version_no))}` : ""} ${item.recent_execution.platform_agent_profile_version_no ? `${escapeHtml(item.platform_label || platformLabel(item.platform))} Agent v${escapeHtml(formatNumber(item.recent_execution.platform_agent_profile_version_no))}` : ""} ${item.recent_execution.source_screen ? `${escapeHtml(screenLabel(item.recent_execution.source_screen) || item.recent_execution.source_screen)}` : ""}
+ ${item.recent_execution.recommended_action?.action ? `${escapeHtml(item.recent_execution.recommended_action.label || "回到业务页")}` : ""} 查看执行结果 回到主 Agent 查看
@@ -4687,7 +4690,7 @@ async function saveCandidateAsBenchmark(candidateIndex, relationType = "benchmar }); markSavedCandidate(candidate, result.links); rememberAction("候选已存对标", `已把「${candidate.candidate_nickname || candidate.candidate_profile_url || "候选账号"}」加入对标关系。`, "green", result); - renderAll(); + focusDiscoveryRelations(); } function screenShell(title, subtitle, actionsHtml, bodyHtml) { @@ -5482,7 +5485,7 @@ function renderProjectsScreen() { `${button("新建项目", "create-project", "primary")} ${button("导入作品", "open-import-video-link")} ${button("导入文本", "open-import-text")} ${button("上传视频", "open-upload-video")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: intakeHandoffAttrs })}`, ` ${renderMainAgentLandingNotice("intake")} -
+

当前项目

${escapeHtml(selectedProject?.name || "还没有项目")} · ${escapeHtml(selectedProject?.description || "创建后即可承接对标、Agent 和生产任务。")}

@@ -5583,6 +5586,84 @@ function renderDetailTabs(stateKey, tabs) { `; } +function focusDiscoveryDetailTab(tabValue) { + appState.discoveryDetailTab = tabValue; + appState.screen = "discovery"; + renderAll(); + window.requestAnimationFrame(() => { + document.getElementById("selected-account-anchor")?.scrollIntoView({ behavior: "smooth", block: "start" }); + }); +} + +function focusDiscoveryInsights() { + appState.discoveryDetailTab = "snapshots"; + appState.screen = "discovery"; + renderAll(); + window.requestAnimationFrame(() => { + (document.getElementById("douyin-insight-anchor") || document.getElementById("selected-account-anchor")) + ?.scrollIntoView({ behavior: "smooth", block: "start" }); + }); +} + +function focusDiscoveryTopVideoInsights() { + appState.discoveryDetailTab = "overview"; + appState.screen = "discovery"; + renderAll(); + window.requestAnimationFrame(() => { + (document.getElementById("top-video-batch-anchor") || document.getElementById("selected-account-anchor")) + ?.scrollIntoView({ behavior: "smooth", block: "start" }); + }); +} + +function focusDiscoveryRelations() { + appState.discoveryDetailTab = "relations"; + appState.screen = "discovery"; + renderAll(); + window.requestAnimationFrame(() => { + (document.getElementById("discovery-relations-anchor") || document.getElementById("selected-account-anchor")) + ?.scrollIntoView({ behavior: "smooth", block: "start" }); + }); +} + +function focusTrackingWorkspace() { + setScreen("tracking"); + renderAll(); + window.requestAnimationFrame(() => { + document.querySelector('[data-screen="tracking"] .mobile-flow-focus-card, [data-screen="tracking"] .panel')?.scrollIntoView({ behavior: "smooth", block: "start" }); + }); +} + +function focusProductionDetailTab(tabValue) { + appState.productionDetailTab = tabValue; + setScreen("production"); + renderAll(); + window.requestAnimationFrame(() => { + document.querySelector('[data-screen="production"] .mobile-flow-focus-card, [data-screen="production"] .panel')?.scrollIntoView({ behavior: "smooth", block: "start" }); + }); +} + +function focusRecentGeneratedCopy() { + appState.playbookDetailTab = "workspace"; + setScreen("playbook"); + renderAll(); + window.requestAnimationFrame(() => { + document.getElementById("recent-generated-copy-anchor")?.scrollIntoView({ behavior: "smooth", block: "start" }); + }); +} + +function focusReviewWorkspace(reviewId = "") { + appState.reviewFocusId = reviewId || ""; + setScreen("review"); + renderAll(); + window.requestAnimationFrame(() => { + const selector = reviewId + ? `[data-review-id="${String(reviewId).replaceAll('"', '\\"')}"]` + : "#review-workspace-anchor"; + (document.querySelector(selector) || document.getElementById("review-workspace-anchor")) + ?.scrollIntoView({ behavior: "smooth", block: "start" }); + }); +} + function renderDiscoveryOverviewSection({ selected, selectedProject, importedSources, tracked, topVideos, reports, latestVideos, currentPlatformLabel, topVideoBatchResult }) { return `
@@ -5630,7 +5711,7 @@ function renderDiscoveryOverviewSection({ selected, selectedProject, importedSou
${topVideoBatchResult ? ` -
+

最近高分拆解

@@ -5677,7 +5758,7 @@ function renderDiscoveryOverviewSection({ selected, selectedProject, importedSou function renderDiscoveryRelationsSection(linkedAccounts, similarCandidates) { return ` -
+

已绑关系

当前账号已经保存的对标关系
${escapeHtml(formatNumber(linkedAccounts.length))} 个
@@ -6059,7 +6140,7 @@ function renderTrackingScreen() {

更新日报

优先看最近更新的作品摘要
${escapeHtml(formatNumber(digestItems.length))} 条
${digestItems.map((item) => ` -
+

${escapeHtml(item.account?.nickname || "账号")} · ${escapeHtml(item.video?.title || item.video?.description || "最新作品")}

${escapeHtml(item.summary || `发布时间 ${formatDateTime(item.video?.published_at)},建议继续判断借鉴点。`)}

@@ -6469,6 +6550,7 @@ function renderPlaybookScreen() {

最近生成

当前先承接文案生成结果
+
${appState.lastGeneratedCopy ? `

${escapeHtml(appState.lastGeneratedCopy.assistantName)}

@@ -8372,6 +8454,9 @@ function openImportHomepageAction() { }); rememberAction("主页同步已启动", `已把主页加入项目,并创建同步任务 ${job.title || job.id}。`, "blue", job); await bootstrap(); + if (job?.id) { + openJobDetailAction(job.id); + } } }); } @@ -8440,6 +8525,9 @@ function openImportSelectedAccountAction() { }); rememberAction("对标已接入项目", `已把「${getAccountName(account) || "当前对标"}」接入项目,并创建同步任务 ${job.title || job.id}。`, "green", { source, job }); await bootstrap(); + if (job?.id) { + openJobDetailAction(job.id); + } } }); } @@ -8473,6 +8561,7 @@ function openTrackSelectedAccountAction() { }); rememberAction(trackedItem ? "跟踪已更新" : "已加入跟踪", `账号「${getAccountName(account) || "当前对标"}」现在会进入更新日报。`, "green"); await bootstrap(); + focusTrackingWorkspace(); } }); } @@ -8507,6 +8596,9 @@ function openImportVideoLinkAction() { }); rememberAction("作品分析已启动", `已创建分析任务 ${job.title || job.id}。`, "blue", job); await bootstrap(); + if (job?.id) { + openJobDetailAction(job.id); + } } }); } @@ -8540,6 +8632,9 @@ function openImportTextAction() { }); rememberAction("文本分析已启动", `已创建文本分析任务 ${job.title || job.id}。`, "blue", job); await bootstrap(); + if (job?.id) { + openJobDetailAction(job.id); + } } }); } @@ -8572,6 +8667,9 @@ function openUploadVideoAction() { }); rememberAction("上传分析已启动", `已上传素材并创建任务 ${job.title || job.id}。`, "blue", job); await bootstrap(); + if (job?.id) { + openJobDetailAction(job.id); + } } }); } @@ -9445,11 +9543,13 @@ async function openPlatformAgentDetailAction(platform) {
${escapeHtml(profile.recent_execution.intent_label || "主 Agent 任务")} ${escapeHtml(profile.recent_execution.run_status || "done")} + ${profile.recent_execution.workstream_label ? `${escapeHtml(profile.recent_execution.workstream_label)}` : ""} ${profile.recent_execution.oneliner_profile_version_no ? `配置 v${escapeHtml(formatNumber(profile.recent_execution.oneliner_profile_version_no))}` : ""} ${profile.recent_execution.platform_agent_profile_version_no ? `${escapeHtml(platformLabel(normalizedPlatform))} Agent v${escapeHtml(formatNumber(profile.recent_execution.platform_agent_profile_version_no))}` : ""} ${profile.recent_execution.source_screen ? `${escapeHtml(screenLabel(profile.recent_execution.source_screen) || profile.recent_execution.source_screen)}` : ""}
+ ${profile.recent_execution.recommended_action?.action ? `${escapeHtml(profile.recent_execution.recommended_action.label || "回到业务页")}` : ""} 查看执行结果 回到主 Agent 查看
@@ -9778,7 +9878,7 @@ function openAnalyzeSelectedAccountAction() { const summary = result?.suggestions?.[0]?.parsed_json?.executive_summary || result?.suggestions?.[0]?.suggestion_text || "已生成新的账号分析。"; rememberAction("对标账号分析完成", brief(summary, 120), "green", result); await loadPlatformAccount(platform, account.id); - renderAll(); + focusDiscoveryInsights(); } }); } @@ -9816,7 +9916,7 @@ function openAnalyzeTopVideosAction() { }; rememberAction("高分作品分析完成", `已补分析 ${formatNumber(result.analyzed_count)} 条高分作品。`, "green", result); await loadPlatformAccount(platform, account.id); - renderAll(); + focusDiscoveryTopVideoInsights(); } }); } @@ -9854,7 +9954,7 @@ function openSimilaritySearchAction() { appState.lastSimilaritySearch = detail; rememberAction("相似账号已生成", `已生成 ${formatNumber(safeArray(detail.candidates).length)} 个候选账号。`, "green", detail); await loadPlatformAccount(platform, account.id); - renderAll(); + focusDiscoveryRelations(); } }); } @@ -9904,7 +10004,7 @@ function openBenchmarkLinkAction(defaults = {}) { }; } rememberAction("对标关系已保存", "当前账号的对标关系已更新。", "green"); - renderAll(); + focusDiscoveryRelations(); } }); } @@ -10295,6 +10395,11 @@ function openRecoverJobAction(jobId) { const result = await recoverJobAction(job.id, { mode: "single", job, user_feedback: values.note?.trim() || "" }); rememberAction("任务已恢复", `${job.title || job.id} 已重新发起,下一步可以去生产中心继续跟进。`, "green", result); await bootstrap(); + if (result?.created?.id) { + openJobDetailAction(result.created.id); + } else { + focusProductionDetailTab("recovery"); + } } finally { setBusy(false, ""); } @@ -10380,6 +10485,11 @@ function openBatchRecoverJobsAction() { }); } await bootstrap(); + if (successes[0]?.result?.created?.id) { + openJobDetailAction(successes[0].result.created.id); + } else { + focusProductionDetailTab("recovery"); + } } finally { setBusy(false, ""); } @@ -10420,7 +10530,7 @@ function openGenerateCopyAction(defaults = {}) { usedDocuments: safeArray(result.used_documents).slice(0, 3) }; rememberAction("文案生成完成", `已用 Agent「${assistant.name}」生成一版文案。`, "green", result); - renderAll(); + focusRecentGeneratedCopy(); } }); } @@ -10466,6 +10576,9 @@ function openCreateAiVideoAction(defaults = {}) { }); rememberAction("AI 视频任务已创建", `已创建任务 ${job.title || job.id}。`, "blue", job); await bootstrap(); + if (job?.id) { + openJobDetailAction(job.id); + } } }); } @@ -10505,6 +10618,9 @@ function openCreateRealCutAction(defaults = {}) { }); rememberAction("实拍剪辑任务已创建", `已创建任务 ${job.title || job.id}。`, "blue", job); await bootstrap(); + if (job?.id) { + openJobDetailAction(job.id); + } } }); } @@ -10680,19 +10796,23 @@ async function deleteLiveRecorderSourceAction(sourceId) { renderAll(); return; } - if (!window.confirm(`确认删除「${source.title || source.source_url || "录制源"}」吗?删除后需要重新导入。`)) { - return; - } - setBusy(true, "正在删除录制源..."); - try { - await storyforgeFetch(`/v2/live-recorder/sources/${encodeURIComponent(source.id)}`, { - method: "DELETE" - }); - rememberAction("录制源已删除", `${source.title || source.source_url || "录制源"} 已从租户视图中移除。`, "green"); - await bootstrap(); - } finally { - setBusy(false, ""); - } + openActionModal({ + title: "确认删除录制源", + description: `确认删除「${source.title || source.source_url || "录制源"}」吗?删除后需要重新导入。`, + submitLabel: "确认删除", + onSubmit: async () => { + setBusy(true, "正在删除录制源..."); + try { + await storyforgeFetch(`/v2/live-recorder/sources/${encodeURIComponent(source.id)}`, { + method: "DELETE" + }); + rememberAction("录制源已删除", `${source.title || source.source_url || "录制源"} 已从租户视图中移除。`, "green"); + await bootstrap(); + } finally { + setBusy(false, ""); + } + } + }); } async function openLiveRecorderFileAction(fileId) { @@ -10796,6 +10916,7 @@ function openReviewAction(defaults = {}) { }); rememberAction(existingReview ? "复盘已更新" : "复盘已创建", `已保存「${review.title}」并回写到项目复盘。`, "green", review); await bootstrap(); + focusReviewWorkspace(review.id); } }); } diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index dca66bd..ca273e0 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -772,7 +772,11 @@ test("platform agent profiles expose history, rollback, and execution version co assert.match(panel, /open-platform-agent-profile-history/); assert.match(detail, /open-platform-agent-profile-history/); assert.match(panel, /platform_agent_profile_version_no/); + assert.match(panel, /recent_execution\.recommended_action\?\.action/); + assert.match(panel, /recent_execution\.workstream_label/); assert.match(detail, /platform_agent_profile_version_no/); + assert.match(detail, /recent_execution\.recommended_action\?\.action/); + assert.match(detail, /recent_execution\.workstream_label/); assert.match(execution, /platformAgentProfile\.version_no/); assert.match(actions, /name === "open-platform-agent-profile-history"/); assert.match(actions, /openPlatformAgentProfileHistoryAction/); @@ -840,6 +844,68 @@ test("tracked-account refresh opens the created sync task when the backend retur assert.match(refresh, /openJobDetailAction\(payload\.sync_job_id\)/); }); +test("job-creating workbench actions jump straight into the created job detail", () => { + const importHomepage = extractBetween(APP, "function openImportHomepageAction()", "function openImportSelectedAccountAction()"); + const importSelected = extractBetween(APP, "function openImportSelectedAccountAction()", "function openTrackSelectedAccountAction()"); + const importVideo = extractBetween(APP, "function openImportVideoLinkAction()", "function openImportTextAction()"); + const importText = extractBetween(APP, "function openImportTextAction()", "function openUploadVideoAction()"); + const uploadVideo = extractBetween(APP, "function openUploadVideoAction()", "function openOneLinerProfileAction()"); + const aiVideo = extractBetween(APP, "function openCreateAiVideoAction(defaults = {})", "function openCreateRealCutAction(defaults = {})"); + const realCut = extractBetween(APP, "function openCreateRealCutAction(defaults = {})", "function openLiveRecorderAction()"); + assert.match(importHomepage, /if \(job\?\.id\) \{\s*openJobDetailAction\(job\.id\);/); + assert.match(importSelected, /if \(job\?\.id\) \{\s*openJobDetailAction\(job\.id\);/); + assert.match(importVideo, /if \(job\?\.id\) \{\s*openJobDetailAction\(job\.id\);/); + assert.match(importText, /if \(job\?\.id\) \{\s*openJobDetailAction\(job\.id\);/); + assert.match(uploadVideo, /if \(job\?\.id\) \{\s*openJobDetailAction\(job\.id\);/); + assert.match(aiVideo, /if \(job\?\.id\) \{\s*openJobDetailAction\(job\.id\);/); + assert.match(realCut, /if \(job\?\.id\) \{\s*openJobDetailAction\(job\.id\);/); +}); + +test("discovery analysis actions focus the most relevant detail tab after success", () => { + const analyzeAccount = extractBetween(APP, "function openAnalyzeSelectedAccountAction()", "function openAnalyzeTopVideosAction()"); + const analyzeTopVideos = extractBetween(APP, "function openAnalyzeTopVideosAction()", "function openSimilaritySearchAction()"); + const similaritySearch = extractBetween(APP, "function openSimilaritySearchAction()", "function openBenchmarkLinkAction(defaults = {})"); + assert.match(APP, /function focusDiscoveryDetailTab\(tabValue\)/); + assert.match(APP, /function focusDiscoveryInsights\(\)/); + assert.match(APP, /function focusDiscoveryTopVideoInsights\(\)/); + assert.match(APP, /function focusDiscoveryRelations\(\)/); + assert.match(analyzeAccount, /focusDiscoveryInsights\(\)/); + assert.match(analyzeTopVideos, /focusDiscoveryTopVideoInsights\(\)/); + assert.match(similaritySearch, /focusDiscoveryRelations\(\)/); +}); + +test("tracking and benchmark actions land on the most relevant workbench area after success", () => { + const trackSelected = extractBetween(APP, "function openTrackSelectedAccountAction()", "function openImportVideoLinkAction()"); + const saveCandidate = extractBetween(APP, "async function saveCandidateAsBenchmark(candidateIndex, relationType = \"benchmark\")", "function screenShell(title, subtitle, actionsHtml, bodyHtml)"); + const openBenchmark = extractBetween(APP, "function openBenchmarkLinkAction(defaults = {})", "async function scanAdminOpsAction()"); + assert.match(APP, /function focusTrackingWorkspace\(\)/); + assert.match(APP, /function focusDiscoveryRelations\(\)/); + assert.match(trackSelected, /focusTrackingWorkspace\(\)/); + assert.match(saveCandidate, /focusDiscoveryRelations\(\)/); + assert.match(openBenchmark, /focusDiscoveryRelations\(\)/); +}); + +test("recovery and copy actions continue into the most useful result view", () => { + const singleRecover = extractBetween(APP, "function openRecoverJobAction(jobId)", "function openBatchRecoverJobsAction()"); + const batchRecover = extractBetween(APP, "function openBatchRecoverJobsAction()", "function openGenerateCopyAction(defaults = {})"); + const generateCopy = extractBetween(APP, "function openGenerateCopyAction(defaults = {})", "function openCreateAiVideoAction(defaults = {})"); + assert.match(APP, /function focusProductionDetailTab\(tabValue\)/); + assert.match(APP, /function focusRecentGeneratedCopy\(\)/); + assert.match(singleRecover, /if \(result\?\.created\?\.id\) \{\s*openJobDetailAction\(result\.created\.id\);/); + assert.match(singleRecover, /focusProductionDetailTab\("recovery"\)/); + assert.match(batchRecover, /if \(successes\[0\]\?\.result\?\.created\?\.id\) \{\s*openJobDetailAction\(successes\[0\]\.result\.created\.id\);/); + assert.match(batchRecover, /focusProductionDetailTab\("recovery"\)/); + assert.match(generateCopy, /focusRecentGeneratedCopy\(\)/); +}); + +test("review actions return to the review workspace with the saved review in focus", () => { + const reviewAction = extractBetween(APP, "function openReviewAction(defaults = {})", "document.addEventListener(\"click\", async (event) => {"); + assert.match(APP, /function focusReviewWorkspace\(reviewId = ""\)/); + assert.match(reviewAction, /focusReviewWorkspace\(review\.id\)/); + assert.match(APP, /id="review-workspace-anchor"/); + assert.match(APP, /data-review-id="\$\{escapeHtml\(review\.id\)\}"/); +}); + test("main agent execution cards can jump to oneliner and platform profile history", () => { const messages = extractBetween(APP, "function renderOneLinerMessagesHtml()", "function renderAutoConnectingScreen(screenTitle, nextStepText)"); assert.match(messages, /data-action="open-oneliner-profile-history"/); @@ -907,10 +973,19 @@ test("oneliner panel auto-polls active runs while the floating panel stays open" test("workbench interaction flow avoids browser alerts and keeps failures inside the product shell", () => { const clicks = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {"); assert.doesNotMatch(APP, /alert\(/); + assert.doesNotMatch(APP, /confirm\(/); assert.match(clicks, /name === "show-disabled-reason"[\s\S]*rememberAction\("动作已拦截"/); assert.match(clicks, /name === "auth-refresh" \|\| name === "refresh-data"[\s\S]*presentActionFailure\(error, "刷新数据失败"\)/); }); +test("live recorder delete uses an in-app confirmation sheet instead of browser confirm", () => { + const removeSource = extractBetween(APP, "async function deleteLiveRecorderSourceAction(sourceId)", "async function openLiveRecorderFileAction(fileId)"); + assert.match(removeSource, /openActionModal\(\{/); + assert.match(removeSource, /title:\s*"确认删除录制源"/); + assert.match(removeSource, /submitLabel:\s*"确认删除"/); + assert.doesNotMatch(removeSource, /window\.confirm/); +}); + test("live-first workbench flows no longer advertise stale missing-capability placeholders for active routes", () => { assert.doesNotMatch(APP, /当前实例还没有开放 OneLiner 会话接口/); assert.doesNotMatch(APP, /当前实例还没有开放主 Agent 运行层/);