import json import re from typing import Any from sqlalchemy.orm import Session from app.core.constants import ActorValue from app.core.config import get_settings from app.modules.ai_agent.service import AIService from app.modules.ai_agent.constants import AIResponseKey from app.modules.audit.constants import AuditSource from app.modules.feishu.constants import ( FEISHU_AI_REPLY_TITLE, FEISHU_MENTION_PATTERN, FEISHU_ZERO_WIDTH_SPACE, FeishuCommandKey, FeishuCommandName, FeishuCommandResultKey, FeishuPayloadKey, FeishuReplyType, ) from app.modules.reports.constants import ReportResponseKey from app.modules.feishu.service import FeishuService from app.modules.reports.services import ReportService from app.modules.risk.constants import RiskSummaryKey from app.modules.risk.services import RiskService DAILY_REPORT_KEYWORDS = ("日报", "晨报", "经营日报", "经营晨报") PROJECT_WEEKLY_KEYWORDS = ("周报", "项目周报") ATTENDANCE_KEYWORDS = ("打卡", "考勤", "attendance") RISK_KEYWORDS = ("风险", "预警", "risk") AI_COMMAND_PREFIXES = ("问 ", "ai ", "AI ", "/ask ") RISK_TITLE = "风险预警" DEFAULT_AI_PROMPT = "请说明你能做什么。" def _parse_content_text(content: Any) -> str: """Extract plain command text from a Feishu message content payload.""" if isinstance(content, dict): return str( content.get(FeishuPayloadKey.TEXT) or content.get(FeishuPayloadKey.CONTENT) or "" ) if not isinstance(content, str): return "" try: data = json.loads(content) except json.JSONDecodeError: return content if isinstance(data, dict): return str( data.get(FeishuPayloadKey.TEXT) or data.get(FeishuPayloadKey.CONTENT) or "" ) return content def _clean_command_text(text: str) -> str: """Remove mentions and invisible characters from Feishu command text.""" text = re.sub(FEISHU_MENTION_PATTERN, "", text or "") text = text.replace(FEISHU_ZERO_WIDTH_SPACE, "") return text.strip() def _command_result( command: FeishuCommandName, reply_type: FeishuReplyType, title: str, content: str, provider_response: dict[str, Any] | None = None, lines: list[str] | None = None, ) -> dict[str, Any]: result: dict[str, Any] = { FeishuCommandResultKey.COMMAND: command, FeishuCommandResultKey.REPLY_TYPE: reply_type, FeishuCommandResultKey.TITLE: title, FeishuCommandResultKey.CONTENT: content, FeishuCommandResultKey.PROVIDER_RESPONSE: provider_response, } if lines is not None: result[FeishuCommandResultKey.LINES] = lines return result class FeishuCommandService: """Route Feishu text commands to reports, risk summaries, or AI replies.""" def __init__(self, db: Session): self.db = db self.feishu = FeishuService(db) def extract_event_command(self, payload: dict[str, Any]) -> dict[str, Any] | None: event = payload.get(FeishuPayloadKey.EVENT) or {} message = event.get(FeishuPayloadKey.MESSAGE) or {} if not message: return None text = _clean_command_text(_parse_content_text(message.get(FeishuPayloadKey.CONTENT))) if not text: return None sender = event.get(FeishuPayloadKey.SENDER) or {} sender_id = sender.get(FeishuPayloadKey.SENDER_ID) or {} actor = ( sender_id.get(FeishuPayloadKey.OPEN_ID) or sender_id.get(FeishuPayloadKey.USER_ID) or ActorValue.FEISHU ) return { FeishuCommandKey.TEXT: text, FeishuCommandKey.CHAT_ID: message.get(FeishuCommandKey.CHAT_ID), FeishuCommandKey.ACTOR: actor, } def handle_text( self, text: str, chat_id: str | None = None, actor: str = ActorValue.FEISHU, auto_reply: bool = True, ) -> dict[str, Any]: command_text = _clean_command_text(text) lowered = command_text.lower() provider_response: dict[str, Any] | None = None if any(keyword in command_text for keyword in DAILY_REPORT_KEYWORDS): report = ReportService(self.db).daily_brief() if auto_reply: provider_response = self._send_card_if_configured( chat_id, report[ReportResponseKey.TITLE], report[ReportResponseKey.LINES], actor, ) return _command_result( FeishuCommandName.DAILY_BRIEF, FeishuReplyType.CARD, report[ReportResponseKey.TITLE], report[ReportResponseKey.CONTENT], provider_response, report[ReportResponseKey.LINES], ) if any(keyword in command_text for keyword in PROJECT_WEEKLY_KEYWORDS): report = ReportService(self.db).project_weekly() if auto_reply: provider_response = self._send_card_if_configured( chat_id, report[ReportResponseKey.TITLE], report[ReportResponseKey.LINES], actor, ) return _command_result( FeishuCommandName.PROJECT_WEEKLY, FeishuReplyType.CARD, report[ReportResponseKey.TITLE], report[ReportResponseKey.CONTENT], provider_response, report[ReportResponseKey.LINES], ) if any(keyword in command_text for keyword in ATTENDANCE_KEYWORDS): report = ReportService(self.db).attendance_summary() if auto_reply: provider_response = self._send_card_if_configured( chat_id, report[ReportResponseKey.TITLE], report[ReportResponseKey.LINES], actor, ) return _command_result( FeishuCommandName.ATTENDANCE_SUMMARY, FeishuReplyType.CARD, report[ReportResponseKey.TITLE], report[ReportResponseKey.CONTENT], provider_response, report[ReportResponseKey.LINES], ) if any(keyword in command_text for keyword in RISK_KEYWORDS): summary = RiskService(self.db).summary() lines = [ f"- 综合风险等级:{summary[RiskSummaryKey.RISK_LEVEL]}", f"- 风险分:{summary[RiskSummaryKey.RISK_SCORE]}", f"- 逾期任务:{len(summary[RiskSummaryKey.OVERDUE_TASKS])}", f"- 延期项目:{len(summary[RiskSummaryKey.DELAYED_PROJECTS])}", f"- 超预算项目:{len(summary[RiskSummaryKey.OVER_BUDGET_PROJECTS])}", f"- 资金风险账户:{len(summary[RiskSummaryKey.FUND_RISKS])}", f"- 供应商风险:{len(summary[RiskSummaryKey.SUPPLIER_RISKS])}", f"- 打开风险事件:{len(summary[RiskSummaryKey.OPEN_EVENTS])}", ] if auto_reply: provider_response = self._send_card_if_configured( chat_id, RISK_TITLE, lines, actor, ) return _command_result( FeishuCommandName.RISK_SUMMARY, FeishuReplyType.CARD, RISK_TITLE, "\n".join(lines), provider_response, lines, ) prompt = command_text for prefix in AI_COMMAND_PREFIXES: if command_text.startswith(prefix): prompt = command_text[len(prefix) :].strip() break if not prompt: prompt = DEFAULT_AI_PROMPT ai_result = AIService(self.db).ask( prompt, context={}, actor=actor, source=AuditSource.FEISHU, ) content = ai_result[AIResponseKey.ANSWER] is_explicit_ai = any( command_text.startswith(prefix) or lowered.startswith(prefix) for prefix in AI_COMMAND_PREFIXES ) if auto_reply: provider_response = self._send_text_if_configured(chat_id, content, actor) return _command_result( FeishuCommandName.AI_ASK if is_explicit_ai else FeishuCommandName.FALLBACK_AI, FeishuReplyType.TEXT, FEISHU_AI_REPLY_TITLE, content, provider_response, ) def _send_card_if_configured( self, chat_id: str | None, title: str, lines: list[str], actor: str, ) -> dict[str, Any] | None: settings = get_settings() if not (settings.feishu_app_id and settings.feishu_app_secret): return None card = FeishuService.build_basic_card(title, lines) return self.feishu.send_card(card, receive_id=chat_id, actor=actor) def _send_text_if_configured( self, chat_id: str | None, text: str, actor: str, ) -> dict[str, Any] | None: settings = get_settings() if not (settings.feishu_app_id and settings.feishu_app_secret): return None return self.feishu.send_text(text, receive_id=chat_id, actor=actor)