diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index e056332..20cf50a 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -1714,10 +1714,6 @@ async function loadKnowledgeDocuments(knowledgeBases) { } async function loadStorageStatus(projectId = "") { - if (!backendSupports("/v2/storage/status")) { - appState.storageStatus = null; - return null; - } const suffix = projectId ? `?project_id=${encodeURIComponent(projectId)}` : ""; const payload = await storyforgeFetch(`/v2/storage/status${suffix}`).catch(() => null); appState.storageStatus = payload; @@ -1884,9 +1880,6 @@ async function loadOneLinerMessages(sessionId) { async function ensureOneLinerSession() { const projectId = getOneLinerProjectId(); if (!projectId) throw new Error("当前还没有项目,OneLiner 需要先绑定项目上下文。"); - if (!backendSupports("/v2/oneliner/sessions")) { - throw new Error("当前后端还没有接入 OneLiner 会话接口。"); - } let session = getCurrentOneLinerSession(); if (!session) { session = await storyforgeFetch("/v2/oneliner/sessions", { @@ -1895,6 +1888,11 @@ async function ensureOneLinerSession() { project_id: projectId, preferred_platform: getPreferredPlatform() } + }).catch((error) => { + if (isMissingBackendCapability(error)) { + throw new Error("当前实例还没有开放 OneLiner 会话接口。"); + } + throw error; }); appState.onelinerSessions = [session, ...safeArray(appState.onelinerSessions)]; appState.selectedOnelinerSessionId = session.id; @@ -1931,9 +1929,6 @@ async function submitOneLinerMessage(content) { } async function createOneLinerRun(runRequest) { - if (!backendSupports("/v2/oneliner/runs")) { - throw new Error("当前后端还没有接入主 Agent 运行层。"); - } const projectId = getOneLinerProjectId(); const payload = await storyforgeFetch("/v2/oneliner/runs", { method: "POST", @@ -1945,6 +1940,11 @@ async function createOneLinerRun(runRequest) { scheduling_mode: "queued", ...runRequest } + }).catch((error) => { + if (isMissingBackendCapability(error)) { + throw new Error("当前实例还没有开放主 Agent 运行层。"); + } + throw error; }); await loadAgentControlSurfaces(projectId); appState.selectedOnelinerRunId = payload?.id || choosePreferredOneLinerRunId(appState.onelinerRuns, ""); @@ -2229,9 +2229,6 @@ function collectOneLinerActionPayload(action) { } async function executeOneLinerAction(executorKey, options = {}) { - if (!backendSupports("/v2/oneliner/actions/execute")) { - throw new Error("当前后端还没有接入 OneLiner 动作执行器。"); - } const projectId = getOneLinerProjectId(); const session = getCurrentOneLinerSession() || await ensureOneLinerSession(); const payload = await storyforgeFetch("/v2/oneliner/actions/execute", { @@ -2243,6 +2240,11 @@ async function executeOneLinerAction(executorKey, options = {}) { session_id: options.sessionId || session?.id || "", payload: options.payload || {} } + }).catch((error) => { + if (isMissingBackendCapability(error)) { + throw new Error("当前实例还没有开放 OneLiner 动作执行器。"); + } + throw error; }); await loadAgentControlSurfaces(projectId); if (appState.selectedOnelinerSessionId) { @@ -2500,10 +2502,8 @@ async function bootstrap() { const runtimePlatforms = getRuntimePlatformValues(); const preferredPlatform = getCurrentPlatformValue(); setCurrentPlatform(preferredPlatform); - const supportsReviews = backendSupports("/v2/reviews"); const supportsIntegrationHealth = backendSupports("/v2/integrations/health"); const supportsLocalModels = backendSupports("/v2/integrations/local-models"); - const supportsStorageStatus = backendSupports("/v2/storage/status"); const supportsLiveRecorderSources = backendSupports("/v2/live-recorder/sources"); const supportsLiveRecorderStatus = backendSupports("/v2/live-recorder/status"); const supportsLiveRecorderFiles = backendSupports("/v2/live-recorder/files"); @@ -2545,7 +2545,7 @@ async function bootstrap() { trackingDigest }; })), - supportsReviews ? storyforgeFetch("/v2/reviews").catch(() => []) : Promise.resolve([]), + storyforgeFetch("/v2/reviews").catch(() => []), supportsIntegrationHealth ? storyforgeFetch("/v2/integrations/health").catch(() => null) : Promise.resolve(null), supportsLocalModels ? storyforgeFetch("/v2/integrations/local-models").catch(() => null) : Promise.resolve(null), supportsLiveRecorderSources ? storyforgeFetch("/v2/live-recorder/sources").catch(() => ({ items: [] })) : Promise.resolve({ items: [] }) @@ -2605,11 +2605,7 @@ async function bootstrap() { appState.liveRecorderHealth = liveRecorderHealth; appState.documents = await loadKnowledgeDocuments(dashboard.knowledge_bases); appState.selectedProjectId = appState.selectedProjectId || dashboard.projects?.[0]?.id || ""; - if (supportsStorageStatus) { - await loadStorageStatus(appState.selectedProjectId || ""); - } else { - appState.storageStatus = null; - } + await loadStorageStatus(appState.selectedProjectId || ""); await loadAgentControlSurfaces(appState.selectedProjectId || ""); if (appState.selectedOnelinerSessionId) { await loadOneLinerMessages(appState.selectedOnelinerSessionId); @@ -3220,11 +3216,7 @@ async function applySelectedProject(projectId = "") { appState.selectedProjectId = projectId || ""; setBusy(true, "正在切换项目视图..."); try { - if (backendSupports("/v2/storage/status")) { - await loadStorageStatus(appState.selectedProjectId || ""); - } else { - appState.storageStatus = null; - } + await loadStorageStatus(appState.selectedProjectId || ""); await loadAgentControlSurfaces(appState.selectedProjectId || ""); if (appState.selectedOnelinerSessionId) { await loadOneLinerMessages(appState.selectedOnelinerSessionId); @@ -3824,13 +3816,13 @@ function renderStorageStatusPanel() {

存储状态

-
后端暂未提供 /v2/storage/status,先用任务和录像文件做本地观察
+
当前实例没有返回存储策略时,先用任务和录像文件做本地观察
降级视图

未拉取到 NAS 策略

-

后端补上 storage/status 后,这里会自动显示账号 / 项目 / 任务分层、容量和最近写入路径。

+

当 storage/status 暂时拿不到时,这里会自动退回到任务和录像文件的降级视图。

最近任务 ${escapeHtml(formatNumber(projectJobCount))} 录制源 ${escapeHtml(formatNumber(liveRecorderSources.length))} @@ -6896,14 +6888,6 @@ function renderReviewScreen() { } return screenShell("发布与复盘", "先自动连接工作区。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("复盘未加载", "自动连接成功后,这里会先用最近任务生成一版复盘入口。")); } - if (!backendSupports("/v2/reviews")) { - return screenShell( - "发布与复盘", - "当前 live collector 还没有接入复盘读写接口。", - `${button("去生产", "goto-production", "primary")}`, - renderEmptyState("复盘能力暂未接入", "这套后端还缺 /v2/reviews,当前可以继续跑生产任务,等 live collector 同步后这里会自动切成真实复盘工作台。") - ); - } const project = getSelectedProject(); const completed = safeArray(appState.dashboard.recent_jobs).filter((item) => item.status === "completed").slice(0, 4); const reviews = getProjectReviews(project?.id || "").slice(0, 8); @@ -11314,11 +11298,7 @@ document.addEventListener("click", async (event) => { appState.selectedProjectId = action.dataset.projectId || ""; setBusy(true, "正在切换项目视图..."); try { - if (backendSupports("/v2/storage/status")) { - await loadStorageStatus(appState.selectedProjectId || ""); - } else { - appState.storageStatus = null; - } + await loadStorageStatus(appState.selectedProjectId || ""); await loadAgentControlSurfaces(appState.selectedProjectId || ""); if (appState.selectedOnelinerSessionId) { await loadOneLinerMessages(appState.selectedOnelinerSessionId); diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 46bc152..90632c8 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -274,21 +274,28 @@ test("governance and quota panels use real empty-state language instead of backe test("quota and review screens foreground live next-step guidance", () => { const tenantQuota = extractBetween(APP, "function renderTenantQuotaPanel()", "function policyScopeTagLabel("); const review = extractBetween(APP, "function renderReviewScreen()", "function renderStrategyScreen()"); + const storage = extractBetween(APP, "function renderStorageStatusPanel()", "function renderAutomationScreen()"); assert.match(tenantQuota, /先处理存储超限|先恢复额度保护|先补项目额度策略|先检查本周期消耗|先跑出第一条计量/); assert.match(tenantQuota, /主要消耗/); assert.match(tenantQuota, /周期 /); assert.match(tenantQuota, /计量 /); + assert.doesNotMatch(review, /复盘能力暂未接入|还缺 \/v2\/reviews/); assert.match(review, /先把最近完成任务写成复盘|先回看高频结论|先跑出第一条可复盘任务/); assert.match(review, /高频结论/); assert.match(review, /已发布/); + assert.doesNotMatch(storage, /后端暂未提供 \/v2\/storage\/status/); + assert.match(storage, /当前实例没有返回存储策略时/); }); test("tracking refresh and top-video analysis flows expose async feedback inside the workbench", () => { const tracking = extractBetween(APP, "function renderTrackingScreen()", "function renderAutomationScreen()"); const discovery = extractBetween(APP, "function renderDiscoveryOverviewSection(", "function renderDiscoveryRelationsSection("); const trackingActions = extractBetween(APP, "async function markTrackingDigestRead()", "function createEmptyTrackingDigest("); + const oneLinerSession = extractBetween(APP, "async function ensureOneLinerSession()", "async function submitOneLinerMessage("); + const oneLinerRun = extractBetween(APP, "async function createOneLinerRun(", "async function confirmOneLinerRun("); + const oneLinerAction = extractBetween(APP, "async function executeOneLinerAction(", "function openCurrentOneLinerRunResultAction("); const skillReview = extractBetween(APP, "function openPlatformSkillReviewAction(", "function openPlatformSkillRollbackAction("); const topVideoAction = extractBetween(APP, "function openAnalyzeTopVideosAction()", "function openSimilaritySearchAction()"); @@ -302,6 +309,9 @@ test("tracking refresh and top-video analysis flows expose async feedback inside assert.match(discovery, /最近高分拆解/); assert.match(discovery, /这批结果已经回流到当前账号页/); assert.doesNotMatch(trackingActions, /当前后端暂不支持.*跟踪已读游标|当前后端暂不支持.*批量跟踪同步|当前后端暂不支持.*单账号跟踪同步/s); + assert.doesNotMatch(oneLinerSession, /当前后端还没有接入 OneLiner 会话接口/); + assert.doesNotMatch(oneLinerRun, /当前后端还没有接入主 Agent 运行层/); + assert.doesNotMatch(oneLinerAction, /当前后端还没有接入 OneLiner 动作执行器/); assert.doesNotMatch(skillReview, /当前后端还没有接入平台技能验收接口/); assert.doesNotMatch(topVideoAction, /当前后端暂不支持.*高分作品批量分析/s); assert.match(topVideoAction, /当前实例未提供/);