from __future__ import annotations from .shared import * class FeishuReplyGuardMixin: @staticmethod def _detect_forbidden_execute_keyword(text: Optional[str]) -> Optional[str]: lowered = str(text or "").strip().lower() if not lowered: return None forbidden = ( "curl", "调用方式", "接口教程", "api 调用", "请求示例", "postman", "sdk 调用", ) for token in forbidden: if token in lowered: return token return None @staticmethod def _detect_forbidden_qa_keyword(text: Optional[str]) -> Optional[str]: lowered = str(text or "").strip().lower() if not lowered: return None forbidden = ( "模型失败了所以没分析", "让我直接处理", "直接操作数据库", "先检查数据库", "检查数据库迁移", "数据库迁移状态", "alembic", "ddl", "alter table", "drop table", "create table", "truncate table", ) for token in forbidden: if token in lowered: return token return None @classmethod def _contains_forbidden_execute_keywords(cls, text: Optional[str]) -> bool: return cls._detect_forbidden_execute_keyword(text) is not None def _guard_execute_reply_text_with_reason( self, text: Optional[str], *, trace_id: str, ) -> tuple[Optional[str], Optional[str]]: if not text: return text, None hit = self._detect_forbidden_execute_keyword(text) if not hit: return text, None self._emit_state_transition( trace_id=trace_id, state="ROUTED", route="execute_template_guard", reason="forbidden_keyword_detected", keyword=hit, ) return ( "已执行完成并入库。如需我继续动作,请直接补充目标、对象和时限。", hit, ) def _guard_execute_reply_text(self, text: Optional[str], *, trace_id: str) -> Optional[str]: sanitized, _ = self._guard_execute_reply_text_with_reason(text=text, trace_id=trace_id) return sanitized def _guard_qa_reply_text_with_reason( self, text: Optional[str], *, trace_id: str, capture_result: Dict[str, Any], ) -> tuple[Optional[str], Optional[str]]: if not text: return text, None hit = self._detect_forbidden_qa_keyword(text) if not hit: return text, None self._emit_state_transition( trace_id=trace_id, state="ROUTED", route="qa_llm_guard", reason="forbidden_keyword_detected", keyword=hit, ) return self._fallback_reply_text(capture_result), hit async def _record_execute_guard_alert( self, *, trace_id: str, message_id: str, message_content: str, capture_result: Dict[str, Any], guard_reason: str, ) -> None: try: async with AsyncSessionLocal() as db: db.add( AIProcessingLog( tenant_id=int(self.tenant_id or 1), feishu_message_id=(message_id or "")[:128] or None, feishu_item_id=None, raw_message=(message_content or "")[:8000] or None, ai_raw_output=json.dumps( { "trace_id": trace_id, "guard_reason": guard_reason, "status": capture_result.get("status"), "classification": capture_result.get("classification"), }, ensure_ascii=False, )[:120000], project_score=None, signal_confidence=None, level="execute_guard", routing_decision="execute_reply_guard_triggered", routing_reason=f"执行态回复命中禁词({guard_reason}),已切换模板模式。", reasoning_bullets=[ "执行态不允许输出调用教程类文本。", "本次回复已被熔断为固定模板。", f"trace_id={trace_id}", ], processing_status="success", error_message=None, duration_ms=0, ) ) recipients = ( await db.execute( select(User.id).where( and_( User.tenant_id == int(self.tenant_id or 1), User.is_active.is_(True), ) ).limit(5) ) ).scalars().all() await NotificationService(db).create_notifications( tenant_id=int(self.tenant_id or 1), user_ids=[int(uid) for uid in recipients], category="alert", title="执行态回复熔断已触发", content=( f"trace_id={trace_id}\n" f"message_id={message_id}\n" f"原因:命中禁词 {guard_reason}\n" "系统已自动切换为固定模板回复。" )[:2000], source="feishu_guard", entity_type="feishu_message", entity_id=None, target_url="/ai-logs", commit=False, ) await db.commit() except Exception as exc: print(f"[Feishu] execute guard alert write failed: trace_id={trace_id}, error={exc}") @staticmethod def _is_duplicate_capture_result(result: Dict[str, Any]) -> bool: if not isinstance(result, dict): return False status = str(result.get("status") or "").strip().lower() if status != "ignored": return False reason = str(result.get("reason") or "").strip().lower() classification = str(result.get("classification") or "").strip().lower() return reason == "duplicate_filtered" or classification == "duplicate" @staticmethod def _should_reply_to_mention(*, mentioned_bot_names: List[str], message_content: Optional[str]) -> bool: # 仅在明确 @PMFlow(而不是 @all / @其他成员)时触发自动回复 normalized_names = {str(x or "").strip().lower() for x in mentioned_bot_names if str(x or "").strip()} if any(name in {"pmflow", "_user_1"} for name in normalized_names): return True # 兼容文本中的 @ 语法(不同消息类型返回格式不一致) text = str(message_content or "").strip().lower() if not text: return False if "@pmflow" in text or "@_user_1" in text: return True return False