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"))'));
});