"""Feishu signal pipeline mixin.""" from __future__ import annotations from .shared import * class FeishuPipelineItemProcessingMixin: async def _process_one_item( self, *, content: str, level: str, sender: str, source_chat_id: str, source_sender_open_id: Optional[str], feishu_message_id: Optional[str], occurred_at: datetime, level_result: dict[str, Any], level_confidence: float, level_reasoning: str, project_score: float, cold_start_enabled: bool, signal_auto_enter_threshold: float, project_auto_create_threshold: float, project_uncertain_threshold: float, chat_context_auto_link_enabled: bool, projects: list[dict[str, Any]], split_from_item_id: Optional[int], chat_type: str, chat_context_resolution: dict[str, Any], ) -> dict[str, Any]: item_started_at = time.perf_counter() level_val = "project" if level == "project" else "signal" matched_project_hint = _safe_int(chat_context_resolution.get("project_id")) dedupe_key = self._build_queue_dedupe_key( level=level_val, intent=INTENT_SIGNAL_INTAKE if level_val == "signal" else INTENT_CREATE_PROJECT, message=content, source_chat_id=source_chat_id, matched_project_id=matched_project_hint, source_object=content, ) existing_item = await self._find_active_dedupe_item(dedupe_key) if existing_item is not None: existing_item.occurrence_count = max(1, int(existing_item.occurrence_count or 1)) + 1 existing_item.raw_message = content[:4000] existing_item.source_sender = (sender or "")[:120] or None existing_item.source_sender_open_id = (source_sender_open_id or "")[:128] or None existing_item.source_chat_id = (source_chat_id or "")[:128] or None existing_item.source_occurred_at = occurred_at.replace(tzinfo=None) existing_item.level_reasoning = level_reasoning[:1000] existing_item.ai_output = json.dumps( { "chat_type": chat_type, "normalized_text": content, "level_result": level_result, }, ensure_ascii=False, )[:8000] await self.db.flush() return await self._build_existing_item_result(existing_item) item = FeishuItem( tenant_id=self.tenant_id, feishu_message_id=feishu_message_id, project_score=project_score, signal_confidence=max(0.0, 1 - project_score), raw_message=content[:4000], split_from_item_id=split_from_item_id, level=level_val, level_confidence=level_confidence, level_reasoning=level_reasoning[:1000], card_type="signal" if level_val == "signal" else "new_project", route_status=ROUTE_STATUS_CLASSIFIED, 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.replace(tzinfo=None), ai_output=json.dumps( { "chat_type": chat_type, "normalized_text": content, "level_result": level_result, }, ensure_ascii=False, )[:8000], decision_reason=level_reasoning[:1000] or None, safe_to_auto_execute=False, auto_execute_state="blocked", dedupe_key=dedupe_key, message_class="action_required", occurrence_count=1, next_step_text="AI 正在校验上下文,确认后会继续推进后续动作。", queue_visible=True, ) self.db.add(item) await self.db.flush() if level_val == "project": project_info, suggestion = await self._create_project_suggestion( item_id=int(item.id), content=content, sender=sender, fallback_confidence=level_confidence, ) return await self._finalize_project_item( item=item, content=content, feishu_message_id=feishu_message_id, level_result=level_result, project_info=project_info, project_score=project_score, signal_confidence=max(0.0, 1 - project_score), suggestion=suggestion, sender=sender, cold_start_enabled=cold_start_enabled, project_auto_create_threshold=project_auto_create_threshold, project_uncertain_threshold=project_uncertain_threshold, item_started_at=item_started_at, ) simple_signal_request = self._is_simple_signal_source_request(content) signal_info = await self._extract_signal(content, projects=projects) matched_project_id = signal_info.get("matched_project_id") if matched_project_id is not None: try: matched_project_id = int(matched_project_id) except (TypeError, ValueError): matched_project_id = None confidence = _safe_float(signal_info.get("confidence"), max(0.55, 1 - project_score)) if simple_signal_request: confidence = max(confidence, min(0.95, signal_auto_enter_threshold + 0.05)) inferred_project_id: Optional[int] = None clarification_message: Optional[str] = None if (chat_context_auto_link_enabled or simple_signal_request) and matched_project_id is None and source_chat_id: inferred_project_id = _safe_int(chat_context_resolution.get("project_id")) if inferred_project_id is not None: matched_project_id = int(inferred_project_id) elif bool(chat_context_resolution.get("ambiguous")): candidate_ids = [int(x) for x in (chat_context_resolution.get("candidate_ids") or [])[:3] if _safe_int(x)] if candidate_ids: clarification_message = ( f"当前群最近同时涉及项目#{'、#'.join([str(pid) for pid in candidate_ids])}," "请明确这条消息归属哪个项目。" ) if matched_project_id is None and self._should_promote_signal_to_project_candidate( content=content, signal_info=signal_info, confidence=confidence, signal_auto_enter_threshold=signal_auto_enter_threshold, simple_signal_request=simple_signal_request, ): promoted_project_score = max(float(project_score or 0.0), float(project_uncertain_threshold)) item.level = "project" item.project_score = promoted_project_score item.signal_confidence = max(0.0, 1 - promoted_project_score) item.card_type = "new_project_candidate" item.matched_project_id = None project_info, suggestion = await self._create_project_suggestion( item_id=int(item.id), content=content, sender=sender, fallback_confidence=confidence, ) return await self._finalize_project_item( item=item, content=content, feishu_message_id=feishu_message_id, level_result=level_result, project_info=project_info, project_score=promoted_project_score, signal_confidence=confidence, suggestion=suggestion, sender=sender, cold_start_enabled=cold_start_enabled, project_auto_create_threshold=project_auto_create_threshold, project_uncertain_threshold=project_uncertain_threshold, item_started_at=item_started_at, extra_ai_fields={ "signal_extraction": signal_info, "promoted_to_project_candidate": True, "simple_signal_request": simple_signal_request, }, ) return await self._finalize_signal_item( item=item, content=content, sender=sender, source_chat_id=source_chat_id, source_sender_open_id=source_sender_open_id, feishu_message_id=feishu_message_id, level_result=level_result, signal_info=signal_info, matched_project_id=matched_project_id, confidence=confidence, project_score=project_score, cold_start_enabled=cold_start_enabled, signal_auto_enter_threshold=signal_auto_enter_threshold, chat_context_resolution=chat_context_resolution, inferred_project_id=inferred_project_id, simple_signal_request=simple_signal_request, clarification_message=clarification_message, item_started_at=item_started_at, ) async def _create_project_suggestion( self, *, item_id: int, content: str, sender: str, fallback_confidence: float, ) -> tuple[dict[str, Any], ProjectSuggestion]: project_info = await self._extract_project_prefill(content) suggestion = ProjectSuggestion( tenant_id=self.tenant_id, feishu_item_id=int(item_id), suggested_name=(project_info.get("suggested_name") or content[:20] or "新项目建议")[:255], suggested_description=(project_info.get("suggested_description") or content[:400])[:4000], suggested_owner=(project_info.get("suggested_owner") or sender or "待分配")[:120], suggested_timeline=(project_info.get("suggested_timeline") or "待确认")[:120], confidence=_safe_float(project_info.get("confidence"), fallback_confidence), reasoning=json.dumps(_normalize_reasoning(project_info.get("reasoning")), ensure_ascii=False), status="pending", ) self.db.add(suggestion) await self.db.flush() return project_info, suggestion async def _finalize_project_item( self, *, item: FeishuItem, content: str, feishu_message_id: Optional[str], level_result: dict[str, Any], project_info: dict[str, Any], project_score: float, signal_confidence: float, suggestion: ProjectSuggestion, sender: str, cold_start_enabled: bool, project_auto_create_threshold: float, project_uncertain_threshold: float, item_started_at: float, extra_ai_fields: Optional[dict[str, Any]] = None, ) -> dict[str, Any]: project_id = await self._route_project_item( item=item, suggestion=suggestion, sender=sender, project_score=project_score, cold_start_enabled=cold_start_enabled, project_auto_create_threshold=project_auto_create_threshold, ) routing_decision, routing_reason, reasoning_bullets = self._build_project_routing_log( project_score=project_score, cold_start_enabled=cold_start_enabled, project_auto_create_threshold=project_auto_create_threshold, project_uncertain_threshold=project_uncertain_threshold, ) ai_raw_payload: dict[str, Any] = { "level_classification": level_result, "project_prefill": project_info, } if isinstance(extra_ai_fields, dict): ai_raw_payload.update(extra_ai_fields) await self._write_processing_log( feishu_message_id=feishu_message_id, feishu_item_id=int(item.id), raw_message=content, ai_raw_output=json.dumps(ai_raw_payload, ensure_ascii=False), project_score=project_score, signal_confidence=signal_confidence, level="project", routing_decision=routing_decision, routing_reason=routing_reason, reasoning_bullets=reasoning_bullets, processing_status="success", error_message=None, duration_ms=int((time.perf_counter() - item_started_at) * 1000), ) return { "item_id": int(item.id), "level": "project", "project_id": project_id, "signal_id": None, "route_status": item.route_status, }