diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2cb4f9d..9f22040 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@
## 2026-04-07
+### AI 视频表单可直接跳到火山视频配置状态
+
+- `创建 AI 视频任务` 里的 `Seedance 配置` 提示现在不再只是静态文案,而是新增了 `查看火山配置状态` 入口。
+- 点击后会直接跳到 `自动流程 -> 依赖健康 -> Huobao` 卡片,立刻看到当前火山视频配置是否就绪、部署位置和配置提示,不用再自己记 `/settings/ai-config -> 视频 -> 火山引擎` 再手动找入口。
+- 同时 `依赖健康` 里的各张集成卡现在都带稳定锚点,后续其他配置提示也可以直接把用户带到最相关的健康卡,而不是只停在说明文字里。
+
### 修复额度页套餐建议引起的全局渲染报错
- `额度` 页面现在会先初始化 `packageRecommendation` 再渲染套餐建议,不再因为变量未定义把整个工作台渲染链打断。
@@ -662,3 +668,9 @@
- `创建 Agent / 编辑 Agent` 这两张表单也补成了带上下文和知识库联动的产品化表单:创建时切项目会同步刷新默认知识库,编辑时可以直接更新默认知识库,不必再回别处改。
- 额度页残留的半成品口径已收口,不再出现“后端尚未完全接入真实预算”这类提示;未配置独立额度策略时,会直接引导按预算基线和动作池去建立试用、增长或规模套餐。
- `smoke_public_storyforge.sh` 和 `smoke_fnos_storyforge_lan.sh` 现在会显式校验 `integrations/health` 的关键依赖状态、部署位置和 `local_model=not_configured` 口径,不再只看页面能打开和基础 healthz。
+
+# 2026-04-07
+
+- 顶层 `AI 视频 / 实拍剪辑` 主按钮改回“先开配置表单”,会自动承接最近完成任务作为默认来源,但不再直接跳过配置页;只有任务上下文里的 `做 AI 视频 / 做实拍剪辑` 仍保持 direct-execute。
+- `AI 视频` 表单新增 `Seedance 配置` 提示,明确说明当前 `Seedance 2.0` 走火山视频配置,默认应在 `Huobao /settings/ai-config -> 视频 -> 火山引擎` 配置;如果不用页面配置,也支持通过 `HUOBAO_VIDEO_BASE_URL / HUOBAO_VIDEO_API_KEY / HUOBAO_VIDEO_MODELS` 环境变量覆盖。
+- `integrations/health` 新增 `huobao` 视频配置摘要,能直接看出当前 `Huobao` 视频配置页是否已经录入视频引擎配置,以及对应的配置页路径,减少排查 `Seedance` 任务为什么只建单不出片的歧义。
diff --git a/collector-service/app/core_main.py b/collector-service/app/core_main.py
index 3e6af45..e3c4d77 100644
--- a/collector-service/app/core_main.py
+++ b/collector-service/app/core_main.py
@@ -3188,6 +3188,39 @@ def probe_http_json(url: str, path: str = "", timeout: float = 3.0) -> dict[str,
return detail
+def probe_huobao_video_config(url: str, timeout: float = 5.0) -> dict[str, Any]:
+ detail = {
+ "video_config_route": "/settings/ai-config -> 视频 -> 火山引擎",
+ "video_config_count": 0,
+ "video_config_ready": False,
+ }
+ if not url:
+ return detail
+ target_url = urljoin(url if url.endswith("/") else f"{url}/", "api/v1/ai-configs")
+ try:
+ response = httpx.get(
+ target_url,
+ params={"service_type": "video"},
+ timeout=timeout,
+ follow_redirects=True,
+ )
+ if response.status_code >= 500:
+ return detail
+ payload = response.json()
+ items: list[dict[str, Any]] = []
+ if isinstance(payload, list):
+ items = [item for item in payload if isinstance(item, dict)]
+ elif isinstance(payload, dict):
+ raw_items = payload.get("data") if isinstance(payload.get("data"), list) else payload.get("configs")
+ if isinstance(raw_items, list):
+ items = [item for item in raw_items if isinstance(item, dict)]
+ detail["video_config_count"] = len(items)
+ detail["video_config_ready"] = bool(items)
+ except Exception:
+ pass
+ return detail
+
+
def live_recorder_request(method: str, path: str, payload: dict[str, Any] | None = None, timeout: float = 20.0) -> Any:
if not LIVE_RECORDER_BASE_URL:
raise HTTPException(status_code=503, detail="LIVE_RECORDER_BASE_URL is not configured")
@@ -3312,6 +3345,8 @@ def integrations_health(account: dict[str, Any] = Depends(require_approved)) ->
cutvideo_uploads = probe_http(CUTVIDEO_BASE_URL, "/api/uploads", timeout=5.0)
asr_probe = probe_http_json(ASR_HTTP_BASE_URL, "/health", timeout=5.0)
asr_runtime = asr_probe.get("json") if isinstance(asr_probe.get("json"), dict) else {}
+ huobao_probe = probe_http(HUOBAO_BASE_URL, "/health", timeout=5.0)
+ huobao_video_config = probe_huobao_video_config(HUOBAO_BASE_URL, timeout=5.0)
cutvideo_supports_uploads = bool(
cutvideo_uploads.get("configured")
and cutvideo_uploads.get("reachable")
@@ -3336,7 +3371,8 @@ def integrations_health(account: dict[str, Any] = Depends(require_approved)) ->
"huobao": {
"base_url": HUOBAO_BASE_URL,
**integration_deployment_payload("huobao", HUOBAO_BASE_URL),
- **probe_http(HUOBAO_BASE_URL, "/health"),
+ **huobao_probe,
+ **huobao_video_config,
},
"n8n": {
"base_url": N8N_BASE_URL,
diff --git a/tests/test_production_baseline.py b/tests/test_production_baseline.py
index ceb0bff..ba8df21 100644
--- a/tests/test_production_baseline.py
+++ b/tests/test_production_baseline.py
@@ -352,6 +352,7 @@ class ProductionBaselineTests(unittest.TestCase):
original_cutvideo = self.core.CUTVIDEO_BASE_URL
original_probe_http = self.core.probe_http
original_probe_http_json = getattr(self.core, "probe_http_json", None)
+ original_probe_huobao_video_config = getattr(self.core, "probe_huobao_video_config", None)
try:
self.core.N8N_BASE_URL = "http://127.0.0.1:25670"
self.core.HUOBAO_BASE_URL = "http://127.0.0.1:25678"
@@ -381,8 +382,16 @@ class ProductionBaselineTests(unittest.TestCase):
}
return detail
+ def fake_probe_huobao_video_config(url: str, timeout: float = 5.0) -> dict[str, Any]:
+ return {
+ "video_config_route": "/settings/ai-config -> 视频 -> 火山引擎",
+ "video_config_count": 1,
+ "video_config_ready": True,
+ }
+
self.core.probe_http = fake_probe_http
self.core.probe_http_json = fake_probe_http_json
+ self.core.probe_huobao_video_config = fake_probe_huobao_video_config
response = self.client.get("/v2/integrations/health", headers=headers)
finally:
self.core.N8N_BASE_URL = original_n8n
@@ -398,11 +407,20 @@ class ProductionBaselineTests(unittest.TestCase):
pass
else:
self.core.probe_http_json = original_probe_http_json
+ if original_probe_huobao_video_config is None:
+ try:
+ delattr(self.core, "probe_huobao_video_config")
+ except AttributeError:
+ pass
+ else:
+ self.core.probe_huobao_video_config = original_probe_huobao_video_config
self.assertEqual(response.status_code, 200, response.text)
payload = response.json()
self.assertEqual(payload["n8n"]["deployment_label"], "服务器")
self.assertEqual(payload["huobao"]["deployment_label"], "服务器")
+ self.assertEqual(payload["huobao"]["video_config_route"], "/settings/ai-config -> 视频 -> 火山引擎")
+ self.assertTrue(payload["huobao"]["video_config_ready"])
self.assertEqual(payload["asr"]["deployment_label"], "Windows")
self.assertEqual(payload["live_recorder"]["deployment_label"], "NAS")
self.assertEqual(payload["cutvideo"]["deployment_label"], "NAS 隧道")
diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js
index b229f27..e29f503 100644
--- a/web/storyforge-web-v4/assets/app.js
+++ b/web/storyforge-web-v4/assets/app.js
@@ -206,13 +206,13 @@ const INTEGRATION_META = {
const PIPELINE_GUARDS = {
aiVideo: {
label: "AI 视频",
- openAction: "direct-create-ai-video",
+ openAction: "open-ai-video",
jobAction: "direct-create-ai-video",
dependencies: ["n8n", "huobao"]
},
realCut: {
label: "实拍剪辑",
- openAction: "direct-create-real-cut",
+ openAction: "open-real-cut",
jobAction: "direct-create-real-cut",
dependencies: ["n8n", "cutvideo"]
}
@@ -4299,7 +4299,10 @@ function getIntegrationDetail(key) {
activeDevice: String(raw?.active_device || ""),
activeComputeType: String(raw?.active_compute_type || ""),
languageMode: String(raw?.language_mode || ""),
- modelName: String(raw?.model_name || "")
+ modelName: String(raw?.model_name || ""),
+ videoConfigRoute: String(raw?.video_config_route || ""),
+ videoConfigCount: Number(raw?.video_config_count || 0),
+ videoConfigReady: Boolean(raw?.video_config_ready),
};
}
@@ -4458,6 +4461,16 @@ function getIntegrationCards() {
if (detail.modelName) {
extra += ` · 当前模型:${detail.modelName}`;
}
+ } else if (key === "huobao") {
+ const configLabel = detail.videoConfigCount
+ ? `视频配置 ${detail.videoConfigCount} 条`
+ : "视频配置未录入";
+ extra = `部署:${detail.deploymentLabel || "待确认"} · ${configLabel}`;
+ if (detail.videoConfigRoute) {
+ note = detail.videoConfigReady
+ ? `Huobao 视频配置已就绪:${detail.videoConfigRoute}`
+ : `Seedance 2.0 走火山视频配置,请在 ${detail.videoConfigRoute} 补齐`;
+ }
} else if (detail.deploymentLabel) {
extra = `部署:${detail.deploymentLabel}`;
}
@@ -5647,6 +5660,7 @@ function renderIntegrationOverviewPanel(options = {}) {
${renderPipelineButton("aiVideo", "primary")}
${renderPipelineButton("realCut")}
+ ${button("视频录制", "focus-live-recorder-maintenance", "secondary")}
` : ""}
@@ -5655,7 +5669,7 @@ function renderIntegrationOverviewPanel(options = {}) {
${cards.map((item) => `
-
+
${escapeHtml(item.meta.label)}
@@ -6523,6 +6537,16 @@ function focusLiveRecorderMaintenance() {
});
}
+function focusAutomationHealthWorkspace(anchorId = "integration-huobao-anchor") {
+ appState.automationDetailTab = "health";
+ setScreen("automation");
+ renderAll();
+ window.requestAnimationFrame(() => {
+ (document.getElementById(anchorId) || document.querySelector('[data-screen="automation"] .integration-panel, [data-screen="automation"] .panel'))
+ ?.scrollIntoView({ behavior: "smooth", block: "start" });
+ });
+}
+
function focusReviewWorkspace(reviewId = "") {
appState.reviewFocusId = reviewId || "";
setScreen("review");
@@ -7541,7 +7565,7 @@ function renderProductionMobileTaskDeck({ activeTab, activeJobs, failedJobs, rec
${escapeHtml(status.running ? `Live Recorder 正在运行,当前有 ${activeCount} 路活动录制。` : "先确认 Live Recorder 是否在线,再检查录制源和文件。")}
${escapeHtml(status.running ? "运行中" : "待检查")}
- ${actionTag("录制维护", "select-page-tab", `data-page-tab-key="productionDetailTab" data-page-tab-value="recorder"`)}
+ ${actionTag("视频录制", "focus-live-recorder-maintenance")}
`);
@@ -7554,12 +7578,12 @@ function renderProductionMobileTaskDeck({ activeTab, activeJobs, failedJobs, rec
sourceScreen: "production",
sourceActionKey: "production-mobile-recorder-handoff",
intentKey: "production_coordination",
- title: "继续处理录制维护",
- goal: "继续处理录制维护",
- summary: "结合录制维护状态给出下一步动作。",
+ title: "继续处理视频录制",
+ goal: "继续处理视频录制",
+ summary: "结合视频录制状态给出下一步动作。",
platform: getPreferredPlatform(),
platformScope: "single_platform",
- planSteps: ["读取录制维护状态", "识别当前阻塞项", "生成下一步处理动作"]
+ planSteps: ["读取视频录制状态", "识别当前阻塞项", "生成下一步处理动作"]
}))}
@@ -7662,13 +7686,13 @@ function renderProductionScreen() {
const tabs = [
{ value: "queue", label: "生产队列" },
{ value: "recovery", label: "失败恢复" },
- { value: "recorder", label: "录制维护" },
+ { value: "recorder", label: "视频录制" },
{ value: "outputs", label: "作品与产物" }
];
const activeTab = getActiveDetailTab("productionDetailTab", tabs);
const productionActionsHtml = isMobileUi
- ? `${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: productionHandoffAttrs })}`
- : `${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: productionHandoffAttrs })} ${button("去复盘", "goto-review", "primary")} ${button("批量恢复", "batch-recover-jobs", "secondary", { disabledReason: recoverableCount ? "" : "当前没有可恢复的失败任务" })}`;
+ ? `${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${button("视频录制", "focus-live-recorder-maintenance", "secondary")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: productionHandoffAttrs })}`
+ : `${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${button("视频录制", "focus-live-recorder-maintenance", "secondary")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: productionHandoffAttrs })} ${button("去复盘", "goto-review", "primary")} ${button("批量恢复", "batch-recover-jobs", "secondary", { disabledReason: recoverableCount ? "" : "当前没有可恢复的失败任务" })}`;
return screenShell(
"生产中心",
"这里已经接上真实任务、失败恢复和知识库文档,适合直接推进生产、恢复和复盘。",
@@ -7717,7 +7741,7 @@ function renderProductionScreen() {
: activeTab === "outputs"
? `${actionTag("去复盘", "goto-review")} ${actionTag("查看产物", "select-page-tab", `data-page-tab-key="productionDetailTab" data-page-tab-value="outputs"`)}`
: activeTab === "recorder"
- ? `${actionTag("录制维护", "select-page-tab", `data-page-tab-key="productionDetailTab" data-page-tab-value="recorder"`)} ${actionTag("交给主 Agent", "handoff-to-main-agent", productionHandoffAttrs)}`
+ ? `${actionTag("视频录制", "focus-live-recorder-maintenance")} ${actionTag("交给主 Agent", "handoff-to-main-agent", productionHandoffAttrs)}`
: `${actionTag("批量恢复", "batch-recover-jobs")} ${actionTag("交给主 Agent", "handoff-to-main-agent", productionHandoffAttrs)}`
}
@@ -12082,6 +12106,29 @@ function openGenerateCopyAction(defaults = {}) {
});
}
+function renderAiVideoProviderHintHtml(provider = "doubao") {
+ const huobao = getIntegrationDetail("huobao");
+ const route = huobao.videoConfigRoute || "/settings/ai-config -> 视频 -> 火山引擎";
+ const providerLabel = String(provider || "doubao").trim() === "seedance2" ? "Seedance 2.0" : "当前默认视频引擎";
+ const configStatus = huobao.videoConfigReady
+ ? `Huobao 视频配置已就绪${huobao.videoConfigCount ? `(${huobao.videoConfigCount} 条)` : ""}`
+ : "Huobao 视频配置页当前还没有录入视频引擎配置";
+ return `
+
+
+
${escapeHtml(`${providerLabel} 走火山视频配置`)}
+
${escapeHtml(`请在 Huobao 服务里配置火山视频 Key:${route}`)}
+
${escapeHtml("如果不是走页面配置,也可以在 huobao 服务环境变量里覆盖 HUOBAO_VIDEO_BASE_URL / HUOBAO_VIDEO_API_KEY / HUOBAO_VIDEO_MODELS。")}
+
+ ${escapeHtml(configStatus)}
+ ${huobao.deploymentLabel ? `${escapeHtml(`部署:${huobao.deploymentLabel}`)}` : ""}
+ 查看火山配置状态
+
+
+
+ `;
+}
+
function openCreateAiVideoAction(defaults = {}) {
const guard = getPipelineGuard("aiVideo");
if (!guard.enabled) {
@@ -12126,6 +12173,12 @@ function openCreateAiVideoAction(defaults = {}) {
value: defaultVideoModel,
placeholder: "例如:seedance-2.0-pro",
},
+ {
+ name: "videoProviderHint",
+ label: "Seedance 配置",
+ type: "html",
+ html: renderAiVideoProviderHintHtml(defaultVideoProvider),
+ },
{ name: "style", label: "风格", value: defaults.style || recommendCreativeStyle(sourceJob) },
{
name: "aspectRatio",
@@ -12788,6 +12841,16 @@ document.addEventListener("click", async (event) => {
setScreen("production");
return;
}
+ if (name === "focus-live-recorder-maintenance") {
+ captureMainAgentLandingContext(action, "goto-production");
+ focusLiveRecorderMaintenance();
+ return;
+ }
+ if (name === "focus-huobao-video-config") {
+ captureMainAgentLandingContext(action, "goto-automation");
+ focusAutomationHealthWorkspace("integration-huobao-anchor");
+ return;
+ }
if (name === "goto-strategy") {
captureMainAgentLandingContext(action, "goto-strategy");
setScreen("strategy");
@@ -13258,15 +13321,7 @@ document.addEventListener("click", async (event) => {
}
if (name === "open-ai-video") {
const fallbackJob = getLatestCompletedProjectJob();
- if (fallbackJob?.id) {
- await runDirectWorkbenchAction("create-ai-video", {
- busyLabel: "正在创建 AI 视频任务...",
- errorTitle: "创建 AI 视频任务失败",
- payload: { source_job_id: fallbackJob.id }
- });
- return;
- }
- openCreateAiVideoAction();
+ openCreateAiVideoAction(fallbackJob?.id ? { sourceJobId: fallbackJob.id, sourceJob: fallbackJob } : {});
return;
}
if (name === "direct-create-ai-video") {
@@ -13282,15 +13337,7 @@ document.addEventListener("click", async (event) => {
}
if (name === "open-real-cut") {
const fallbackJob = getLatestCompletedProjectJob();
- if (fallbackJob?.id) {
- await runDirectWorkbenchAction("create-real-cut", {
- busyLabel: "正在创建实拍剪辑任务...",
- errorTitle: "创建实拍剪辑任务失败",
- payload: { source_job_id: fallbackJob.id }
- });
- return;
- }
- openCreateRealCutAction();
+ openCreateRealCutAction(fallbackJob?.id ? { sourceJobId: fallbackJob.id, sourceJob: fallbackJob } : {});
return;
}
if (name === "direct-create-real-cut") {
diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
index 5a42d17..b7dcf08 100644
--- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs
+++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
@@ -254,6 +254,7 @@ test("discovery, production, and admin screens use page tabs for heavy content",
assert.match(discovery, /renderDetailTabs\("discoveryDetailTab"/);
assert.match(production, /renderDetailTabs\("productionDetailTab"/);
+ assert.match(production, /value: "recorder", label: "视频录制"/);
assert.match(admin, /renderDetailTabs\("adminWorkbenchTab"/);
assert.match(admin, /renderAdminGovernanceSummaryPanel\(/);
assert.match(admin, /覆盖与审计/);
@@ -417,8 +418,8 @@ test("pipeline follow-up tags route source-job actions through direct execute ha
const pipelineGuards = extractBetween(APP, "const PIPELINE_GUARDS = {", "const ONELINER_INTENT_LABELS = {");
const clickActions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
- assert.match(pipelineGuards, /openAction:\s*"direct-create-ai-video"/);
- assert.match(pipelineGuards, /openAction:\s*"direct-create-real-cut"/);
+ assert.match(pipelineGuards, /openAction:\s*"open-ai-video"/);
+ assert.match(pipelineGuards, /openAction:\s*"open-real-cut"/);
assert.match(pipelineGuards, /jobAction:\s*"direct-create-ai-video"/);
assert.match(pipelineGuards, /jobAction:\s*"direct-create-real-cut"/);
assert.match(clickActions, /name === "direct-create-ai-video"[\s\S]*if \(action\.dataset\.jobId\) \{[\s\S]*closeActionModal\(\);/);
@@ -472,6 +473,7 @@ test("discovery and production screens expose mobile focus cards with next-step
test("mobile discovery and production simplify duplicated top-level actions", () => {
const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()");
const production = extractBetween(APP, "function renderProductionScreen()", "function renderReviewScreen()");
+ const clickActions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "navButtons.forEach((button) => {");
assert.match(APP, /function isMobileViewport\(\)/);
assert.match(discovery, /const isMobileUi = isMobileViewport\(\);/);
@@ -482,8 +484,10 @@ test("mobile discovery and production simplify duplicated top-level actions", ()
assert.match(discovery, /saveBenchmarkActionName = similarCandidates\.length \? "direct-save-benchmark-link" : "open-benchmark-link"/);
assert.match(production, /const isMobileUi = isMobileViewport\(\);/);
assert.match(production, /const productionActionsHtml = isMobileUi/);
+ assert.match(production, /button\("视频录制", "focus-live-recorder-maintenance", "secondary"\)/);
assert.match(production, /button\("交给主 Agent", "handoff-to-main-agent"/);
assert.match(production, /button\("去复盘", "goto-review", "primary"\)/);
+ assert.match(clickActions, /name === "focus-live-recorder-maintenance"[\s\S]*captureMainAgentLandingContext\(action,\s*"goto-production"\);[\s\S]*focusLiveRecorderMaintenance\(\)/);
});
test("discovery page promotes selected-account actions into direct execute flows", () => {
@@ -1080,6 +1084,8 @@ test("playbook and review high-frequency actions now reuse direct execute handle
const playbook = extractBetween(APP, "function renderPlaybookScreen()", "function renderProductionScreen()");
const review = extractBetween(APP, "function renderReviewScreen()", "function renderStrategyScreen()");
const clickActions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
+ const openAiVideoBlock = extractBetween(clickActions, 'if (name === "open-ai-video") {', 'if (name === "direct-create-ai-video") {');
+ const openRealCutBlock = extractBetween(clickActions, 'if (name === "open-real-cut") {', 'if (name === "direct-create-real-cut") {');
assert.match(playbook, /direct-create-assistant/);
assert.match(review, /direct-review-draft/);
assert.match(APP, /payload: action\.dataset\.jobId \? \{ source_job_id: action\.dataset\.jobId \} : \{\}/);
@@ -1091,12 +1097,12 @@ test("playbook and review high-frequency actions now reuse direct execute handle
assert.match(clickActions, /name === "open-generate-copy"[\s\S]*openGenerateCopyAction\(\)/);
assert.match(clickActions, /name === "open-review-from-job"[\s\S]*runDirectWorkbenchAction\("review-draft"/);
assert.match(clickActions, /name === "open-review-from-job"[\s\S]*payload: \{ source_job_id: jobId \}/);
- assert.match(clickActions, /name === "open-ai-video"[\s\S]*const fallbackJob = getLatestCompletedProjectJob\(\)/);
- assert.match(clickActions, /name === "open-ai-video"[\s\S]*runDirectWorkbenchAction\("create-ai-video"/);
- assert.match(clickActions, /name === "open-ai-video"[\s\S]*openCreateAiVideoAction\(\)/);
- assert.match(clickActions, /name === "open-real-cut"[\s\S]*const fallbackJob = getLatestCompletedProjectJob\(\)/);
- assert.match(clickActions, /name === "open-real-cut"[\s\S]*runDirectWorkbenchAction\("create-real-cut"/);
- assert.match(clickActions, /name === "open-real-cut"[\s\S]*openCreateRealCutAction\(\)/);
+ assert.match(openAiVideoBlock, /const fallbackJob = getLatestCompletedProjectJob\(\)/);
+ assert.match(openAiVideoBlock, /openCreateAiVideoAction\(/);
+ assert.doesNotMatch(openAiVideoBlock, /runDirectWorkbenchAction\("create-ai-video"/);
+ assert.match(openRealCutBlock, /const fallbackJob = getLatestCompletedProjectJob\(\)/);
+ assert.match(openRealCutBlock, /openCreateRealCutAction\(/);
+ assert.doesNotMatch(openRealCutBlock, /runDirectWorkbenchAction\("create-real-cut"/);
assert.match(clickActions, /name === "open-create-assistant"[\s\S]*const project = getSelectedProject\(\)/);
assert.match(clickActions, /name === "open-create-assistant"[\s\S]*runDirectWorkbenchAction\("create-assistant"/);
assert.match(clickActions, /name === "open-create-assistant"[\s\S]*openCreateAssistantAction\(\)/);
@@ -1105,6 +1111,23 @@ test("playbook and review high-frequency actions now reuse direct execute handle
assert.match(clickActions, /name === "open-import-homepage"[\s\S]*openImportHomepageAction\(\)/);
});
+test("ai video form explains where Seedance 火山配置 lives", () => {
+ const clickActions = extractBetween(
+ APP,
+ 'document.addEventListener("click", async (event) => {',
+ 'navButtons.forEach((button) => {'
+ );
+ assert.match(APP, /function renderAiVideoProviderHintHtml\(provider = "doubao"\)/);
+ assert.match(APP, /Seedance 2\.0 走火山视频配置/);
+ assert.match(APP, /\/settings\/ai-config/);
+ assert.match(APP, /视频 -> 火山引擎/);
+ assert.match(APP, /HUOBAO_VIDEO_API_KEY/);
+ assert.match(APP, /data-action="focus-huobao-video-config"/);
+ assert.match(APP, /id="integration-\$\{escapeHtml\(item\.key\)\}-anchor"/);
+ assert.match(APP, /function focusAutomationHealthWorkspace\(anchorId = "integration-huobao-anchor"\)/);
+ assert.match(clickActions, /name === "focus-huobao-video-config"[\s\S]*focusAutomationHealthWorkspace\("integration-huobao-anchor"\)/);
+});
+
test("main agent landing notices expose a compact mobile follow-up strip", () => {
const landing = extractBetween(APP, "function renderMainAgentLandingNotice(screenKey)", "function renderEmptyState(title, description)");
assert.match(landing, /mobile-only compact-summary-row/);