"""Feishu signal pipeline mixin.""" from __future__ import annotations from .shared import * class FeishuPipelineFollowupBMixin: @classmethod def _select_followup_status_target(cls, *, message: str, recent_items: list[FeishuItem]) -> FeishuItem: if not recent_items: raise ValueError("recent_items_empty") normalized = re.sub(r"\s+", "", str(message or "")).lower() wants_project_create = any(token in normalized for token in ("创建", "建好", "建成", "项目")) wants_source_status = any(token in normalized for token in ("信号源", "web搜索", "webhook", "source")) if wants_source_status: for item in recent_items: operation_code = cls._extract_item_operation_code(item) if operation_code == "source.connect" or "信号源" in str(item.raw_message or ""): return item if wants_project_create: for item in recent_items: operation_code = cls._extract_item_operation_code(item) level = str(item.level or "").strip().lower() card_type = str(item.card_type or "").strip().lower() if ( level == "project" or operation_code == "project.create" or card_type in {"project_create", "new_project", "new_project_candidate"} ): return item return recent_items[0] async def _build_followup_status_reply(self, item: FeishuItem) -> str: route_status = self._normalize_route_status(item.route_status) level = str(item.level or "").strip().lower() if level == "project": suggestion = await self._get_project_suggestion(int(item.id)) project_id = suggestion.created_project_id if suggestion and suggestion.created_project_id else item.matched_project_id if route_status == ROUTE_STATUS_AUTO_DONE and project_id: return f"最近一条项目候选已确认,项目#{int(project_id)} 已落库。" if route_status == ROUTE_STATUS_UNDONE: return "最近一条项目候选已撤销,系统没有保留该次自动结果。" return "最近一条项目候选仍在待确认,系统还没有落正式项目。" if level == "command": payload = item.result_payload if isinstance(item.result_payload, dict) else {} operation_code = str(payload.get("operation_code") or "").strip() project_id = _safe_int(payload.get("project_id") or item.matched_project_id) summary = str(payload.get("result_summary") or "").strip() if route_status == ROUTE_STATUS_AUTO_DONE: return summary or ( f"最近一条命令已执行完成,归属项目#{int(project_id)}。" if project_id is not None else "最近一条命令已执行完成。" ) if route_status == ROUTE_STATUS_UNDONE: return "最近一条命令结果已撤销。" if route_status == ROUTE_STATUS_QUEUED: return ( f"最近一条命令 {operation_code or '待处理动作'} 已入队待确认。" if operation_code else "最近一条命令已入队待确认。" ) return "最近一条命令仍在处理中。" signal = await self._get_signal_by_item(int(item.id)) signal_id = int(signal.id) if signal is not None else None project_id = int(signal.matched_project_id or item.matched_project_id) if (signal and (signal.matched_project_id or item.matched_project_id)) else None if route_status == ROUTE_STATUS_AUTO_DONE and signal_id and project_id: return f"最近一条信号#{signal_id} 已确认并关联项目#{project_id}。" if route_status == ROUTE_STATUS_QUEUED and signal_id and project_id: return f"最近一条信号#{signal_id} 已入队待确认,候选项目#{project_id}。" if route_status == ROUTE_STATUS_QUEUED and signal_id: return f"最近一条信号#{signal_id} 已入队待确认,但项目归属仍不明确。" if route_status == ROUTE_STATUS_UNDONE: return "最近一条信号结果已撤销。" return "最近一条飞书处理项仍在处理中,尚未得到可确认的 DB 状态。" async def _resolve_command_operator_user(self, *, sender: str) -> Optional[User]: sender_id = await self._resolve_user_id_by_name(sender) if sender_id is not None: sender_user = await self._load_active_user_by_id(user_id=int(sender_id)) if sender_user is not None: return sender_user manager = ( await self.db.execute( select(User).where( and_( User.tenant_id == self.tenant_id, User.is_active.is_(True), User.status == "active", User.role.in_(["BOSS", "ADMIN", "PM"]), ) ).order_by(User.id.asc()).limit(1) ) ).scalar_one_or_none() if manager is not None: return manager return await self._load_active_user_by_id(user_id=await self._fallback_user_id()) async def _load_active_user_by_id(self, user_id: Optional[int]) -> Optional[User]: if user_id is None: return None return ( await self.db.execute( select(User).where( and_( User.tenant_id == self.tenant_id, User.id == int(user_id), User.is_active.is_(True), User.status == "active", ) ).limit(1) ) ).scalar_one_or_none() async def _record_command_context_item( self, *, message: str, sender: str, source_chat_id: str, source_sender_open_id: Optional[str], feishu_message_id: Optional[str], captured_at: Optional[datetime], matched_project_id: Optional[int], route_status: str, command_result: dict[str, Any], ) -> Optional[int]: if matched_project_id is None and not source_chat_id: return None occurred_at = captured_at or _utc_now() if occurred_at.tzinfo is not None: occurred_at = occurred_at.astimezone(CN_TZ).replace(tzinfo=None) item = FeishuItem( tenant_id=self.tenant_id, feishu_message_id=feishu_message_id, project_score=1.0 if matched_project_id is not None else None, signal_confidence=0.0 if matched_project_id is not None else None, raw_message=message[:4000], split_from_item_id=None, level="command", level_confidence=1.0, level_reasoning="legacy_command_first_execution", card_type="command", route_status=self._normalize_route_status(route_status or ROUTE_STATUS_AUTO_DONE)[:30], matched_project_id=int(matched_project_id) if matched_project_id is not None else None, source_chat_id=(source_chat_id or "")[:128] or None, source_sender=(sender or "")[:120] or None, source_sender_open_id=(source_sender_open_id or "")[:128] or None, source_occurred_at=occurred_at, ai_output=json.dumps({"command_result": command_result}, ensure_ascii=False)[:8000], message_class="auto_done", safe_to_auto_execute=True, auto_execute_state="done", result_payload={ "intent": "command", "executor": "legacy_command_planner", "operation_code": None, "result_summary": self._build_operation_commit_reply(command_result), "command_result": command_result, "project_id": int(matched_project_id) if matched_project_id is not None else None, "target_url": f"/projects/{int(matched_project_id)}/board" if matched_project_id is not None else None, }, queue_visible=False, processed_at=_utc_now().replace(tzinfo=None), ) self.db.add(item) await self.db.flush() return int(item.id) @staticmethod def _resolve_command_route_status(command_result: dict[str, Any]) -> str: status = str( command_result.get("command_status") or command_result.get("run_status") or command_result.get("execution_status") or "" ).strip().lower() if status in {"completed", "partial", "planned"}: return ROUTE_STATUS_AUTO_DONE return ROUTE_STATUS_FAILED @staticmethod def _collect_command_project_ids(command_result: dict[str, Any]) -> list[int]: project_ids: set[int] = set() project_info = command_result.get("project") if isinstance(command_result, dict) else None if isinstance(project_info, dict): project_id = project_info.get("id") try: if project_id is not None and int(project_id) > 0: project_ids.add(int(project_id)) except (TypeError, ValueError): pass execution = command_result.get("execution") if isinstance(command_result, dict) else None results = execution.get("results") if isinstance(execution, dict) else None if isinstance(results, list): for row in results: if not isinstance(row, dict): continue if str(row.get("entity_type") or "").strip() != "project": continue payload = row.get("result") if isinstance(row.get("result"), dict) else {} candidate = payload.get("project_id") or row.get("entity_id") try: if candidate is not None and int(candidate) > 0: project_ids.add(int(candidate)) except (TypeError, ValueError): continue return sorted(project_ids) @staticmethod def _extract_created_project_id(command_result: dict[str, Any]) -> Optional[int]: execution = command_result.get("execution") if isinstance(command_result, dict) else None results = execution.get("results") if isinstance(execution, dict) else None if not isinstance(results, list): return None for row in results: if not isinstance(row, dict): continue if str(row.get("operation_code") or "").strip() != "project.create": continue payload = row.get("result") if isinstance(row.get("result"), dict) else {} candidate = payload.get("project_id") or row.get("entity_id") try: if candidate is not None and int(candidate) > 0: return int(candidate) except (TypeError, ValueError): continue return None @classmethod def _build_operation_commit_reply(cls, command_result: dict[str, Any]) -> str: execution = command_result.get("execution") if isinstance(command_result, dict) else {} results = execution.get("results") if isinstance(execution, dict) else [] if not isinstance(results, list) or not results: return "未执行:未识别到可安全落库的业务操作。" fragments = [cls._format_operation_result_fragment(row) for row in results[:3] if isinstance(row, dict)] fragments = [item for item in fragments if item] applied_count = int((execution or {}).get("applied_count") or 0) skipped_count = int((execution or {}).get("skipped_count") or 0) failed_count = int((execution or {}).get("failed_count") or 0) if applied_count <= 0 and skipped_count <= 0 and failed_count > 0: first_failed = next((row for row in results if isinstance(row, dict) and str(row.get("status") or "") == "failed"), {}) message = str(first_failed.get("message") or "执行失败").strip() return f"未执行:{message}" if not fragments: return "已接收指令,但未生成可展示的执行回执。" prefix = "未重复执行:" if applied_count <= 0 and skipped_count > 0 else "已执行完成:" reply = f"{prefix}{';'.join(fragments[:2])}" if failed_count > 0 and applied_count > 0: reply = f"{reply};另有 {failed_count} 项失败" return reply + "。" @staticmethod def _format_operation_result_fragment(row: dict[str, Any]) -> str: operation_code = str(row.get("operation_code") or "").strip() status = str(row.get("status") or "").strip().lower() message = str(row.get("message") or "").strip() payload = row.get("result") if isinstance(row.get("result"), dict) else {} if status == "failed": return "" if operation_code.startswith("project."): project_id = payload.get("project_id") or row.get("entity_id") title = str(payload.get("title") or payload.get("project_title") or "").strip() status_text = str(payload.get("status") or "").strip() label = f"项目#{project_id}" if title: label = f"{label}「{title}」" if operation_code == "project.create" and status == "applied": return f"{label}已创建,状态 {status_text or 'active'}" if operation_code == "project.create" and status == "skipped": return f"{label}已存在,状态 {status_text or 'active'}" if status_text: return f"{label}状态已更新为 {status_text}" return f"{label}{message or '已处理'}" if operation_code == "task.create": return f"任务#{row.get('entity_id') or ''}已创建".strip() if operation_code == "task.assign": return f"任务#{row.get('entity_id') or ''}已分配".strip() if operation_code == "task.prioritize": return f"任务#{row.get('entity_id') or ''}优先级已更新".strip() if operation_code == "task.status_transition": return f"任务#{row.get('entity_id') or ''}状态已更新".strip() if operation_code == "personnel.workload_update": return f"成员#{row.get('entity_id') or ''}工时已更新".strip() if operation_code == "notification.create": return f"通知#{row.get('entity_id') or ''}已创建".strip() return message or operation_code