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": [], }