diff --git a/CHANGELOG.md b/CHANGELOG.md
index 054e1a7..ebf38be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -457,3 +457,4 @@
- 主 Agent 落到 `找对标 / Agent / 生产中心 / 发布与复盘` 后,快捷动作里原先的 `高分分析 / 新建 Agent / 写复盘 / 做 AI 视频 / 做实拍剪辑` 已优先改成 direct-execute。
- 这些动作现在直接调用 `OneLiner` 执行器并按真实结果继续落到对象详情、Agent 编辑页、复盘页或任务详情,而不是先打开旧表单。
+- `review-draft` 现在支持显式 `source_job_id`,所以从任务详情、复盘页和最近完成任务入口点“写复盘”,会围绕指定任务直接生成草稿,不再总是退回“最近一条任务”。
diff --git a/collector-service/app/oneliner_features.py b/collector-service/app/oneliner_features.py
index 59ad89a..0e0c1ce 100644
--- a/collector-service/app/oneliner_features.py
+++ b/collector-service/app/oneliner_features.py
@@ -5734,7 +5734,18 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
}
async def _run_review_draft() -> dict[str, Any]:
- latest_job = _latest_project_job(account, project_id=project["id"])
+ requested_job_id = str(
+ requested_payload.get("source_job_id")
+ or requested_payload.get("sourceJobId")
+ or requested_payload.get("job_id")
+ or requested_payload.get("jobId")
+ or ""
+ ).strip()
+ latest_job = _load_owned_job(account, requested_job_id) if requested_job_id else None
+ if latest_job and str(latest_job.get("project_id") or "") != str(project["id"]):
+ latest_job = None
+ if not latest_job:
+ latest_job = _latest_project_job(account, project_id=project["id"])
if not latest_job:
raise HTTPException(status_code=404, detail="No completed job available for review draft")
existing = legacy.db.fetch_one(
diff --git a/tests/test_main_agent_governance.py b/tests/test_main_agent_governance.py
index b1f8967..e923a17 100644
--- a/tests/test_main_agent_governance.py
+++ b/tests/test_main_agent_governance.py
@@ -969,6 +969,23 @@ class MainAgentGovernanceTests(unittest.TestCase):
self.assertEqual(review_payload["recommended_action"]["job_id"], "job_review_source")
self.assertTrue(review_payload["recommended_action"]["review_id"])
+ explicit_review_response = self.client.post(
+ "/v2/oneliner/actions/execute",
+ headers=self.ctx["member_headers"],
+ json={
+ "action_key": "review-draft",
+ "project_id": self.ctx["project_id"],
+ "platform": "douyin",
+ "payload": {
+ "source_job_id": "job_review_source",
+ },
+ },
+ )
+ self.assertEqual(explicit_review_response.status_code, 200, explicit_review_response.text)
+ explicit_review_payload = explicit_review_response.json()
+ self.assertEqual(explicit_review_payload["recommended_action"]["action"], "open-review-edit")
+ self.assertEqual(explicit_review_payload["recommended_action"]["job_id"], "job_review_source")
+
self_check_response = self.client.post(
"/v2/oneliner/actions/execute",
headers=self.ctx["member_headers"],
diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js
index 18e3b80..bf8406d 100644
--- a/web/storyforge-web-v4/assets/app.js
+++ b/web/storyforge-web-v4/assets/app.js
@@ -5693,7 +5693,7 @@ function renderDashboardScreen() {
return screenShell(
"项目总台",
"先做最能推进当前项目的一步,再按需看概览。",
- `${button("新建项目", "create-project")} ${button("导入主页", "open-import-homepage")} ${button("创建 Agent", "open-create-assistant", "primary")}`,
+ `${button("新建项目", "create-project")} ${button("导入主页", "open-import-homepage")} ${button("创建 Agent", "direct-create-assistant", "primary")}`,
dashboardHomeRenderer?.renderDashboardHome
? `
${dashboardHomeRenderer.renderDashboardHome(homeModel, { escapeHtml })}
`
: renderEmptyState("首页模块未加载", "请刷新页面后重试。")
@@ -6698,7 +6698,7 @@ function renderPlaybookScreen() {
return screenShell(
"Agent",
"这里接真实 Agent 列表,当前已经支持切换和编辑 Agent。",
- `${button("配置 OneLiner", "open-oneliner-profile")} ${button("看配置历史", "open-oneliner-profile-history", "secondary")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: playbookHandoffAttrs })} ${button("设主模型", "open-preferred-model")} ${button("新建 Agent", "open-create-assistant")} ${button("去生产", "goto-production", "primary")}`,
+ `${button("配置 OneLiner", "open-oneliner-profile")} ${button("看配置历史", "open-oneliner-profile-history", "secondary")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: playbookHandoffAttrs })} ${button("设主模型", "open-preferred-model")} ${button("新建 Agent", "direct-create-assistant")} ${button("去生产", "goto-production", "primary")}`,
`
${renderMainAgentLandingNotice("playbook")}
@@ -6736,7 +6736,7 @@ function renderPlaybookScreen() {
? `${actionTag("看平台 Agent", "select-page-tab", `data-page-tab-key="playbookDetailTab" data-page-tab-value="platform_agents"`)} ${actionTag("交给主 Agent", "handoff-to-main-agent", playbookHandoffAttrs)}`
: activeTab === "models"
? `${actionTag("设主模型", "open-preferred-model")} ${actionTag("回工作区", "select-page-tab", `data-page-tab-key="playbookDetailTab" data-page-tab-value="workspace"`)}`
- : `${actionTag("配置 OneLiner", "open-oneliner-profile")} ${actionTag("看配置历史", "open-oneliner-profile-history")} ${actionTag(currentAssistant ? "去生产" : "新建 Agent", currentAssistant ? "goto-production" : "open-create-assistant")} ${actionTag("交给主 Agent", "handoff-to-main-agent", playbookHandoffAttrs)}`
+ : `${actionTag("配置 OneLiner", "open-oneliner-profile")} ${actionTag("看配置历史", "open-oneliner-profile-history")} ${actionTag(currentAssistant ? "去生产" : "新建 Agent", currentAssistant ? "goto-production" : "direct-create-assistant")} ${actionTag("交给主 Agent", "handoff-to-main-agent", playbookHandoffAttrs)}`
}
@@ -7296,7 +7296,7 @@ function renderReviewScreen() {
return screenShell(
"发布与复盘",
"先看已保存复盘,再把完成任务转成结构化复盘。",
- `${button("写复盘", "open-create-review")} ${button("刷新", "refresh-data")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: reviewHandoffAttrs })} ${button("去生产", "goto-production", "primary")}`,
+ `${button("写复盘", "direct-review-draft")} ${button("刷新", "refresh-data")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: reviewHandoffAttrs })} ${button("去生产", "goto-production", "primary")}`,
`
${renderMainAgentLandingNotice("review")}
@@ -7313,7 +7313,7 @@ function renderReviewScreen() {
${escapeHtml(reviewTaskTitle)}
${escapeHtml(reviewTaskSummary)}
- ${actionTag(completed.length ? "写复盘" : reviews.length ? "看复盘" : "去生产", completed.length ? "open-review-from-job" : reviews.length ? "goto-review" : "goto-production", completed[0]?.id ? `data-job-id="${escapeHtml(completed[0].id)}"` : "")}
+ ${actionTag(completed.length ? "写复盘" : reviews.length ? "看复盘" : "去生产", completed.length ? "direct-review-draft" : reviews.length ? "goto-review" : "goto-production", completed[0]?.id ? `data-job-id="${escapeHtml(completed[0].id)}"` : "")}
${actionTag("交给主 Agent", "handoff-to-main-agent", reviewHandoffAttrs)}
${escapeHtml(`已保存 ${formatNumber(reviews.length)} 条`)}
${escapeHtml(`已发布 ${formatNumber(publishedReviewCount)} 条`)}
@@ -7333,7 +7333,7 @@ function renderReviewScreen() {
: "当前还没有可用复盘,先回到生产中心跑出一条完成链路。"
)}
- ${actionTag(completed.length ? "写复盘" : "去生产", completed.length ? "open-review-from-job" : "goto-production", completed[0]?.id ? `data-job-id="${escapeHtml(completed[0].id)}"` : "")}
+ ${actionTag(completed.length ? "写复盘" : "去生产", completed.length ? "direct-review-draft" : "goto-production", completed[0]?.id ? `data-job-id="${escapeHtml(completed[0].id)}"` : "")}
${actionTag("刷新", "refresh-data")}
${actionTag("交给主 Agent", "handoff-to-main-agent", reviewHandoffAttrs)}
@@ -7375,7 +7375,7 @@ function renderReviewScreen() {
已完成
${escapeHtml(job.line_type || "analysis")}
- ${actionTag("写复盘", "open-review-from-job", `data-job-id="${escapeHtml(job.id)}"`)}
+ ${actionTag("写复盘", "direct-review-draft", `data-job-id="${escapeHtml(job.id)}"`)}
${canDeriveAiVideo(job) ? renderPipelineJobTag("aiVideo", job, "做 AI 视频") : ""}
${canDeriveRealCut(job) ? renderPipelineJobTag("realCut", job, "做实拍剪辑") : ""}
${actionTag("看详情", "open-job-detail", `data-job-id="${escapeHtml(job.id)}"`)}
@@ -8968,7 +8968,7 @@ function renderLastJobDetailCard() {
${escapeHtml(detail.job.line_type || "-")}
${detail.job.status === "failed" ? `${escapeHtml(recovery.reason)}` : ""}
- ${detail.job.status === "completed" ? actionTag("写复盘", "open-review-from-job", `data-job-id="${escapeHtml(detail.job.id)}"`) : ""}
+ ${detail.job.status === "completed" ? actionTag("写复盘", "direct-review-draft", `data-job-id="${escapeHtml(detail.job.id)}"`) : ""}
${detail.job.status === "failed" ? actionTag(
recovery.actionLabel,
recovery.recoverable ? "recover-job" : recovery.actionKey,
@@ -12361,7 +12361,8 @@ document.addEventListener("click", async (event) => {
if (name === "direct-create-ai-video") {
await runDirectWorkbenchAction("create-ai-video", {
busyLabel: "正在创建 AI 视频任务...",
- errorTitle: "创建 AI 视频任务失败"
+ errorTitle: "创建 AI 视频任务失败",
+ payload: action.dataset.jobId ? { source_job_id: action.dataset.jobId } : {}
});
return;
}
@@ -12372,7 +12373,8 @@ document.addEventListener("click", async (event) => {
if (name === "direct-create-real-cut") {
await runDirectWorkbenchAction("create-real-cut", {
busyLabel: "正在创建实拍剪辑任务...",
- errorTitle: "创建实拍剪辑任务失败"
+ errorTitle: "创建实拍剪辑任务失败",
+ payload: action.dataset.jobId ? { source_job_id: action.dataset.jobId } : {}
});
return;
}
@@ -12383,7 +12385,8 @@ document.addEventListener("click", async (event) => {
if (name === "direct-review-draft") {
await runDirectWorkbenchAction("review-draft", {
busyLabel: "正在生成复盘草稿...",
- errorTitle: "生成复盘草稿失败"
+ errorTitle: "生成复盘草稿失败",
+ payload: action.dataset.jobId ? { source_job_id: action.dataset.jobId } : {}
});
return;
}
diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
index 5e1234a..87fa8c9 100644
--- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs
+++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
@@ -998,6 +998,14 @@ test("main agent landing quick actions prefer direct execute flows where executo
assert.match(APP, /async function runDirectWorkbenchAction\(executorKey, options = \{\}\)/);
});
+test("playbook and review high-frequency actions now reuse direct execute handlers", () => {
+ const playbook = extractBetween(APP, "function renderPlaybookScreen()", "function renderProductionScreen()");
+ const review = extractBetween(APP, "function renderReviewScreen()", "function renderStrategyScreen()");
+ assert.match(playbook, /direct-create-assistant/);
+ assert.match(review, /direct-review-draft/);
+ assert.match(APP, /payload: action\.dataset\.jobId \? \{ source_job_id: action\.dataset\.jobId \} : \{\}/);
+});
+
test("main agent landing notices expose a compact mobile follow-up strip", () => {
const landing = extractBetween(APP, "function renderMainAgentLandingNotice(screenKey)", "function renderEmptyState(title, description)");
assert.match(landing, /mobile-only compact-summary-row/);