diff --git a/CHANGELOG.md b/CHANGELOG.md index 52c2d94..35eb04d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ - `OneLiner 动作注册表 / 平台 Agent / 租户额度 / 复盘` 已按 live collector 实际能力展示,不再误导成“还没接”。 - `额度` 和 `复盘` 页面首屏已改成围绕 live 数据的任务页,直接展示风险、主要消耗、高频结论和下一步动作。 - `跟踪已读 / 批量跟踪同步 / 单账号跟踪同步 / 高分作品分析 / 平台技能验收` 已改成“真实调用优先”,避免旧 capability 口径把已接好的接口误判成未接入。 +- `OneLiner 会话 / 运行详情 / 治理控制面 / integrations / live-recorder` 这些固定接口也已经切成 live-first,请求失败才降级,不再先被陈旧 capability 表拦住。 +- 任务恢复链会优先真实调用 `/v2/explore/jobs/{job_id}/retry`,只有接口真的不存在时才回退到手动恢复模板。 ### NAS 联调与回归 @@ -35,7 +37,7 @@ - Web: `http://192.168.31.188:19192/` - Collector: `http://192.168.31.188:19193/healthz` - 当前基线通过: - - 前端测试 `59/59` + - 前端测试 `60/60` - `bash scripts/check_repo_baseline.sh` - `bash scripts/smoke_fnos_storyforge_lan.sh` diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 68264be..0bc04b8 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -1722,7 +1722,7 @@ async function loadStorageStatus(projectId = "") { async function hydrateSelectedOneLinerRun() { const runId = appState.selectedOnelinerRunId || ""; - if (!runId || !backendSupports("/v2/oneliner/runs/{run_id}")) { + if (!runId) { return null; } const detail = await storyforgeFetch(`/v2/oneliner/runs/${encodeURIComponent(runId)}`).catch(() => null); @@ -1742,74 +1742,33 @@ async function hydrateSelectedOneLinerRun() { async function loadAgentControlSurfaces(projectId = "") { const normalizedProjectId = projectId || getOneLinerProjectId(); const governancePlatform = normalizePlatformValue(getPreferredPlatform(), "douyin"); - const supportsOneLinerProfile = backendSupports("/v2/oneliner/profile"); - const supportsOneLinerSessions = backendSupports("/v2/oneliner/sessions"); - const supportsOneLinerRuns = backendSupports("/v2/oneliner/runs"); - const supportsActionRegistry = backendSupports("/v2/oneliner/action-registry"); - const supportsPlatformAgents = backendSupports("/v2/platform-agents"); - const supportsGovernanceEffective = backendSupports("/v2/oneliner/governance/effective"); - const supportsUserGlobalPolicy = backendSupports("/v2/oneliner/governance/user/global"); - const supportsUserPlatformPolicy = backendSupports("/v2/oneliner/governance/user/platforms/{platform}"); - const supportsUserPolicyAudits = backendSupports("/v2/oneliner/governance/user/audits"); - const supportsAdminSystemMainPolicy = backendSupports("/v2/admin/oneliner/governance/system/main-agent"); - const supportsAdminSystemPlatformPolicy = backendSupports("/v2/admin/oneliner/governance/system/platforms/{platform}"); - const supportsAdminGovernanceDirectory = backendSupports("/v2/admin/oneliner/governance/directory"); - const supportsAdminOverridePolicy = backendSupports("/v2/admin/oneliner/governance/overrides"); - const supportsAdminGovernanceAudits = backendSupports("/v2/admin/oneliner/governance/audits"); - const supportsAdminOps = backendSupports("/v2/admin/ops/overview"); - const supportsAdminFixRuns = backendSupports("/v2/admin/ops/fix-runs"); - const supportsTenantQuota = backendSupports("/v2/tenant/quota"); - const supportsTenantUsage = backendSupports("/v2/tenant/usage"); - const [profile, sessionsPayload, runsPayload, actionRegistryPayload, platformAgentsPayload, governanceEffective, userGlobalPolicy, userCurrentPlatformPolicy, userPolicyAuditsPayload, adminSystemMainPolicy, adminSystemPlatformPolicies, adminGovernanceDirectory, tenantQuota, tenantUsage, adminOpsOverview, adminFixRunsPayload] = await Promise.all([ - supportsOneLinerProfile - ? storyforgeFetch(`/v2/oneliner/profile?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null) - : Promise.resolve(null), - supportsOneLinerSessions - ? storyforgeFetch(`/v2/oneliner/sessions?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })) - : Promise.resolve({ items: [] }), - supportsOneLinerRuns - ? storyforgeFetch(`/v2/oneliner/runs?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })) - : Promise.resolve({ items: [] }), - supportsActionRegistry - ? storyforgeFetch(`/v2/oneliner/action-registry?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })) - : Promise.resolve({ items: [] }), - supportsPlatformAgents - ? storyforgeFetch(`/v2/platform-agents?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })) - : Promise.resolve({ items: [] }), - supportsGovernanceEffective - ? storyforgeFetch(`/v2/oneliner/governance/effective?project_id=${encodeURIComponent(normalizedProjectId)}&platform=${encodeURIComponent(governancePlatform)}`).catch(() => null) - : Promise.resolve(null), - supportsUserGlobalPolicy - ? storyforgeFetch(`/v2/oneliner/governance/user/global?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null) - : Promise.resolve(null), - supportsUserPlatformPolicy - ? storyforgeFetch(`/v2/oneliner/governance/user/platforms/${encodeURIComponent(governancePlatform)}?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null) - : Promise.resolve(null), - supportsUserPolicyAudits - ? storyforgeFetch(`/v2/oneliner/governance/user/audits?project_id=${encodeURIComponent(normalizedProjectId)}&platform=${encodeURIComponent(governancePlatform)}`).catch(() => ({ items: [] })) - : Promise.resolve({ items: [] }), - supportsAdminSystemMainPolicy && isSuperAdmin() + storyforgeFetch(`/v2/oneliner/profile?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null), + storyforgeFetch(`/v2/oneliner/sessions?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })), + storyforgeFetch(`/v2/oneliner/runs?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })), + storyforgeFetch(`/v2/oneliner/action-registry?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })), + storyforgeFetch(`/v2/platform-agents?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })), + storyforgeFetch(`/v2/oneliner/governance/effective?project_id=${encodeURIComponent(normalizedProjectId)}&platform=${encodeURIComponent(governancePlatform)}`).catch(() => null), + storyforgeFetch(`/v2/oneliner/governance/user/global?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null), + storyforgeFetch(`/v2/oneliner/governance/user/platforms/${encodeURIComponent(governancePlatform)}?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null), + storyforgeFetch(`/v2/oneliner/governance/user/audits?project_id=${encodeURIComponent(normalizedProjectId)}&platform=${encodeURIComponent(governancePlatform)}`).catch(() => ({ items: [] })), + isSuperAdmin() ? storyforgeFetch("/v2/admin/oneliner/governance/system/main-agent").catch(() => null) : Promise.resolve(null), - supportsAdminSystemPlatformPolicy && isSuperAdmin() + isSuperAdmin() ? Promise.all(ACTIVE_PLATFORMS.map((item) => storyforgeFetch(`/v2/admin/oneliner/governance/system/platforms/${encodeURIComponent(item.value)}`).catch(() => null) )) : Promise.resolve([]), - supportsAdminGovernanceDirectory && isSuperAdmin() + isSuperAdmin() ? storyforgeFetch("/v2/admin/oneliner/governance/directory").catch(() => ({ items: [] })) : Promise.resolve({ items: [] }), - supportsTenantQuota - ? storyforgeFetch(`/v2/tenant/quota?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null) - : Promise.resolve(null), - supportsTenantUsage - ? storyforgeFetch(`/v2/tenant/usage?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null) - : Promise.resolve(null), - supportsAdminOps && isSuperAdmin() + storyforgeFetch(`/v2/tenant/quota?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null), + storyforgeFetch(`/v2/tenant/usage?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null), + isSuperAdmin() ? storyforgeFetch("/v2/admin/ops/overview").catch(() => null) : Promise.resolve(null), - supportsAdminFixRuns && isSuperAdmin() + isSuperAdmin() ? storyforgeFetch("/v2/admin/ops/fix-runs").catch(() => ({ items: [] })) : Promise.resolve({ items: [] }) ]); @@ -1833,7 +1792,7 @@ async function loadAgentControlSurfaces(projectId = "") { appState.adminSystemMainPolicy = adminSystemMainPolicy; appState.adminSystemPlatformPolicies = safeArray(adminSystemPlatformPolicies); appState.adminGovernanceDirectory = safeArray(adminGovernanceDirectory?.items || adminGovernanceDirectory); - if (isSuperAdmin() && supportsAdminOverridePolicy && appState.adminGovernanceDirectory.length) { + if (isSuperAdmin() && appState.adminGovernanceDirectory.length) { const existingTarget = appState.adminOverrideTarget || {}; const hasExistingProjectTarget = Object.prototype.hasOwnProperty.call(existingTarget, "targetProjectId") || Object.prototype.hasOwnProperty.call(existingTarget, "target_project_id"); @@ -1853,9 +1812,7 @@ async function loadAgentControlSurfaces(projectId = "") { platform: targetPlatform }; appState.adminOverridePolicy = await storyforgeFetch(`/v2/admin/oneliner/governance/overrides?target_user_id=${encodeURIComponent(targetUserId)}&target_project_id=${encodeURIComponent(targetProjectId)}&platform=${encodeURIComponent(targetPlatform)}`).catch(() => null); - appState.adminPolicyAudits = supportsAdminGovernanceAudits - ? safeArray((await storyforgeFetch(`/v2/admin/oneliner/governance/audits?target_user_id=${encodeURIComponent(targetUserId)}&target_project_id=${encodeURIComponent(targetProjectId)}&platform=${encodeURIComponent(targetPlatform)}&include_system=1`).catch(() => ({ items: [] })))?.items || []) - : []; + appState.adminPolicyAudits = safeArray((await storyforgeFetch(`/v2/admin/oneliner/governance/audits?target_user_id=${encodeURIComponent(targetUserId)}&target_project_id=${encodeURIComponent(targetProjectId)}&platform=${encodeURIComponent(targetPlatform)}&include_system=1`).catch(() => ({ items: [] })))?.items || []); } else { appState.adminOverrideTarget = null; appState.adminOverridePolicy = null; @@ -1868,7 +1825,7 @@ async function loadAgentControlSurfaces(projectId = "") { } async function loadOneLinerMessages(sessionId) { - if (!sessionId || !backendSupports("/v2/oneliner/sessions/{session_id}/messages")) { + if (!sessionId) { appState.onelinerMessages = []; return []; } @@ -2498,12 +2455,6 @@ async function bootstrap() { const runtimePlatforms = getRuntimePlatformValues(); const preferredPlatform = getCurrentPlatformValue(); setCurrentPlatform(preferredPlatform); - const supportsIntegrationHealth = backendSupports("/v2/integrations/health"); - const supportsLocalModels = backendSupports("/v2/integrations/local-models"); - const supportsLiveRecorderSources = backendSupports("/v2/live-recorder/sources"); - const supportsLiveRecorderStatus = backendSupports("/v2/live-recorder/status"); - const supportsLiveRecorderFiles = backendSupports("/v2/live-recorder/files"); - const supportsLiveRecorderHealth = backendSupports("/v2/live-recorder/health"); const [contentSources, platformPayloads, reviews, integrationHealth, localModelCatalog, liveRecorderSourcesPayload] = await Promise.all([ storyforgeFetch("/v2/content-sources").catch(() => []), Promise.all(runtimePlatforms.map(async (platform) => { @@ -2539,16 +2490,16 @@ async function bootstrap() { }; })), 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: [] }) + storyforgeFetch("/v2/integrations/health").catch(() => null), + storyforgeFetch("/v2/integrations/local-models").catch(() => null), + storyforgeFetch("/v2/live-recorder/sources").catch(() => ({ items: [] })) ]); const liveRecorderIntegration = integrationHealth?.live_recorder || null; const canLoadLiveRecorderRuntime = Boolean(liveRecorderIntegration?.reachable); const [liveRecorderStatus, liveRecorderFilesPayload, liveRecorderHealth] = await Promise.all([ - supportsLiveRecorderStatus && canLoadLiveRecorderRuntime ? storyforgeFetch("/v2/live-recorder/status").catch(() => null) : Promise.resolve(null), - supportsLiveRecorderFiles && canLoadLiveRecorderRuntime ? storyforgeFetch("/v2/live-recorder/files?limit=16").catch(() => ({ items: [] })) : Promise.resolve({ items: [] }), - supportsLiveRecorderHealth && canLoadLiveRecorderRuntime ? storyforgeFetch("/v2/live-recorder/health").catch(() => null) : Promise.resolve(null) + canLoadLiveRecorderRuntime ? storyforgeFetch("/v2/live-recorder/status").catch(() => null) : Promise.resolve(null), + canLoadLiveRecorderRuntime ? storyforgeFetch("/v2/live-recorder/files?limit=16").catch(() => ({ items: [] })) : Promise.resolve({ items: [] }), + canLoadLiveRecorderRuntime ? storyforgeFetch("/v2/live-recorder/health").catch(() => null) : Promise.resolve(null) ]); const mergedAccounts = safeArray(platformPayloads) .flatMap((entry) => safeArray(entry.accounts)) @@ -7832,7 +7783,7 @@ function getJobRecoverability(job) { }; } if (sourceType === "upload_video") { - if (backendSupports("/v2/explore/jobs/{job_id}/retry") && uploadedPath) { + if (uploadedPath) { return { ...base, state: "recoverable", @@ -8080,7 +8031,7 @@ async function recoverJobAction(jobId, options = {}) { if (!recovery.recoverable) { throw new Error(recovery.reason || "当前任务暂不支持恢复"); } - if (backendSupports("/v2/explore/jobs/{job_id}/retry")) { + try { const retried = await storyforgeFetch(`/v2/explore/jobs/${encodeURIComponent(job.id)}/retry`, { method: "POST" }); @@ -8111,6 +8062,13 @@ async function recoverJobAction(jobId, options = {}) { reason: "通过任务重试接口恢复" } }; + } catch (error) { + if (!isMissingBackendCapability(error)) { + throw error; + } + if (job?.source_type === "upload_video") { + throw new Error("当前实例没有开放上传任务重试接口,暂时无法自动恢复已上传素材。"); + } } const request = getJobRecoveryRequest(job); const payload = await storyforgeFetch(request.endpoint, { @@ -10748,7 +10706,7 @@ document.addEventListener("click", async (event) => { await loadAgentControlSurfaces(appState.selectedProjectId || ""); if (appState.selectedOnelinerSessionId) { await loadOneLinerMessages(appState.selectedOnelinerSessionId); - } else if (backendSupports("/v2/oneliner/sessions")) { + } else { await ensureOneLinerSession(); } } diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 57c0dd4..46bfbe4 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -293,6 +293,10 @@ test("tracking refresh and top-video analysis flows expose async feedback inside const tracking = extractBetween(APP, "function renderTrackingScreen()", "function renderAutomationScreen()"); const discovery = extractBetween(APP, "function renderDiscoveryOverviewSection(", "function renderDiscoveryRelationsSection("); const accountWorkspaceLoad = extractBetween(APP, "async function loadPlatformAccount(", "async function bootstrap()"); + const bootstrapSource = extractBetween(APP, "async function bootstrap()", "async function markTrackingDigestRead()"); + const oneLinerHydrate = extractBetween(APP, "async function hydrateSelectedOneLinerRun()", "async function loadAgentControlSurfaces("); + const controlSurfaces = extractBetween(APP, "async function loadAgentControlSurfaces(", "async function loadOneLinerMessages("); + const oneLinerMessages = extractBetween(APP, "async function loadOneLinerMessages(", "async function ensureOneLinerSession("); 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("); @@ -300,6 +304,9 @@ test("tracking refresh and top-video analysis flows expose async feedback inside const skillReview = extractBetween(APP, "function openPlatformSkillReviewAction(", "function openPlatformSkillRollbackAction("); const agentDetail = extractBetween(APP, "async function openPlatformAgentDetailAction(", "function openPlatformSkillReviewAction("); const topVideoAction = extractBetween(APP, "function openAnalyzeTopVideosAction()", "function openSimilaritySearchAction()"); + const jobRecoverability = extractBetween(APP, "function getJobRecoverability(job) {", "function getJobRecoveryRequest(job) {"); + const recoveryAction = extractBetween(APP, "async function recoverJobAction(", "function getRecoverableFailedJobs()"); + const clickActions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {"); assert.match(APP, /function summarizeTrackingRefreshPayload\(/); assert.match(APP, /function rememberTrackingRefreshNotice\(/); @@ -313,6 +320,12 @@ test("tracking refresh and top-video analysis flows expose async feedback inside assert.match(discovery, /这批结果已经回流到当前账号页/); assert.doesNotMatch(trackingActions, /当前后端暂不支持.*跟踪已读游标|当前后端暂不支持.*批量跟踪同步|当前后端暂不支持.*单账号跟踪同步/s); assert.doesNotMatch(accountWorkspaceLoad, /supportsAccountVideos|supportsAccountSnapshots|supportsCreatorFields|supportsAnalysisReports/); + assert.ok(!bootstrapSource.includes('backendSupports("/v2/integrations/health")')); + assert.ok(!bootstrapSource.includes('backendSupports("/v2/live-recorder/sources")')); + assert.ok(!oneLinerHydrate.includes('backendSupports("/v2/oneliner/runs/{run_id}")')); + assert.ok(!oneLinerMessages.includes('backendSupports("/v2/oneliner/sessions/{session_id}/messages")')); + assert.ok(!controlSurfaces.includes('backendSupports("/v2/oneliner/profile")')); + assert.ok(!controlSurfaces.includes('backendSupports("/v2/platform-agents")')); assert.doesNotMatch(oneLinerSession, /当前后端还没有接入 OneLiner 会话接口/); assert.doesNotMatch(oneLinerRun, /当前后端还没有接入主 Agent 运行层/); assert.doesNotMatch(oneLinerAction, /当前后端还没有接入 OneLiner 动作执行器/); @@ -320,6 +333,9 @@ test("tracking refresh and top-video analysis flows expose async feedback inside assert.ok(!agentDetail.includes('backendSupports("/v2/platform-agents/{platform}/skills/{skill_id}/versions")')); assert.doesNotMatch(topVideoAction, /当前后端暂不支持.*高分作品批量分析/s); assert.match(topVideoAction, /当前实例未提供/); + assert.ok(!jobRecoverability.includes('backendSupports("/v2/explore/jobs/{job_id}/retry") && uploadedPath')); + assert.ok(!recoveryAction.includes('if (backendSupports("/v2/explore/jobs/{job_id}/retry"))')); + assert.ok(!clickActions.includes('else if (backendSupports("/v2/oneliner/sessions"))')); }); test("discovery and production screens expose compact mobile flow summaries", () => { @@ -498,9 +514,10 @@ test("bootstrap only loads live recorder runtime endpoints when the integration const bootstrap = extractBetween(APP, "async function bootstrap()", "async function markTrackingDigestRead()"); assert.match(bootstrap, /const liveRecorderIntegration = integrationHealth\?\.live_recorder \|\| null/); assert.match(bootstrap, /const canLoadLiveRecorderRuntime = Boolean\(liveRecorderIntegration\?\.reachable\)/); - assert.match(bootstrap, /supportsLiveRecorderStatus && canLoadLiveRecorderRuntime/); - assert.match(bootstrap, /supportsLiveRecorderFiles && canLoadLiveRecorderRuntime/); - assert.match(bootstrap, /supportsLiveRecorderHealth && canLoadLiveRecorderRuntime/); + assert.doesNotMatch(bootstrap, /supportsLiveRecorderStatus|supportsLiveRecorderFiles|supportsLiveRecorderHealth/); + assert.match(bootstrap, /canLoadLiveRecorderRuntime \? storyforgeFetch\("\/v2\/live-recorder\/status"\)/); + assert.match(bootstrap, /canLoadLiveRecorderRuntime \? storyforgeFetch\("\/v2\/live-recorder\/files\?limit=16"\)/); + assert.match(bootstrap, /canLoadLiveRecorderRuntime \? storyforgeFetch\("\/v2\/live-recorder\/health"\)/); }); test("oneliner submit failures stay inside the app instead of using a browser alert", () => {