PMFlow/backend/app/api/ai_pmo_api/helpers_resume.py

192 lines
6.8 KiB
Python

from __future__ import annotations
from .shared import *
def _queue_resume_work_item(
*,
tenant_id: int,
work_item_id: int,
source_message_id: Optional[str],
sender: str,
project_id: int,
) -> None:
message_id = (
str(source_message_id or "").strip()
or f"decision-approve-bg-{int(work_item_id)}-{int(datetime.now().timestamp() * 1000)}"
)[:128]
task = asyncio.create_task(
_run_resume_work_item_in_background(
tenant_id=int(tenant_id),
work_item_id=int(work_item_id),
source_message_id=message_id,
sender=str(sender or "PMFlow用户"),
project_id=int(project_id),
),
name=f"ai-pmo-resume-{int(work_item_id)}",
)
def _on_done(done_task: asyncio.Task[None]) -> None:
if done_task.cancelled():
return
exc = done_task.exception()
if exc is not None:
logger.warning(
"ai_pmo_background_resume_failed",
tenant_id=int(tenant_id),
work_item_id=int(work_item_id),
error=str(exc)[:400],
)
task.add_done_callback(_on_done)
async def _run_resume_work_item_in_background(
*,
tenant_id: int,
work_item_id: int,
source_message_id: str,
sender: str,
project_id: int,
) -> None:
async with AsyncSessionLocal() as bg_db:
service = AIPMOKernelService(bg_db, tenant_id=int(tenant_id))
await service.process_ingress(
source_system="ai_pmo_ui",
source_message_id=source_message_id,
message_text=f"继续处理 work_item #{int(work_item_id)}",
sender=str(sender or "PMFlow用户"),
channel_ref="ai_pmo_ui",
source_sender_open_id=None,
captured_at=None,
chat_type="manual",
project_hint_id=int(project_id),
)
def _normalize_resume_result(*, raw: dict[str, Any], target_work_item: WorkItem) -> dict[str, Any]:
if not bool(raw.get("handled")):
reason = str(raw.get("error") or raw.get("reason") or "继续执行失败").strip() or "继续执行失败"
return {
"handled": False,
"status": "failed_safe",
"followup_reply": reason,
"target_work_item_id": int(target_work_item.id),
"target_state": str(target_work_item.state or "").strip() or None,
"target_intent": str(target_work_item.intent or "").strip() or None,
"project_id": _safe_int(target_work_item.project_id),
"signal_id": _safe_int(target_work_item.signal_id),
"auto_tasks": [],
}
state = None
rows = raw.get("results") if isinstance(raw.get("results"), list) else []
if rows and isinstance(rows[0], dict):
state = str(rows[0].get("state") or "").strip() or None
if not state:
state = str(target_work_item.state or "").strip() or None
if state == "succeeded":
status = "succeeded"
elif state == "waiting_manual":
status = "pending_manual"
elif state == "ignored":
status = "invalidated"
else:
status = "failed_safe"
followup_reply = str(raw.get("direct_reply_text") or "").strip()
if not followup_reply:
if status == "succeeded":
followup_reply = "已继续执行并完成。"
elif status == "pending_manual":
followup_reply = "继续执行后仍需补充信息或人工确认。"
elif status == "invalidated":
followup_reply = "该工作项已失效,不再继续执行。"
else:
followup_reply = "继续执行失败,请查看详情。"
linked_project_ids = raw.get("linked_project_ids") if isinstance(raw.get("linked_project_ids"), list) else []
project_id = _safe_int(linked_project_ids[0]) if linked_project_ids else _safe_int(target_work_item.project_id)
return {
"handled": True,
"status": status,
"followup_reply": followup_reply,
"target_work_item_id": int(target_work_item.id),
"target_state": state,
"target_intent": str(target_work_item.intent or "").strip() or None,
"project_id": project_id,
"signal_id": _safe_int(raw.get("signal_id")) or _safe_int(target_work_item.signal_id),
"auto_tasks": raw.get("auto_tasks") if isinstance(raw.get("auto_tasks"), list) else [],
}
async def _reject_work_item(
*,
db: AsyncSession,
tenant_id: int,
target: WorkItem,
current_user: User,
reason: Optional[str],
) -> dict[str, Any]:
current_state = str(target.state or "").strip()
if current_state == "ignored":
return {
"handled": True,
"status": "invalidated",
"followup_reply": "该工作项已处于失效状态。",
"target_work_item_id": int(target.id),
"target_state": "ignored",
"target_intent": str(target.intent or "").strip() or None,
"project_id": _safe_int(target.project_id),
"signal_id": _safe_int(target.signal_id),
"auto_tasks": [],
}
reject_reason = str(reason or "").strip() or "人工审批拒绝,工作项已失效。"
existing_execution = target.execution_result if isinstance(target.execution_result, dict) else {}
target.state = "ignored"
target.execution_result = {
**existing_execution,
"status": "invalidated",
"manual_decision": "reject",
"manual_reason": reject_reason,
"operator_user_id": _safe_int(current_user.id),
"processed_at": datetime.now().isoformat(),
}
target.review_summary = {
"verified": False,
"issues": ["rejected_by_user"],
"next_step": "该工作项已进入失效状态",
"operator": str(current_user.name or current_user.username or "").strip() or None,
"reason": reject_reason,
}
target.reply_template_key = "action_rejected"
decision_row = (
await db.execute(
select(DecisionRecord)
.where(
and_(
DecisionRecord.tenant_id == tenant_id,
DecisionRecord.work_item_id == int(target.id),
)
)
.order_by(DecisionRecord.id.desc())
.limit(1)
)
).scalar_one_or_none()
if decision_row is not None:
decision_row.execution_result = target.execution_result
decision_row.final_outcome = "ignored"
await db.commit()
return {
"handled": True,
"status": "invalidated",
"followup_reply": reject_reason,
"target_work_item_id": int(target.id),
"target_state": "ignored",
"target_intent": str(target.intent or "").strip() or None,
"project_id": _safe_int(target.project_id),
"signal_id": _safe_int(target.signal_id),
"auto_tasks": [],
}