diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aaf15e..97a8cea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ ## 2026-04-04 +### OneLiner 直接执行结果补齐精确落点 + +- OneLiner 直接执行动作现在统一返回结构化 `recommended_action`,不再只有“执行完成”说明块。 +- 这次补通的重点包括: + - 平台自检会直接指向对应 `平台 Agent` 详情 + - 复盘草稿会直接打开对应复盘项 + - 导入主页和高分分析会直接回到 `找对标` + - AI 视频 / 实拍剪辑会直接落到任务详情 + - 存储状态 / 录制状态 / 运维扫描会回到最合适的业务或治理页 +- 前端新增统一的 `buildRecommendedActionAttrs(...)`,把 `job_id / review_id / platform / source_id` 这类上下文一起带进最近动作卡和执行结果卡,后续新增直接动作时不用再重复拼接跳转参数。 +- 后端回归新增了 `review-draft / platform-self-check / generate-copy` 三类真实动作的推荐落点断言;前端回归则锁住了结果卡和最近动作卡必须使用统一的推荐动作属性映射。 +- 这轮还顺手修掉了一个真实 bug:保存录制源时,usage 记账错误地读取了 `binding["id"]`,现在已改成兼容 `binding_id / id`,不会再因为键名差异导致录制源创建链路直接报错。 + ### 主 Agent 消息卡补齐配置追溯与主动作执行上下文 - OneLiner 助手消息卡里的 `主配置历史 / 平台配置历史` 现在终于拿到真实 `version_id`,不再出现“入口在,但打开后只能停在列表顶部”的半截体验。 diff --git a/collector-service/app/core_main.py b/collector-service/app/core_main.py index 33dccce..7b484fb 100644 --- a/collector-service/app/core_main.py +++ b/collector-service/app/core_main.py @@ -3398,7 +3398,7 @@ def create_live_recorder_source( project_id=project["id"], category="live_recorder", reference_type="live_recorder_binding", - reference_id=binding["id"], + reference_id=binding.get("binding_id") or binding.get("id") or "", details={"source_url": request.source_url, "platform": request.platform or infer_platform_from_url(request.source_url)}, ) sync_result = sync_live_recorder_remote_config() diff --git a/collector-service/app/oneliner_features.py b/collector-service/app/oneliner_features.py index 3e3a728..230579d 100644 --- a/collector-service/app/oneliner_features.py +++ b/collector-service/app/oneliner_features.py @@ -5109,6 +5109,29 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: latest_user_message = _last_user_message_text(request.session_id, account["id"]) if request.session_id else "" requested_payload = request.payload or {} + def _recommended_action( + action: str, + *, + label: str, + summary: str, + screen: str = "", + **extra: Any, + ) -> dict[str, Any]: + payload = { + "action": action, + "label": label, + "summary": summary, + "screen": screen, + } + for key, value in extra.items(): + if value is None: + continue + text = str(value).strip() if isinstance(value, str) else value + if text == "": + continue + payload[key] = value + return payload + async def _run_platform_self_check() -> dict[str, Any]: if not normalized_platform: raise HTTPException(status_code=400, detail="Platform is required for self-check") @@ -5123,6 +5146,13 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "title": f"{payload['platform_label']} Agent 自检", "summary": f"平台自检得分 {payload['score']},当前状态:{payload['readiness_label']}。", "payload": payload, + "recommended_action": _recommended_action( + "open-platform-agent-detail", + label="查看平台 Agent", + summary=f"继续查看 {payload['platform_label']} Agent 当前状态、自检建议和最近执行。", + screen="playbook", + platform=normalized_platform, + ), } async def _run_storage_status() -> dict[str, Any]: @@ -5135,6 +5165,12 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: f"downloads 占用 {tenant_usage.get('project_downloads', {}).get('human_size', '0B')}。" ), "payload": payload, + "recommended_action": _recommended_action( + "goto-automation", + label="去自动流程", + summary="继续查看存储、依赖和自动流程健康状态。", + screen="automation", + ), } async def _run_live_recorder_status() -> dict[str, Any]: @@ -5143,6 +5179,12 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "title": "直播录制状态", "summary": f"当前共 {len(payload.get('items', []))} 条录制源,最近文件 {len(payload.get('files', []))} 个。", "payload": payload, + "recommended_action": _recommended_action( + "open-live-recorder", + label="打开录制维护", + summary="继续查看录制源、文件和 NAS 录制状态。", + screen="production", + ), } async def _run_ops_scan() -> dict[str, Any]: @@ -5152,6 +5194,12 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "title": "运维 Agent 故障扫描", "summary": f"本轮共归集 {payload.get('count', 0)} 条事件。", "payload": payload, + "recommended_action": _recommended_action( + "goto-automation", + label="去自动流程", + summary="继续检查运维扫描结果、依赖健康和自动流程状态。", + screen="automation", + ), } async def _run_generate_copy() -> dict[str, Any]: @@ -5176,6 +5224,13 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "title": "OneLiner 已生成文案", "summary": f"已用 {assistant.get('name') or '默认 Agent'} 生成一版可发布文案。", "payload": payload, + "recommended_action": _recommended_action( + "open-generate-copy", + label="继续调文案", + summary="继续修改提示、受众和补充要求,快速迭代这版文案。", + screen="playbook", + platform=normalized_platform or "douyin", + ), } async def _run_review_draft() -> dict[str, Any]: @@ -5192,6 +5247,14 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "title": "OneLiner 找到已有复盘", "summary": f"任务「{latest_job.get('title') or latest_job['id']}」已经有复盘记录。", "payload": payload, + "recommended_action": _recommended_action( + "open-review-edit", + label="打开复盘", + summary="继续完善这条复盘记录的 verdict、亮点和下一步。", + screen="review", + review_id=payload.get("id", ""), + job_id=payload.get("source_job_id", ""), + ), } assistant = _resolve_execution_assistant(account, project_id=project["id"], platform=normalized_platform) result = latest_job.get("result_json") or "{}" @@ -5218,6 +5281,14 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "title": "OneLiner 已生成复盘草稿", "summary": f"已基于最近完成任务「{latest_job.get('title') or latest_job['id']}」生成复盘草稿。", "payload": payload, + "recommended_action": _recommended_action( + "open-review-edit", + label="打开复盘", + summary="继续完善这条复盘草稿,并确认 verdict 和下一步动作。", + screen="review", + review_id=payload.get("id", ""), + job_id=payload.get("source_job_id", ""), + ), } async def _run_import_homepage() -> dict[str, Any]: @@ -5269,6 +5340,14 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "source_url": source_url, "existing_source_id": (existing_source or {}).get("id", ""), }, + "recommended_action": _recommended_action( + "goto-discovery", + label="去找对标", + summary=f"继续查看 {legacy.platform_label(inferred_platform)} 主页导入后的账号分析和候选对标。", + screen="discovery", + platform=inferred_platform, + job_id=sync_job.get("id", ""), + ), } async def _run_analyze_top_videos() -> dict[str, Any]: @@ -5364,6 +5443,13 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "items": items, "memory": memory, }, + "recommended_action": _recommended_action( + "goto-discovery", + label="去找对标", + summary=f"继续查看 {legacy.platform_label(normalized_platform)} 高分作品拆解和相似账号。", + screen="discovery", + platform=normalized_platform, + ), } async def _run_create_ai_video() -> dict[str, Any]: @@ -5407,6 +5493,13 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "source_job": legacy.job_payload(source_job), "brief": brief, }, + "recommended_action": _recommended_action( + "open-job-detail", + label="看任务详情", + summary="继续查看这条 AI 视频任务的执行进度和后续动作。", + screen="production", + job_id=job.get("id", ""), + ), } async def _run_create_real_cut() -> dict[str, Any]: @@ -5439,6 +5532,13 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "job": job, "source_job": legacy.job_payload(source_job), }, + "recommended_action": _recommended_action( + "open-job-detail", + label="看任务详情", + summary="继续查看这条实拍剪辑任务的执行进度和恢复动作。", + screen="production", + job_id=job.get("id", ""), + ), } async def _run_save_live_recorder_source() -> dict[str, Any]: @@ -5484,6 +5584,13 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "platform": recorder_platform, "source_url": source_url, }, + "recommended_action": _recommended_action( + "edit-live-recorder-source", + label="继续录制维护", + summary="继续查看这条录制源的启停状态、项目归属和录制文件。", + screen="production", + source_id=(saved or {}).get("id", ""), + ), } executors = { diff --git a/tests/test_main_agent_governance.py b/tests/test_main_agent_governance.py index 33bb7e2..cac8c40 100644 --- a/tests/test_main_agent_governance.py +++ b/tests/test_main_agent_governance.py @@ -51,6 +51,9 @@ class MainAgentGovernanceTests(unittest.TestCase): tables = [ "job_events", "jobs", + "publish_reviews", + "live_recorder_bindings", + "live_recorder_sources", "agent_run_events", "agent_runs", "agent_policy_audit_logs", @@ -61,6 +64,7 @@ class MainAgentGovernanceTests(unittest.TestCase): "agent_skills", "agent_memories", "platform_agent_profiles", + "oneliner_action_definitions", "tenant_usage_ledger", "tenant_quota_profiles", "admin_ops_audit_logs", @@ -143,6 +147,74 @@ class MainAgentGovernanceTests(unittest.TestCase): "member_headers": {"Authorization": f"Bearer {member_token}"}, } + def _insert_completed_job( + self, + *, + job_id: str = "job_completed", + title: str = "Completed Job", + result_json: str = '{"summary":"done"}', + ) -> str: + now = self.db_module.utc_now() + knowledge_base_id = "kb_member_default" + existing_kb = self.core.db.fetch_one("SELECT id FROM knowledge_bases WHERE id = ?", (knowledge_base_id,)) + if not existing_kb: + self.core.db.execute( + """ + INSERT INTO knowledge_bases (id, user_id, project_id, name, description, sync_status, created_at, updated_at) + VALUES (?, ?, ?, 'Default KB', '', 'ready', ?, ?) + """, + (knowledge_base_id, self.ctx["member_id"], self.ctx["project_id"], now, now), + ) + self.core.db.execute( + """ + INSERT INTO jobs ( + id, user_id, project_id, parent_job_id, assistant_id, knowledge_base_id, content_source_id, + source_type, line_type, workflow_key, orchestrator, provider_name, provider_task_id, + source_url, title, language, status, transcript_text, style_summary, upload_status, + error, artifacts_json, result_json, analysis_model_profile_id, created_at, updated_at + ) VALUES (?, ?, ?, '', NULL, ?, NULL, ?, ?, ?, 'n8n', 'collector', '', '', ?, 'auto', 'completed', '', '', 'completed', '', '{}', ?, '', ?, ?) + """, + ( + job_id, + self.ctx["member_id"], + self.ctx["project_id"], + knowledge_base_id, + "text", + "analysis", + "analysis_pipeline", + title, + result_json, + now, + now, + ), + ) + return job_id + + def _insert_assistant( + self, + *, + assistant_id: str = "asst_member_default", + name: str = "Default Assistant", + ) -> str: + now = self.db_module.utc_now() + self.core.db.execute( + """ + INSERT INTO assistants ( + id, user_id, project_id, name, description, system_prompt, generation_goal, config_json, + model_profile_id, created_at, updated_at + ) VALUES (?, ?, ?, ?, '', '', '', '{}', '', ?, ?) + """, + ( + assistant_id, + self.ctx["member_id"], + self.ctx["project_id"], + name, + now, + now, + ), + ) + return assistant_id + def _seed_approved_member_without_project(self) -> dict[str, Any]: now = self.db_module.utc_now() admin_id = "acct_admin" @@ -703,6 +775,61 @@ class MainAgentGovernanceTests(unittest.TestCase): self.assertIn("categories", usage_payload) self.assertIn("storage_bytes", usage_payload) + def test_direct_oneliner_actions_return_structured_followup_targets(self) -> None: + self._insert_completed_job(job_id="job_review_source", title="Review Source Job") + self._insert_assistant() + + 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": {}, + }, + ) + self.assertEqual(review_response.status_code, 200, review_response.text) + review_payload = review_response.json() + self.assertEqual(review_payload["recommended_action"]["action"], "open-review-edit") + self.assertEqual(review_payload["recommended_action"]["screen"], "review") + self.assertEqual(review_payload["recommended_action"]["job_id"], "job_review_source") + self.assertTrue(review_payload["recommended_action"]["review_id"]) + + self_check_response = self.client.post( + "/v2/oneliner/actions/execute", + headers=self.ctx["member_headers"], + json={ + "action_key": "platform-self-check", + "project_id": self.ctx["project_id"], + "platform": "douyin", + "payload": {}, + }, + ) + self.assertEqual(self_check_response.status_code, 200, self_check_response.text) + self_check_payload = self_check_response.json() + self.assertEqual(self_check_payload["recommended_action"]["action"], "open-platform-agent-detail") + self.assertEqual(self_check_payload["recommended_action"]["screen"], "playbook") + self.assertEqual(self_check_payload["recommended_action"]["platform"], "douyin") + + copy_response = self.client.post( + "/v2/oneliner/actions/execute", + headers=self.ctx["member_headers"], + json={ + "action_key": "generate-copy", + "project_id": self.ctx["project_id"], + "platform": "douyin", + "payload": { + "brief": "给我一版成交向短视频文案", + }, + }, + ) + self.assertEqual(copy_response.status_code, 200, copy_response.text) + copy_payload = copy_response.json() + self.assertEqual(copy_payload["recommended_action"]["action"], "open-generate-copy") + self.assertEqual(copy_payload["recommended_action"]["screen"], "playbook") + self.assertEqual(copy_payload["recommended_action"]["platform"], "douyin") + def test_platform_agent_routes_are_live(self) -> None: save_profile = self.client.put( "/v2/platform-agents/douyin/profile", diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 4842f8a..3c6da4b 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -1996,6 +1996,9 @@ function renderOneLinerExecutionPayloadHtml(payload) { if (!payload || typeof payload !== "object") { return `
当前执行器没有附带额外数据。
${escapeHtml(item.summary_text || "已完成拆解。")}
- - `).join("")} + `).join("")}