From 2f74b4632447bd299bc7a48205685252b2c16832 Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 5 Apr 2026 03:14:47 +0800 Subject: [PATCH] feat: polish recovery and quota messaging --- CHANGELOG.md | 6 +++++ web/storyforge-web-v4/assets/app.js | 24 +++++++++---------- .../tests/workbench-pages.test.mjs | 6 +++++ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db985e7..85a0ac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -329,3 +329,9 @@ - 切换当前项目后,现在会自动回到 `项目总台` 的首页工作区,并聚焦到 dashboard 主内容,而不是只留在原地刷新。 - 项目切换的移动端 sheet 和桌面项目切换入口都共用这条回跳逻辑,方便切完项目后立刻继续推进当前项目。 - 前端回归新增了 dashboard 工作区锚点和项目切换 refocus 断言,锁住这条落点体验。 + +### 恢复链与额度文案收口 + +- `生产中心` 不再用“后续再补任务创建动作”这类半成品口径,当前页面直接按真实任务、恢复和复盘来表达。 +- 任务恢复链里的失败提示统一成“先补信息 / 需人工处理”,不再弹出“暂不支持自动恢复”这类生硬口径。 +- `额度` 页把“后续再接真实套餐”改成当前就能落地的套餐表达,明确按预算、动作池和项目阶段去配置套餐。 diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 7537eab..ffb09cf 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -1486,7 +1486,7 @@ function renderOneLinerUi() { ${profile?.current_version?.version_no ? `配置 v${escapeHtml(formatNumber(profile.current_version.version_no || 0))}` : ""} ${escapeHtml(formatNumber(safeArray(appState.platformAgents).length))} 个平台 Agent -
${escapeHtml(profile?.long_term_goal || "当前没有设置长期目标。你可以先在这里说目标,后续再逐步产品化。")}
+
${escapeHtml(profile?.long_term_goal || "当前还没有设长期目标。你可以先直接告诉主 Agent 你要推进的方向,它会按当前项目持续收敛执行策略。")}
${layers.map((layer) => `${escapeHtml(policyScopeTagLabel(layer.scope_kind, layer.scope?.platform || effective?.platform || ""))}`).join("") || `还没有策略层`} ${highlights.map((item) => `${escapeHtml(item)}`).join("")} @@ -6954,7 +6954,7 @@ function renderProductionScreen() { : `${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: productionHandoffAttrs })} ${button("去复盘", "goto-review", "primary")} ${button("批量恢复", "batch-recover-jobs", "secondary", { disabledReason: recoverableCount ? "" : "当前没有可恢复的失败任务" })}`; return screenShell( "生产中心", - "这里已经接上真实任务和知识库文档,后续再继续补任务创建动作。", + "这里已经接上真实任务、失败恢复和知识库文档,适合直接推进生产、恢复和复盘。", productionActionsHtml, ` ${renderMainAgentLandingNotice("production")} @@ -7522,7 +7522,7 @@ function renderCreditsScreen() {

动作额度

-

${escapeHtml(quota ? `文案 ${formatNumber(quota.copy_quota || 0)} / AI 视频 ${formatNumber(quota.ai_video_quota || 0)} / 实拍剪辑 ${formatNumber(quota.real_cut_quota || 0)}。` : "当前优先展示文案、封面、视频三类额度池,后续再接真实套餐。")}

+

${escapeHtml(quota ? `文案 ${formatNumber(quota.copy_quota || 0)} / AI 视频 ${formatNumber(quota.ai_video_quota || 0)} / 实拍剪辑 ${formatNumber(quota.real_cut_quota || 0)}。` : "当前先按文案、AI 视频、实拍剪辑三类动作池展示,便于先按项目阶段配置套餐。")}

使用建议

@@ -7545,7 +7545,7 @@ function renderCreditsScreen() {

风险提示

-

${escapeHtml(quota?.storage_over_limit ? "当前存储已超限,后续应优先处理清理或扩容。" : "当前没有明显超限风险,但仍建议补齐真实计费链路。")}

+

${escapeHtml(quota?.storage_over_limit ? "当前存储已超限,优先处理清理或扩容,再继续放量。" : "当前没有明显超限风险,适合把预算、动作池和项目阶段绑定成正式套餐。")}

@@ -8147,7 +8147,7 @@ function getJobRecoverability(job) { ...base, state: "manual", label: "缺少源任务", - reason: "实拍剪辑缺少源任务,暂时无法自动恢复。", + reason: "实拍剪辑缺少源任务,请先补回源任务后再重跑。", recoverable: false, actionLabel: "看源任务", actionKey: "open-job-detail" @@ -8169,7 +8169,7 @@ function getJobRecoverability(job) { ...base, state: "manual", label: "缺少源任务", - reason: "AI 视频缺少源任务,暂时无法自动恢复。", + reason: "AI 视频缺少源任务,请先补回源任务后再重跑。", recoverable: false, actionLabel: "看源任务", actionKey: "open-job-detail" @@ -8191,7 +8191,7 @@ function getJobRecoverability(job) { ...base, state: "manual", label: "缺少主页", - reason: "内容源同步缺少主页地址,暂时无法自动恢复。", + reason: "内容源同步缺少主页地址,请先补回主页后再同步。", recoverable: false, actionLabel: "去导入主页", actionKey: "open-import-homepage" @@ -8216,7 +8216,7 @@ function getJobRecoverability(job) { ...base, state: "manual", label: "缺少输入", - reason: sourceType === "text" ? "缺少原始文本,暂时无法自动恢复。" : "缺少原始视频链接,暂时无法自动恢复。", + reason: sourceType === "text" ? "缺少原始文本,请先补回文本后再重跑。" : "缺少原始视频链接,请先补回链接后再重跑。", recoverable: false, actionLabel: "查看详情", actionKey: "open-job-detail" @@ -8248,7 +8248,7 @@ function getJobRecoverability(job) { function getJobRecoveryRequest(job) { const recovery = getJobRecoverability(job); if (!recovery.recoverable) { - throw new Error(recovery.reason || "当前任务暂不支持自动恢复"); + throw new Error(recovery.reason || "当前任务需要人工处理后再继续"); } const projectId = job?.project_id || appState.selectedProjectId || ""; const assistantId = job?.assistant_id || ""; @@ -8352,7 +8352,7 @@ function getJobRecoveryRequest(job) { reason: "基于源任务重新发起 AI 视频" }; } - throw new Error("当前任务暂不支持自动恢复"); + throw new Error("当前任务需要人工处理后再继续"); } async function recoverJobAction(jobId, options = {}) { @@ -8364,7 +8364,7 @@ async function recoverJobAction(jobId, options = {}) { } const recovery = getJobRecoverability(job); if (!recovery.recoverable) { - throw new Error(recovery.reason || "当前任务暂不支持恢复"); + throw new Error(recovery.reason || "当前任务需要人工处理后再继续"); } try { const retried = await storyforgeFetch(`/v2/explore/jobs/${encodeURIComponent(job.id)}/retry`, { @@ -10662,7 +10662,7 @@ function openRecoverJobAction(jobId) { } const recovery = getJobRecoverability(job); if (!recovery.recoverable) { - presentActionFailure(new Error(recovery.reason || "当前任务暂不支持恢复"), "当前任务暂不可恢复"); + presentActionFailure(new Error(recovery.reason || "当前任务需要人工处理后再继续"), "当前任务需要先补信息或转人工处理"); return; } openActionModal({ diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 9e6671a..1e0a61f 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -295,6 +295,7 @@ test("platform agent surfaces recent execution feedback from main agent runs", ( test("quota and review screens foreground live next-step guidance", () => { const tenantQuota = extractBetween(APP, "function renderTenantQuotaPanel()", "function policyScopeTagLabel("); + const credits = extractBetween(APP, "function renderCreditsScreen()", "function renderSettingsScreen()"); const review = extractBetween(APP, "function renderReviewScreen()", "function renderStrategyScreen()"); const storage = extractBetween(APP, "function renderStorageStatusPanel()", "function renderAutomationScreen()"); @@ -309,6 +310,9 @@ test("quota and review screens foreground live next-step guidance", () => { assert.match(review, /已发布/); assert.doesNotMatch(storage, /后端暂未提供 \/v2\/storage\/status/); assert.match(storage, /当前实例没有返回存储策略时/); + assert.doesNotMatch(credits, /后续再接真实套餐/); + assert.match(credits, /按项目阶段配置套餐/); + assert.match(credits, /预算、动作池和项目阶段绑定成正式套餐/); }); test("tracking refresh and top-video analysis flows expose async feedback inside the workbench", () => { @@ -355,7 +359,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.doesNotMatch(topVideoAction, /当前实例未提供/); + assert.doesNotMatch(jobRecoverability, /暂时无法自动恢复/); assert.ok(!jobRecoverability.includes('backendSupports("/v2/explore/jobs/{job_id}/retry") && uploadedPath')); + assert.doesNotMatch(recoveryAction, /暂不支持自动恢复|暂不支持恢复/); assert.ok(!recoveryAction.includes('if (backendSupports("/v2/explore/jobs/{job_id}/retry"))')); assert.ok(!clickActions.includes('else if (backendSupports("/v2/oneliner/sessions"))')); });