feat: improve ai video entry and video recorder discoverability
This commit is contained in:
12
CHANGELOG.md
12
CHANGELOG.md
@@ -4,6 +4,12 @@
|
|||||||
|
|
||||||
## 2026-04-07
|
## 2026-04-07
|
||||||
|
|
||||||
|
### AI 视频表单可直接跳到火山视频配置状态
|
||||||
|
|
||||||
|
- `创建 AI 视频任务` 里的 `Seedance 配置` 提示现在不再只是静态文案,而是新增了 `查看火山配置状态` 入口。
|
||||||
|
- 点击后会直接跳到 `自动流程 -> 依赖健康 -> Huobao` 卡片,立刻看到当前火山视频配置是否就绪、部署位置和配置提示,不用再自己记 `/settings/ai-config -> 视频 -> 火山引擎` 再手动找入口。
|
||||||
|
- 同时 `依赖健康` 里的各张集成卡现在都带稳定锚点,后续其他配置提示也可以直接把用户带到最相关的健康卡,而不是只停在说明文字里。
|
||||||
|
|
||||||
### 修复额度页套餐建议引起的全局渲染报错
|
### 修复额度页套餐建议引起的全局渲染报错
|
||||||
|
|
||||||
- `额度` 页面现在会先初始化 `packageRecommendation` 再渲染套餐建议,不再因为变量未定义把整个工作台渲染链打断。
|
- `额度` 页面现在会先初始化 `packageRecommendation` 再渲染套餐建议,不再因为变量未定义把整个工作台渲染链打断。
|
||||||
@@ -662,3 +668,9 @@
|
|||||||
- `创建 Agent / 编辑 Agent` 这两张表单也补成了带上下文和知识库联动的产品化表单:创建时切项目会同步刷新默认知识库,编辑时可以直接更新默认知识库,不必再回别处改。
|
- `创建 Agent / 编辑 Agent` 这两张表单也补成了带上下文和知识库联动的产品化表单:创建时切项目会同步刷新默认知识库,编辑时可以直接更新默认知识库,不必再回别处改。
|
||||||
- 额度页残留的半成品口径已收口,不再出现“后端尚未完全接入真实预算”这类提示;未配置独立额度策略时,会直接引导按预算基线和动作池去建立试用、增长或规模套餐。
|
- 额度页残留的半成品口径已收口,不再出现“后端尚未完全接入真实预算”这类提示;未配置独立额度策略时,会直接引导按预算基线和动作池去建立试用、增长或规模套餐。
|
||||||
- `smoke_public_storyforge.sh` 和 `smoke_fnos_storyforge_lan.sh` 现在会显式校验 `integrations/health` 的关键依赖状态、部署位置和 `local_model=not_configured` 口径,不再只看页面能打开和基础 healthz。
|
- `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` 任务为什么只建单不出片的歧义。
|
||||||
|
|||||||
@@ -3188,6 +3188,39 @@ def probe_http_json(url: str, path: str = "", timeout: float = 3.0) -> dict[str,
|
|||||||
return detail
|
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:
|
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:
|
if not LIVE_RECORDER_BASE_URL:
|
||||||
raise HTTPException(status_code=503, detail="LIVE_RECORDER_BASE_URL is not configured")
|
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)
|
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_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 {}
|
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_supports_uploads = bool(
|
||||||
cutvideo_uploads.get("configured")
|
cutvideo_uploads.get("configured")
|
||||||
and cutvideo_uploads.get("reachable")
|
and cutvideo_uploads.get("reachable")
|
||||||
@@ -3336,7 +3371,8 @@ def integrations_health(account: dict[str, Any] = Depends(require_approved)) ->
|
|||||||
"huobao": {
|
"huobao": {
|
||||||
"base_url": HUOBAO_BASE_URL,
|
"base_url": HUOBAO_BASE_URL,
|
||||||
**integration_deployment_payload("huobao", HUOBAO_BASE_URL),
|
**integration_deployment_payload("huobao", HUOBAO_BASE_URL),
|
||||||
**probe_http(HUOBAO_BASE_URL, "/health"),
|
**huobao_probe,
|
||||||
|
**huobao_video_config,
|
||||||
},
|
},
|
||||||
"n8n": {
|
"n8n": {
|
||||||
"base_url": N8N_BASE_URL,
|
"base_url": N8N_BASE_URL,
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ class ProductionBaselineTests(unittest.TestCase):
|
|||||||
original_cutvideo = self.core.CUTVIDEO_BASE_URL
|
original_cutvideo = self.core.CUTVIDEO_BASE_URL
|
||||||
original_probe_http = self.core.probe_http
|
original_probe_http = self.core.probe_http
|
||||||
original_probe_http_json = getattr(self.core, "probe_http_json", None)
|
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:
|
try:
|
||||||
self.core.N8N_BASE_URL = "http://127.0.0.1:25670"
|
self.core.N8N_BASE_URL = "http://127.0.0.1:25670"
|
||||||
self.core.HUOBAO_BASE_URL = "http://127.0.0.1:25678"
|
self.core.HUOBAO_BASE_URL = "http://127.0.0.1:25678"
|
||||||
@@ -381,8 +382,16 @@ class ProductionBaselineTests(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
return detail
|
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 = fake_probe_http
|
||||||
self.core.probe_http_json = fake_probe_http_json
|
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)
|
response = self.client.get("/v2/integrations/health", headers=headers)
|
||||||
finally:
|
finally:
|
||||||
self.core.N8N_BASE_URL = original_n8n
|
self.core.N8N_BASE_URL = original_n8n
|
||||||
@@ -398,11 +407,20 @@ class ProductionBaselineTests(unittest.TestCase):
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
self.core.probe_http_json = original_probe_http_json
|
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)
|
self.assertEqual(response.status_code, 200, response.text)
|
||||||
payload = response.json()
|
payload = response.json()
|
||||||
self.assertEqual(payload["n8n"]["deployment_label"], "服务器")
|
self.assertEqual(payload["n8n"]["deployment_label"], "服务器")
|
||||||
self.assertEqual(payload["huobao"]["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["asr"]["deployment_label"], "Windows")
|
||||||
self.assertEqual(payload["live_recorder"]["deployment_label"], "NAS")
|
self.assertEqual(payload["live_recorder"]["deployment_label"], "NAS")
|
||||||
self.assertEqual(payload["cutvideo"]["deployment_label"], "NAS 隧道")
|
self.assertEqual(payload["cutvideo"]["deployment_label"], "NAS 隧道")
|
||||||
|
|||||||
@@ -206,13 +206,13 @@ const INTEGRATION_META = {
|
|||||||
const PIPELINE_GUARDS = {
|
const PIPELINE_GUARDS = {
|
||||||
aiVideo: {
|
aiVideo: {
|
||||||
label: "AI 视频",
|
label: "AI 视频",
|
||||||
openAction: "direct-create-ai-video",
|
openAction: "open-ai-video",
|
||||||
jobAction: "direct-create-ai-video",
|
jobAction: "direct-create-ai-video",
|
||||||
dependencies: ["n8n", "huobao"]
|
dependencies: ["n8n", "huobao"]
|
||||||
},
|
},
|
||||||
realCut: {
|
realCut: {
|
||||||
label: "实拍剪辑",
|
label: "实拍剪辑",
|
||||||
openAction: "direct-create-real-cut",
|
openAction: "open-real-cut",
|
||||||
jobAction: "direct-create-real-cut",
|
jobAction: "direct-create-real-cut",
|
||||||
dependencies: ["n8n", "cutvideo"]
|
dependencies: ["n8n", "cutvideo"]
|
||||||
}
|
}
|
||||||
@@ -4299,7 +4299,10 @@ function getIntegrationDetail(key) {
|
|||||||
activeDevice: String(raw?.active_device || ""),
|
activeDevice: String(raw?.active_device || ""),
|
||||||
activeComputeType: String(raw?.active_compute_type || ""),
|
activeComputeType: String(raw?.active_compute_type || ""),
|
||||||
languageMode: String(raw?.language_mode || ""),
|
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) {
|
if (detail.modelName) {
|
||||||
extra += ` · 当前模型:${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) {
|
} else if (detail.deploymentLabel) {
|
||||||
extra = `部署:${detail.deploymentLabel}`;
|
extra = `部署:${detail.deploymentLabel}`;
|
||||||
}
|
}
|
||||||
@@ -5647,6 +5660,7 @@ function renderIntegrationOverviewPanel(options = {}) {
|
|||||||
<div class="integration-actions">
|
<div class="integration-actions">
|
||||||
${renderPipelineButton("aiVideo", "primary")}
|
${renderPipelineButton("aiVideo", "primary")}
|
||||||
${renderPipelineButton("realCut")}
|
${renderPipelineButton("realCut")}
|
||||||
|
${button("视频录制", "focus-live-recorder-maintenance", "secondary")}
|
||||||
</div>
|
</div>
|
||||||
` : ""}
|
` : ""}
|
||||||
</div>
|
</div>
|
||||||
@@ -5655,7 +5669,7 @@ function renderIntegrationOverviewPanel(options = {}) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="layout-grid grid-4 integration-grid">
|
<div class="layout-grid grid-4 integration-grid">
|
||||||
${cards.map((item) => `
|
${cards.map((item) => `
|
||||||
<div class="integration-card ${item.status.tone}">
|
<div class="integration-card ${item.status.tone}" id="integration-${escapeHtml(item.key)}-anchor">
|
||||||
<div class="integration-card-head">
|
<div class="integration-card-head">
|
||||||
<div>
|
<div>
|
||||||
<h4>${escapeHtml(item.meta.label)}</h4>
|
<h4>${escapeHtml(item.meta.label)}</h4>
|
||||||
@@ -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 = "") {
|
function focusReviewWorkspace(reviewId = "") {
|
||||||
appState.reviewFocusId = reviewId || "";
|
appState.reviewFocusId = reviewId || "";
|
||||||
setScreen("review");
|
setScreen("review");
|
||||||
@@ -7541,7 +7565,7 @@ function renderProductionMobileTaskDeck({ activeTab, activeJobs, failedJobs, rec
|
|||||||
<p>${escapeHtml(status.running ? `Live Recorder 正在运行,当前有 ${activeCount} 路活动录制。` : "先确认 Live Recorder 是否在线,再检查录制源和文件。")}</p>
|
<p>${escapeHtml(status.running ? `Live Recorder 正在运行,当前有 ${activeCount} 路活动录制。` : "先确认 Live Recorder 是否在线,再检查录制源和文件。")}</p>
|
||||||
<div class="task-meta">
|
<div class="task-meta">
|
||||||
<span class="tag ${status.running ? "green" : "orange"}">${escapeHtml(status.running ? "运行中" : "待检查")}</span>
|
<span class="tag ${status.running ? "green" : "orange"}">${escapeHtml(status.running ? "运行中" : "待检查")}</span>
|
||||||
${actionTag("录制维护", "select-page-tab", `data-page-tab-key="productionDetailTab" data-page-tab-value="recorder"`)}
|
${actionTag("视频录制", "focus-live-recorder-maintenance")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
@@ -7554,12 +7578,12 @@ function renderProductionMobileTaskDeck({ activeTab, activeJobs, failedJobs, rec
|
|||||||
sourceScreen: "production",
|
sourceScreen: "production",
|
||||||
sourceActionKey: "production-mobile-recorder-handoff",
|
sourceActionKey: "production-mobile-recorder-handoff",
|
||||||
intentKey: "production_coordination",
|
intentKey: "production_coordination",
|
||||||
title: "继续处理录制维护",
|
title: "继续处理视频录制",
|
||||||
goal: "继续处理录制维护",
|
goal: "继续处理视频录制",
|
||||||
summary: "结合录制维护状态给出下一步动作。",
|
summary: "结合视频录制状态给出下一步动作。",
|
||||||
platform: getPreferredPlatform(),
|
platform: getPreferredPlatform(),
|
||||||
platformScope: "single_platform",
|
platformScope: "single_platform",
|
||||||
planSteps: ["读取录制维护状态", "识别当前阻塞项", "生成下一步处理动作"]
|
planSteps: ["读取视频录制状态", "识别当前阻塞项", "生成下一步处理动作"]
|
||||||
}))}
|
}))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -7662,13 +7686,13 @@ function renderProductionScreen() {
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{ value: "queue", label: "生产队列" },
|
{ value: "queue", label: "生产队列" },
|
||||||
{ value: "recovery", label: "失败恢复" },
|
{ value: "recovery", label: "失败恢复" },
|
||||||
{ value: "recorder", label: "录制维护" },
|
{ value: "recorder", label: "视频录制" },
|
||||||
{ value: "outputs", label: "作品与产物" }
|
{ value: "outputs", label: "作品与产物" }
|
||||||
];
|
];
|
||||||
const activeTab = getActiveDetailTab("productionDetailTab", tabs);
|
const activeTab = getActiveDetailTab("productionDetailTab", tabs);
|
||||||
const productionActionsHtml = isMobileUi
|
const productionActionsHtml = isMobileUi
|
||||||
? `${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${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 })}`
|
||||||
: `${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 })} ${button("去复盘", "goto-review", "primary")} ${button("批量恢复", "batch-recover-jobs", "secondary", { disabledReason: recoverableCount ? "" : "当前没有可恢复的失败任务" })}`;
|
||||||
return screenShell(
|
return screenShell(
|
||||||
"生产中心",
|
"生产中心",
|
||||||
"这里已经接上真实任务、失败恢复和知识库文档,适合直接推进生产、恢复和复盘。",
|
"这里已经接上真实任务、失败恢复和知识库文档,适合直接推进生产、恢复和复盘。",
|
||||||
@@ -7717,7 +7741,7 @@ function renderProductionScreen() {
|
|||||||
: activeTab === "outputs"
|
: activeTab === "outputs"
|
||||||
? `${actionTag("去复盘", "goto-review")} ${actionTag("查看产物", "select-page-tab", `data-page-tab-key="productionDetailTab" data-page-tab-value="outputs"`)}`
|
? `${actionTag("去复盘", "goto-review")} ${actionTag("查看产物", "select-page-tab", `data-page-tab-key="productionDetailTab" data-page-tab-value="outputs"`)}`
|
||||||
: activeTab === "recorder"
|
: 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)}`
|
: `${actionTag("批量恢复", "batch-recover-jobs")} ${actionTag("交给主 Agent", "handoff-to-main-agent", productionHandoffAttrs)}`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -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 `
|
||||||
|
<div class="sheet-html">
|
||||||
|
<div class="task-item compact">
|
||||||
|
<h4>${escapeHtml(`${providerLabel} 走火山视频配置`)}</h4>
|
||||||
|
<p>${escapeHtml(`请在 Huobao 服务里配置火山视频 Key:${route}`)}</p>
|
||||||
|
<p>${escapeHtml("如果不是走页面配置,也可以在 huobao 服务环境变量里覆盖 HUOBAO_VIDEO_BASE_URL / HUOBAO_VIDEO_API_KEY / HUOBAO_VIDEO_MODELS。")}</p>
|
||||||
|
<div class="task-meta">
|
||||||
|
<span class="tag ${huobao.videoConfigReady ? "green" : "orange"}">${escapeHtml(configStatus)}</span>
|
||||||
|
${huobao.deploymentLabel ? `<span class="tag">${escapeHtml(`部署:${huobao.deploymentLabel}`)}</span>` : ""}
|
||||||
|
<span class="tag clickable-tag" data-action="focus-huobao-video-config">查看火山配置状态</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function openCreateAiVideoAction(defaults = {}) {
|
function openCreateAiVideoAction(defaults = {}) {
|
||||||
const guard = getPipelineGuard("aiVideo");
|
const guard = getPipelineGuard("aiVideo");
|
||||||
if (!guard.enabled) {
|
if (!guard.enabled) {
|
||||||
@@ -12126,6 +12173,12 @@ function openCreateAiVideoAction(defaults = {}) {
|
|||||||
value: defaultVideoModel,
|
value: defaultVideoModel,
|
||||||
placeholder: "例如:seedance-2.0-pro",
|
placeholder: "例如:seedance-2.0-pro",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "videoProviderHint",
|
||||||
|
label: "Seedance 配置",
|
||||||
|
type: "html",
|
||||||
|
html: renderAiVideoProviderHintHtml(defaultVideoProvider),
|
||||||
|
},
|
||||||
{ name: "style", label: "风格", value: defaults.style || recommendCreativeStyle(sourceJob) },
|
{ name: "style", label: "风格", value: defaults.style || recommendCreativeStyle(sourceJob) },
|
||||||
{
|
{
|
||||||
name: "aspectRatio",
|
name: "aspectRatio",
|
||||||
@@ -12788,6 +12841,16 @@ document.addEventListener("click", async (event) => {
|
|||||||
setScreen("production");
|
setScreen("production");
|
||||||
return;
|
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") {
|
if (name === "goto-strategy") {
|
||||||
captureMainAgentLandingContext(action, "goto-strategy");
|
captureMainAgentLandingContext(action, "goto-strategy");
|
||||||
setScreen("strategy");
|
setScreen("strategy");
|
||||||
@@ -13258,15 +13321,7 @@ document.addEventListener("click", async (event) => {
|
|||||||
}
|
}
|
||||||
if (name === "open-ai-video") {
|
if (name === "open-ai-video") {
|
||||||
const fallbackJob = getLatestCompletedProjectJob();
|
const fallbackJob = getLatestCompletedProjectJob();
|
||||||
if (fallbackJob?.id) {
|
openCreateAiVideoAction(fallbackJob?.id ? { sourceJobId: fallbackJob.id, sourceJob: fallbackJob } : {});
|
||||||
await runDirectWorkbenchAction("create-ai-video", {
|
|
||||||
busyLabel: "正在创建 AI 视频任务...",
|
|
||||||
errorTitle: "创建 AI 视频任务失败",
|
|
||||||
payload: { source_job_id: fallbackJob.id }
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
openCreateAiVideoAction();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (name === "direct-create-ai-video") {
|
if (name === "direct-create-ai-video") {
|
||||||
@@ -13282,15 +13337,7 @@ document.addEventListener("click", async (event) => {
|
|||||||
}
|
}
|
||||||
if (name === "open-real-cut") {
|
if (name === "open-real-cut") {
|
||||||
const fallbackJob = getLatestCompletedProjectJob();
|
const fallbackJob = getLatestCompletedProjectJob();
|
||||||
if (fallbackJob?.id) {
|
openCreateRealCutAction(fallbackJob?.id ? { sourceJobId: fallbackJob.id, sourceJob: fallbackJob } : {});
|
||||||
await runDirectWorkbenchAction("create-real-cut", {
|
|
||||||
busyLabel: "正在创建实拍剪辑任务...",
|
|
||||||
errorTitle: "创建实拍剪辑任务失败",
|
|
||||||
payload: { source_job_id: fallbackJob.id }
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
openCreateRealCutAction();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (name === "direct-create-real-cut") {
|
if (name === "direct-create-real-cut") {
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ test("discovery, production, and admin screens use page tabs for heavy content",
|
|||||||
|
|
||||||
assert.match(discovery, /renderDetailTabs\("discoveryDetailTab"/);
|
assert.match(discovery, /renderDetailTabs\("discoveryDetailTab"/);
|
||||||
assert.match(production, /renderDetailTabs\("productionDetailTab"/);
|
assert.match(production, /renderDetailTabs\("productionDetailTab"/);
|
||||||
|
assert.match(production, /value: "recorder", label: "视频录制"/);
|
||||||
assert.match(admin, /renderDetailTabs\("adminWorkbenchTab"/);
|
assert.match(admin, /renderDetailTabs\("adminWorkbenchTab"/);
|
||||||
assert.match(admin, /renderAdminGovernanceSummaryPanel\(/);
|
assert.match(admin, /renderAdminGovernanceSummaryPanel\(/);
|
||||||
assert.match(admin, /覆盖与审计/);
|
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 pipelineGuards = extractBetween(APP, "const PIPELINE_GUARDS = {", "const ONELINER_INTENT_LABELS = {");
|
||||||
const clickActions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
|
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*"open-ai-video"/);
|
||||||
assert.match(pipelineGuards, /openAction:\s*"direct-create-real-cut"/);
|
assert.match(pipelineGuards, /openAction:\s*"open-real-cut"/);
|
||||||
assert.match(pipelineGuards, /jobAction:\s*"direct-create-ai-video"/);
|
assert.match(pipelineGuards, /jobAction:\s*"direct-create-ai-video"/);
|
||||||
assert.match(pipelineGuards, /jobAction:\s*"direct-create-real-cut"/);
|
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\(\);/);
|
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", () => {
|
test("mobile discovery and production simplify duplicated top-level actions", () => {
|
||||||
const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()");
|
const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()");
|
||||||
const production = extractBetween(APP, "function renderProductionScreen()", "function renderReviewScreen()");
|
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(APP, /function isMobileViewport\(\)/);
|
||||||
assert.match(discovery, /const isMobileUi = 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(discovery, /saveBenchmarkActionName = similarCandidates\.length \? "direct-save-benchmark-link" : "open-benchmark-link"/);
|
||||||
assert.match(production, /const isMobileUi = isMobileViewport\(\);/);
|
assert.match(production, /const isMobileUi = isMobileViewport\(\);/);
|
||||||
assert.match(production, /const productionActionsHtml = isMobileUi/);
|
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\("交给主 Agent", "handoff-to-main-agent"/);
|
||||||
assert.match(production, /button\("去复盘", "goto-review", "primary"\)/);
|
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", () => {
|
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 playbook = extractBetween(APP, "function renderPlaybookScreen()", "function renderProductionScreen()");
|
||||||
const review = extractBetween(APP, "function renderReviewScreen()", "function renderStrategyScreen()");
|
const review = extractBetween(APP, "function renderReviewScreen()", "function renderStrategyScreen()");
|
||||||
const clickActions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
|
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(playbook, /direct-create-assistant/);
|
||||||
assert.match(review, /direct-review-draft/);
|
assert.match(review, /direct-review-draft/);
|
||||||
assert.match(APP, /payload: action\.dataset\.jobId \? \{ source_job_id: action\.dataset\.jobId \} : \{\}/);
|
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-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]*runDirectWorkbenchAction\("review-draft"/);
|
||||||
assert.match(clickActions, /name === "open-review-from-job"[\s\S]*payload: \{ source_job_id: jobId \}/);
|
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(openAiVideoBlock, /const fallbackJob = getLatestCompletedProjectJob\(\)/);
|
||||||
assert.match(clickActions, /name === "open-ai-video"[\s\S]*runDirectWorkbenchAction\("create-ai-video"/);
|
assert.match(openAiVideoBlock, /openCreateAiVideoAction\(/);
|
||||||
assert.match(clickActions, /name === "open-ai-video"[\s\S]*openCreateAiVideoAction\(\)/);
|
assert.doesNotMatch(openAiVideoBlock, /runDirectWorkbenchAction\("create-ai-video"/);
|
||||||
assert.match(clickActions, /name === "open-real-cut"[\s\S]*const fallbackJob = getLatestCompletedProjectJob\(\)/);
|
assert.match(openRealCutBlock, /const fallbackJob = getLatestCompletedProjectJob\(\)/);
|
||||||
assert.match(clickActions, /name === "open-real-cut"[\s\S]*runDirectWorkbenchAction\("create-real-cut"/);
|
assert.match(openRealCutBlock, /openCreateRealCutAction\(/);
|
||||||
assert.match(clickActions, /name === "open-real-cut"[\s\S]*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]*const project = getSelectedProject\(\)/);
|
||||||
assert.match(clickActions, /name === "open-create-assistant"[\s\S]*runDirectWorkbenchAction\("create-assistant"/);
|
assert.match(clickActions, /name === "open-create-assistant"[\s\S]*runDirectWorkbenchAction\("create-assistant"/);
|
||||||
assert.match(clickActions, /name === "open-create-assistant"[\s\S]*openCreateAssistantAction\(\)/);
|
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\(\)/);
|
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", () => {
|
test("main agent landing notices expose a compact mobile follow-up strip", () => {
|
||||||
const landing = extractBetween(APP, "function renderMainAgentLandingNotice(screenKey)", "function renderEmptyState(title, description)");
|
const landing = extractBetween(APP, "function renderMainAgentLandingNotice(screenKey)", "function renderEmptyState(title, description)");
|
||||||
assert.match(landing, /mobile-only compact-summary-row/);
|
assert.match(landing, /mobile-only compact-summary-row/);
|
||||||
|
|||||||
Reference in New Issue
Block a user