feat: add direct oneliner follow-up actions
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -4,6 +4,19 @@
|
|||||||
|
|
||||||
## 2026-04-04
|
## 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 消息卡补齐配置追溯与主动作执行上下文
|
### 主 Agent 消息卡补齐配置追溯与主动作执行上下文
|
||||||
|
|
||||||
- OneLiner 助手消息卡里的 `主配置历史 / 平台配置历史` 现在终于拿到真实 `version_id`,不再出现“入口在,但打开后只能停在列表顶部”的半截体验。
|
- OneLiner 助手消息卡里的 `主配置历史 / 平台配置历史` 现在终于拿到真实 `version_id`,不再出现“入口在,但打开后只能停在列表顶部”的半截体验。
|
||||||
|
|||||||
@@ -3398,7 +3398,7 @@ def create_live_recorder_source(
|
|||||||
project_id=project["id"],
|
project_id=project["id"],
|
||||||
category="live_recorder",
|
category="live_recorder",
|
||||||
reference_type="live_recorder_binding",
|
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)},
|
details={"source_url": request.source_url, "platform": request.platform or infer_platform_from_url(request.source_url)},
|
||||||
)
|
)
|
||||||
sync_result = sync_live_recorder_remote_config()
|
sync_result = sync_live_recorder_remote_config()
|
||||||
|
|||||||
@@ -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 ""
|
latest_user_message = _last_user_message_text(request.session_id, account["id"]) if request.session_id else ""
|
||||||
requested_payload = request.payload or {}
|
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]:
|
async def _run_platform_self_check() -> dict[str, Any]:
|
||||||
if not normalized_platform:
|
if not normalized_platform:
|
||||||
raise HTTPException(status_code=400, detail="Platform is required for self-check")
|
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 自检",
|
"title": f"{payload['platform_label']} Agent 自检",
|
||||||
"summary": f"平台自检得分 {payload['score']},当前状态:{payload['readiness_label']}。",
|
"summary": f"平台自检得分 {payload['score']},当前状态:{payload['readiness_label']}。",
|
||||||
"payload": payload,
|
"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]:
|
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')}。"
|
f"downloads 占用 {tenant_usage.get('project_downloads', {}).get('human_size', '0B')}。"
|
||||||
),
|
),
|
||||||
"payload": payload,
|
"payload": payload,
|
||||||
|
"recommended_action": _recommended_action(
|
||||||
|
"goto-automation",
|
||||||
|
label="去自动流程",
|
||||||
|
summary="继续查看存储、依赖和自动流程健康状态。",
|
||||||
|
screen="automation",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _run_live_recorder_status() -> dict[str, Any]:
|
async def _run_live_recorder_status() -> dict[str, Any]:
|
||||||
@@ -5143,6 +5179,12 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
|
|||||||
"title": "直播录制状态",
|
"title": "直播录制状态",
|
||||||
"summary": f"当前共 {len(payload.get('items', []))} 条录制源,最近文件 {len(payload.get('files', []))} 个。",
|
"summary": f"当前共 {len(payload.get('items', []))} 条录制源,最近文件 {len(payload.get('files', []))} 个。",
|
||||||
"payload": payload,
|
"payload": payload,
|
||||||
|
"recommended_action": _recommended_action(
|
||||||
|
"open-live-recorder",
|
||||||
|
label="打开录制维护",
|
||||||
|
summary="继续查看录制源、文件和 NAS 录制状态。",
|
||||||
|
screen="production",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _run_ops_scan() -> dict[str, Any]:
|
async def _run_ops_scan() -> dict[str, Any]:
|
||||||
@@ -5152,6 +5194,12 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
|
|||||||
"title": "运维 Agent 故障扫描",
|
"title": "运维 Agent 故障扫描",
|
||||||
"summary": f"本轮共归集 {payload.get('count', 0)} 条事件。",
|
"summary": f"本轮共归集 {payload.get('count', 0)} 条事件。",
|
||||||
"payload": payload,
|
"payload": payload,
|
||||||
|
"recommended_action": _recommended_action(
|
||||||
|
"goto-automation",
|
||||||
|
label="去自动流程",
|
||||||
|
summary="继续检查运维扫描结果、依赖健康和自动流程状态。",
|
||||||
|
screen="automation",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _run_generate_copy() -> dict[str, Any]:
|
async def _run_generate_copy() -> dict[str, Any]:
|
||||||
@@ -5176,6 +5224,13 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
|
|||||||
"title": "OneLiner 已生成文案",
|
"title": "OneLiner 已生成文案",
|
||||||
"summary": f"已用 {assistant.get('name') or '默认 Agent'} 生成一版可发布文案。",
|
"summary": f"已用 {assistant.get('name') or '默认 Agent'} 生成一版可发布文案。",
|
||||||
"payload": payload,
|
"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]:
|
async def _run_review_draft() -> dict[str, Any]:
|
||||||
@@ -5192,6 +5247,14 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
|
|||||||
"title": "OneLiner 找到已有复盘",
|
"title": "OneLiner 找到已有复盘",
|
||||||
"summary": f"任务「{latest_job.get('title') or latest_job['id']}」已经有复盘记录。",
|
"summary": f"任务「{latest_job.get('title') or latest_job['id']}」已经有复盘记录。",
|
||||||
"payload": payload,
|
"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)
|
assistant = _resolve_execution_assistant(account, project_id=project["id"], platform=normalized_platform)
|
||||||
result = latest_job.get("result_json") or "{}"
|
result = latest_job.get("result_json") or "{}"
|
||||||
@@ -5218,6 +5281,14 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
|
|||||||
"title": "OneLiner 已生成复盘草稿",
|
"title": "OneLiner 已生成复盘草稿",
|
||||||
"summary": f"已基于最近完成任务「{latest_job.get('title') or latest_job['id']}」生成复盘草稿。",
|
"summary": f"已基于最近完成任务「{latest_job.get('title') or latest_job['id']}」生成复盘草稿。",
|
||||||
"payload": payload,
|
"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]:
|
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,
|
"source_url": source_url,
|
||||||
"existing_source_id": (existing_source or {}).get("id", ""),
|
"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]:
|
async def _run_analyze_top_videos() -> dict[str, Any]:
|
||||||
@@ -5364,6 +5443,13 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
|
|||||||
"items": items,
|
"items": items,
|
||||||
"memory": memory,
|
"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]:
|
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),
|
"source_job": legacy.job_payload(source_job),
|
||||||
"brief": brief,
|
"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]:
|
async def _run_create_real_cut() -> dict[str, Any]:
|
||||||
@@ -5439,6 +5532,13 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
|
|||||||
"job": job,
|
"job": job,
|
||||||
"source_job": legacy.job_payload(source_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]:
|
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,
|
"platform": recorder_platform,
|
||||||
"source_url": source_url,
|
"source_url": source_url,
|
||||||
},
|
},
|
||||||
|
"recommended_action": _recommended_action(
|
||||||
|
"edit-live-recorder-source",
|
||||||
|
label="继续录制维护",
|
||||||
|
summary="继续查看这条录制源的启停状态、项目归属和录制文件。",
|
||||||
|
screen="production",
|
||||||
|
source_id=(saved or {}).get("id", ""),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
executors = {
|
executors = {
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ class MainAgentGovernanceTests(unittest.TestCase):
|
|||||||
tables = [
|
tables = [
|
||||||
"job_events",
|
"job_events",
|
||||||
"jobs",
|
"jobs",
|
||||||
|
"publish_reviews",
|
||||||
|
"live_recorder_bindings",
|
||||||
|
"live_recorder_sources",
|
||||||
"agent_run_events",
|
"agent_run_events",
|
||||||
"agent_runs",
|
"agent_runs",
|
||||||
"agent_policy_audit_logs",
|
"agent_policy_audit_logs",
|
||||||
@@ -61,6 +64,7 @@ class MainAgentGovernanceTests(unittest.TestCase):
|
|||||||
"agent_skills",
|
"agent_skills",
|
||||||
"agent_memories",
|
"agent_memories",
|
||||||
"platform_agent_profiles",
|
"platform_agent_profiles",
|
||||||
|
"oneliner_action_definitions",
|
||||||
"tenant_usage_ledger",
|
"tenant_usage_ledger",
|
||||||
"tenant_quota_profiles",
|
"tenant_quota_profiles",
|
||||||
"admin_ops_audit_logs",
|
"admin_ops_audit_logs",
|
||||||
@@ -143,6 +147,74 @@ class MainAgentGovernanceTests(unittest.TestCase):
|
|||||||
"member_headers": {"Authorization": f"Bearer {member_token}"},
|
"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]:
|
def _seed_approved_member_without_project(self) -> dict[str, Any]:
|
||||||
now = self.db_module.utc_now()
|
now = self.db_module.utc_now()
|
||||||
admin_id = "acct_admin"
|
admin_id = "acct_admin"
|
||||||
@@ -703,6 +775,61 @@ class MainAgentGovernanceTests(unittest.TestCase):
|
|||||||
self.assertIn("categories", usage_payload)
|
self.assertIn("categories", usage_payload)
|
||||||
self.assertIn("storage_bytes", 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:
|
def test_platform_agent_routes_are_live(self) -> None:
|
||||||
save_profile = self.client.put(
|
save_profile = self.client.put(
|
||||||
"/v2/platform-agents/douyin/profile",
|
"/v2/platform-agents/douyin/profile",
|
||||||
|
|||||||
@@ -1996,6 +1996,9 @@ function renderOneLinerExecutionPayloadHtml(payload) {
|
|||||||
if (!payload || typeof payload !== "object") {
|
if (!payload || typeof payload !== "object") {
|
||||||
return `<div class="task-item compact"><h4>没有返回执行结果</h4><p>当前执行器没有附带额外数据。</p></div>`;
|
return `<div class="task-item compact"><h4>没有返回执行结果</h4><p>当前执行器没有附带额外数据。</p></div>`;
|
||||||
}
|
}
|
||||||
|
const recommendedAction = payload.recommended_action && typeof payload.recommended_action === "object"
|
||||||
|
? payload.recommended_action
|
||||||
|
: null;
|
||||||
if (payload.result_kind === "main_agent_plan") {
|
if (payload.result_kind === "main_agent_plan") {
|
||||||
const landingRunId = String(payload.run_id || "").trim();
|
const landingRunId = String(payload.run_id || "").trim();
|
||||||
const landingScreen = String(payload.recommended_action?.screen || "").trim();
|
const landingScreen = String(payload.recommended_action?.screen || "").trim();
|
||||||
@@ -2106,6 +2109,11 @@ function renderOneLinerExecutionPayloadHtml(payload) {
|
|||||||
if (payload.job) {
|
if (payload.job) {
|
||||||
const job = payload.job || {};
|
const job = payload.job || {};
|
||||||
const sourceJob = payload.source_job || {};
|
const sourceJob = payload.source_job || {};
|
||||||
|
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
|
||||||
|
screen: "production",
|
||||||
|
title: job.title || "任务已创建",
|
||||||
|
summary: recommendedAction?.summary || payload.brief || sourceJob.title || ""
|
||||||
|
});
|
||||||
return `
|
return `
|
||||||
<div class="task-item compact">
|
<div class="task-item compact">
|
||||||
<h4>${escapeHtml(job.title || "任务已创建")}</h4>
|
<h4>${escapeHtml(job.title || "任务已创建")}</h4>
|
||||||
@@ -2113,7 +2121,7 @@ function renderOneLinerExecutionPayloadHtml(payload) {
|
|||||||
<div class="task-meta">
|
<div class="task-meta">
|
||||||
<span class="tag blue">${escapeHtml(job.line_type || job.source_type || "analysis")}</span>
|
<span class="tag blue">${escapeHtml(job.line_type || job.source_type || "analysis")}</span>
|
||||||
<span class="tag ${job.status === "completed" ? "green" : "orange"}">${escapeHtml(job.status || "queued")}</span>
|
<span class="tag ${job.status === "completed" ? "green" : "orange"}">${escapeHtml(job.status || "queued")}</span>
|
||||||
${job.id ? `<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(job.id)}">看任务详情</span>` : ""}
|
${recommendedAction?.action ? actionTag(recommendedAction.label || "看任务详情", recommendedAction.action, recommendedAttrs) : job.id ? `<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(job.id)}">看任务详情</span>` : ""}
|
||||||
<span class="tag clickable-tag" data-action="goto-production">去生产中心</span>
|
<span class="tag clickable-tag" data-action="goto-production">去生产中心</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2123,6 +2131,11 @@ function renderOneLinerExecutionPayloadHtml(payload) {
|
|||||||
const saved = payload.saved || {};
|
const saved = payload.saved || {};
|
||||||
const item = saved.item || {};
|
const item = saved.item || {};
|
||||||
const started = payload.started || {};
|
const started = payload.started || {};
|
||||||
|
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
|
||||||
|
screen: "production",
|
||||||
|
title: item.binding_title || payload.source_url || "录制源已保存",
|
||||||
|
summary: recommendedAction?.summary || item.source_url || payload.source_url || ""
|
||||||
|
});
|
||||||
return `
|
return `
|
||||||
<div class="task-item compact">
|
<div class="task-item compact">
|
||||||
<h4>${escapeHtml(item.binding_title || payload.source_url || "录制源已保存")}</h4>
|
<h4>${escapeHtml(item.binding_title || payload.source_url || "录制源已保存")}</h4>
|
||||||
@@ -2131,12 +2144,17 @@ function renderOneLinerExecutionPayloadHtml(payload) {
|
|||||||
<span class="tag blue">${escapeHtml(platformLabel(payload.platform || item.platform || "kuaishou"))}</span>
|
<span class="tag blue">${escapeHtml(platformLabel(payload.platform || item.platform || "kuaishou"))}</span>
|
||||||
<span class="tag">${escapeHtml(item.quality || "原画")}</span>
|
<span class="tag">${escapeHtml(item.quality || "原画")}</span>
|
||||||
<span class="tag ${started && started.ok === false ? "orange" : "green"}">${escapeHtml(started && started.ok === false ? "启动待重试" : "已同步")}</span>
|
<span class="tag ${started && started.ok === false ? "orange" : "green"}">${escapeHtml(started && started.ok === false ? "启动待重试" : "已同步")}</span>
|
||||||
<span class="tag clickable-tag" data-action="open-live-recorder">打开录制控制</span>
|
${recommendedAction?.action ? actionTag(recommendedAction.label || "打开录制控制", recommendedAction.action, recommendedAttrs) : `<span class="tag clickable-tag" data-action="open-live-recorder">打开录制控制</span>`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (payload.analyzed_count !== undefined && safeArray(payload.items).length) {
|
if (payload.analyzed_count !== undefined && safeArray(payload.items).length) {
|
||||||
|
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
|
||||||
|
screen: "discovery",
|
||||||
|
title: "OneLiner 已分析高分作品",
|
||||||
|
summary: recommendedAction?.summary || ""
|
||||||
|
});
|
||||||
return `
|
return `
|
||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<div class="mini-card"><small>平台</small><strong>${escapeHtml(platformLabel(payload.platform || payload.account?.platform || "douyin"))}</strong></div>
|
<div class="mini-card"><small>平台</small><strong>${escapeHtml(platformLabel(payload.platform || payload.account?.platform || "douyin"))}</strong></div>
|
||||||
@@ -2156,9 +2174,19 @@ function renderOneLinerExecutionPayloadHtml(payload) {
|
|||||||
</div>
|
</div>
|
||||||
`).join("")}
|
`).join("")}
|
||||||
</div>
|
</div>
|
||||||
|
${recommendedAction?.action ? `
|
||||||
|
<div class="task-meta" style="margin-top:12px;">
|
||||||
|
${actionTag(recommendedAction.label || "回到找对标", recommendedAction.action, recommendedAttrs)}
|
||||||
|
</div>
|
||||||
|
` : ""}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (payload.route_checks) {
|
if (payload.route_checks) {
|
||||||
|
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
|
||||||
|
screen: recommendedAction?.screen || "playbook",
|
||||||
|
title: payload.platform_label || payload.platform || "平台自检",
|
||||||
|
summary: recommendedAction?.summary || ""
|
||||||
|
});
|
||||||
return `
|
return `
|
||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<div class="mini-card"><small>平台</small><strong>${escapeHtml(payload.platform_label || payload.platform || "-")}</strong></div>
|
<div class="mini-card"><small>平台</small><strong>${escapeHtml(payload.platform_label || payload.platform || "-")}</strong></div>
|
||||||
@@ -2191,9 +2219,19 @@ function renderOneLinerExecutionPayloadHtml(payload) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${recommendedAction?.action ? `
|
||||||
|
<div class="task-meta" style="margin-top:12px;">
|
||||||
|
${actionTag(recommendedAction.label || "查看平台 Agent", recommendedAction.action, recommendedAttrs)}
|
||||||
|
</div>
|
||||||
|
` : ""}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (payload.strategy && payload.tenant_usage) {
|
if (payload.strategy && payload.tenant_usage) {
|
||||||
|
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
|
||||||
|
screen: "automation",
|
||||||
|
title: "当前存储状态",
|
||||||
|
summary: recommendedAction?.summary || ""
|
||||||
|
});
|
||||||
return `
|
return `
|
||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<div class="mini-card"><small>jobs</small><strong>${escapeHtml(payload.tenant_usage?.project_jobs?.human_size || "0B")}</strong></div>
|
<div class="mini-card"><small>jobs</small><strong>${escapeHtml(payload.tenant_usage?.project_jobs?.human_size || "0B")}</strong></div>
|
||||||
@@ -2201,17 +2239,37 @@ function renderOneLinerExecutionPayloadHtml(payload) {
|
|||||||
<div class="mini-card"><small>模型目录</small><strong>${escapeHtml(payload.strategy?.models?.mode || "-")}</strong></div>
|
<div class="mini-card"><small>模型目录</small><strong>${escapeHtml(payload.strategy?.models?.mode || "-")}</strong></div>
|
||||||
<div class="mini-card"><small>录像</small><strong>${escapeHtml(payload.strategy?.live_recorder?.mode || "-")}</strong></div>
|
<div class="mini-card"><small>录像</small><strong>${escapeHtml(payload.strategy?.live_recorder?.mode || "-")}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
|
${recommendedAction?.action ? `
|
||||||
|
<div class="task-meta" style="margin-top:12px;">
|
||||||
|
${actionTag(recommendedAction.label || "去自动流程", recommendedAction.action, recommendedAttrs)}
|
||||||
|
</div>
|
||||||
|
` : ""}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (payload.items || payload.files) {
|
if (payload.items || payload.files) {
|
||||||
|
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
|
||||||
|
screen: "production",
|
||||||
|
title: "直播录制状态",
|
||||||
|
summary: recommendedAction?.summary || ""
|
||||||
|
});
|
||||||
return `
|
return `
|
||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<div class="mini-card"><small>录制源</small><strong>${escapeHtml(formatNumber(safeArray(payload.items).length))}</strong></div>
|
<div class="mini-card"><small>录制源</small><strong>${escapeHtml(formatNumber(safeArray(payload.items).length))}</strong></div>
|
||||||
<div class="mini-card"><small>最近文件</small><strong>${escapeHtml(formatNumber(safeArray(payload.files).length))}</strong></div>
|
<div class="mini-card"><small>最近文件</small><strong>${escapeHtml(formatNumber(safeArray(payload.files).length))}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
|
${recommendedAction?.action ? `
|
||||||
|
<div class="task-meta" style="margin-top:12px;">
|
||||||
|
${actionTag(recommendedAction.label || "打开录制维护", recommendedAction.action, recommendedAttrs)}
|
||||||
|
</div>
|
||||||
|
` : ""}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (payload.content) {
|
if (payload.content) {
|
||||||
|
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
|
||||||
|
screen: "playbook",
|
||||||
|
title: "生成文案",
|
||||||
|
summary: recommendedAction?.summary || ""
|
||||||
|
});
|
||||||
return `
|
return `
|
||||||
<div class="task-item compact">
|
<div class="task-item compact">
|
||||||
<h4>生成文案</h4>
|
<h4>生成文案</h4>
|
||||||
@@ -2219,11 +2277,17 @@ function renderOneLinerExecutionPayloadHtml(payload) {
|
|||||||
<div class="task-meta">
|
<div class="task-meta">
|
||||||
${payload.assistant_id ? `<span class="tag blue">${escapeHtml(payload.assistant_id)}</span>` : ""}
|
${payload.assistant_id ? `<span class="tag blue">${escapeHtml(payload.assistant_id)}</span>` : ""}
|
||||||
<span class="tag">${escapeHtml(formatNumber(safeArray(payload.used_documents).length))} 个参考素材</span>
|
<span class="tag">${escapeHtml(formatNumber(safeArray(payload.used_documents).length))} 个参考素材</span>
|
||||||
|
${recommendedAction?.action ? actionTag(recommendedAction.label || "继续调文案", recommendedAction.action, recommendedAttrs) : ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (payload.verdict !== undefined || payload.next_actions !== undefined || payload.highlights !== undefined) {
|
if (payload.verdict !== undefined || payload.next_actions !== undefined || payload.highlights !== undefined) {
|
||||||
|
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
|
||||||
|
screen: "review",
|
||||||
|
title: payload.title || "复盘草稿",
|
||||||
|
summary: recommendedAction?.summary || ""
|
||||||
|
});
|
||||||
return `
|
return `
|
||||||
<div class="task-item compact">
|
<div class="task-item compact">
|
||||||
<h4>${escapeHtml(payload.title || "复盘草稿")}</h4>
|
<h4>${escapeHtml(payload.title || "复盘草稿")}</h4>
|
||||||
@@ -2232,7 +2296,7 @@ function renderOneLinerExecutionPayloadHtml(payload) {
|
|||||||
<span class="tag blue">${escapeHtml(platformLabel(payload.platform || "douyin"))}</span>
|
<span class="tag blue">${escapeHtml(platformLabel(payload.platform || "douyin"))}</span>
|
||||||
<span class="tag">${escapeHtml(payload.verdict || "待补充")}</span>
|
<span class="tag">${escapeHtml(payload.verdict || "待补充")}</span>
|
||||||
${payload.source_job_id ? `<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(payload.source_job_id)}">看任务详情</span>` : ""}
|
${payload.source_job_id ? `<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(payload.source_job_id)}">看任务详情</span>` : ""}
|
||||||
${payload.id ? `<span class="tag clickable-tag" data-action="open-review-edit" data-review-id="${escapeHtml(payload.id)}">打开复盘</span>` : ""}
|
${recommendedAction?.action ? actionTag(recommendedAction.label || "打开复盘", recommendedAction.action, recommendedAttrs) : payload.id ? `<span class="tag clickable-tag" data-action="open-review-edit" data-review-id="${escapeHtml(payload.id)}">打开复盘</span>` : ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -7825,12 +7889,40 @@ function extractGeneratedCopy(payload) {
|
|||||||
return brief(raw, 2400);
|
return brief(raw, 2400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRecommendedActionAttrs(recommendedAction, landing = {}) {
|
||||||
|
const action = recommendedAction && typeof recommendedAction === "object" ? recommendedAction : {};
|
||||||
|
const attrs = [];
|
||||||
|
const landingAttrs = buildMainAgentLandingAttrs({
|
||||||
|
runId: landing.runId || "",
|
||||||
|
screen: action.screen || landing.screen || "",
|
||||||
|
title: landing.title || "",
|
||||||
|
summary: action.summary || landing.summary || ""
|
||||||
|
});
|
||||||
|
if (landingAttrs) attrs.push(landingAttrs);
|
||||||
|
const attrMap = {
|
||||||
|
job_id: "data-job-id",
|
||||||
|
review_id: "data-review-id",
|
||||||
|
platform: "data-platform",
|
||||||
|
source_id: "data-source-id",
|
||||||
|
file_id: "data-file-id",
|
||||||
|
incident_id: "data-incident-id",
|
||||||
|
run_id: "data-run-id"
|
||||||
|
};
|
||||||
|
Object.entries(attrMap).forEach(([key, attr]) => {
|
||||||
|
const value = action[key];
|
||||||
|
const text = String(value ?? "").trim();
|
||||||
|
if (!text) return;
|
||||||
|
attrs.push(`${attr}="${escapeHtml(text)}"`);
|
||||||
|
});
|
||||||
|
return attrs.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
function renderLastActionCard() {
|
function renderLastActionCard() {
|
||||||
if (!appState.lastAction) return "";
|
if (!appState.lastAction) return "";
|
||||||
const payload = appState.lastAction.payload || {};
|
const payload = appState.lastAction.payload || {};
|
||||||
const recommendedAction = payload?.result?.recommended_action || payload?.recommended_action || null;
|
const recommendedAction = payload?.result?.recommended_action || payload?.recommended_action || null;
|
||||||
const runId = payload?.id || payload?.run_id || "";
|
const runId = payload?.id || payload?.run_id || "";
|
||||||
const landingAttrs = buildMainAgentLandingAttrs({
|
const actionAttrs = buildRecommendedActionAttrs(recommendedAction, {
|
||||||
runId,
|
runId,
|
||||||
screen: recommendedAction?.screen || "",
|
screen: recommendedAction?.screen || "",
|
||||||
title: appState.lastAction.title || "",
|
title: appState.lastAction.title || "",
|
||||||
@@ -7851,7 +7943,7 @@ function renderLastActionCard() {
|
|||||||
${(runId || recommendedAction?.action) ? `
|
${(runId || recommendedAction?.action) ? `
|
||||||
<div class="task-meta" style="margin-top:10px;">
|
<div class="task-meta" style="margin-top:10px;">
|
||||||
${runId ? `<span class="tag clickable-tag" data-action="open-oneliner-run-result" data-run-id="${escapeHtml(runId)}">查看结果</span>` : ""}
|
${runId ? `<span class="tag clickable-tag" data-action="open-oneliner-run-result" data-run-id="${escapeHtml(runId)}">查看结果</span>` : ""}
|
||||||
${recommendedAction?.action ? `<span class="tag clickable-tag" data-action="${escapeHtml(recommendedAction.action)}" ${landingAttrs}>${escapeHtml(recommendedAction.label || "回到对应页面")}</span>` : ""}
|
${recommendedAction?.action ? actionTag(recommendedAction.label || "回到对应页面", recommendedAction.action, actionAttrs) : ""}
|
||||||
</div>
|
</div>
|
||||||
` : ""}
|
` : ""}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -754,6 +754,25 @@ test("main agent result rendering offers a direct route back into the recommende
|
|||||||
assert.match(lastAction, /recommended_action/);
|
assert.match(lastAction, /recommended_action/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("direct oneliner execution results preserve structured follow-up attrs", () => {
|
||||||
|
const helpers = extractBetween(APP, "function extractGeneratedCopy(payload)", "function renderLastActionCard()");
|
||||||
|
const execution = extractBetween(APP, "function renderOneLinerExecutionPayloadHtml(payload)", "function parseOneLinerActionPayloadValue(value)");
|
||||||
|
const lastAction = extractBetween(APP, "function renderLastActionCard()", "function getJobRecoveryCategory(job)");
|
||||||
|
assert.match(helpers, /function buildRecommendedActionAttrs\(recommendedAction, landing = \{\}\)/);
|
||||||
|
assert.match(helpers, /job_id: "data-job-id"/);
|
||||||
|
assert.match(helpers, /review_id: "data-review-id"/);
|
||||||
|
assert.match(helpers, /platform: "data-platform"/);
|
||||||
|
assert.match(helpers, /source_id: "data-source-id"/);
|
||||||
|
assert.match(execution, /const recommendedAction = payload\.recommended_action/);
|
||||||
|
assert.match(execution, /buildRecommendedActionAttrs\(recommendedAction/);
|
||||||
|
assert.match(execution, /actionTag\(recommendedAction\.label \|\| "看任务详情"/);
|
||||||
|
assert.match(execution, /actionTag\(recommendedAction\.label \|\| "打开复盘"/);
|
||||||
|
assert.match(execution, /actionTag\(recommendedAction\.label \|\| "打开录制控制"/);
|
||||||
|
assert.match(execution, /actionTag\(recommendedAction\.label \|\| "查看平台 Agent"/);
|
||||||
|
assert.match(lastAction, /buildRecommendedActionAttrs\(recommendedAction/);
|
||||||
|
assert.match(lastAction, /actionTag\(recommendedAction\.label \|\| "回到对应页面"/);
|
||||||
|
});
|
||||||
|
|
||||||
test("platform agent profiles expose history, rollback, and execution version context", () => {
|
test("platform agent profiles expose history, rollback, and execution version context", () => {
|
||||||
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
|
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
|
||||||
const profileEditor = extractBetween(APP, "function openPlatformAgentProfileAction(platform)", "async function openPlatformAgentProfileHistoryAction(platform, preferredVersionId = \"\")");
|
const profileEditor = extractBetween(APP, "function openPlatformAgentProfileAction(platform)", "async function openPlatformAgentProfileHistoryAction(platform, preferredVersionId = \"\")");
|
||||||
|
|||||||
Reference in New Issue
Block a user