import json import re from typing import Any from sqlalchemy.orm import Session from app.application.feishu.delivery import send_card_if_configured, send_text_if_configured from app.application.feishu.handlers import ( handle_finance_command, handle_market_command, handle_rule_command, ) from app.application.feishu.results import command_result from app.core.constants import ActorValue from app.modules.ai_agent.constants import AIResponseKey from app.modules.ai_agent.service import AIService 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, FeishuPayloadKey, FeishuReplyType, ) from app.modules.feishu.service import FeishuService from app.modules.reports.constants import ReportResponseKey from app.modules.reports.services import ReportService DAILY_REPORT_KEYWORDS = ("日报", "晨报", "经营日报", "经营晨报") PROJECT_WEEKLY_KEYWORDS = ("周报", "项目周报") ATTENDANCE_KEYWORDS = ("打卡", "考勤", "attendance") RISK_KEYWORDS = ("风险", "预警", "risk") AI_COMMAND_PREFIXES = ("问 ", "ai ", "AI ", "/ask ") 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() class FeishuCommandService: """Route Feishu text commands to focused application handlers.""" 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() for handler in ( handle_rule_command, handle_finance_command, handle_market_command, ): result = handler( self.db, self.feishu, command_text, chat_id, actor, auto_reply, ) if result is not None: return result report_result = self._handle_report_command(command_text, chat_id, actor, auto_reply) if report_result is not None: return report_result return self._handle_ai_command(command_text, lowered, chat_id, actor, auto_reply) def _handle_report_command( self, command_text: str, chat_id: str | None, actor: str, auto_reply: bool, ) -> dict[str, Any] | None: service = ReportService(self.db) if any(keyword in command_text for keyword in DAILY_REPORT_KEYWORDS): command = FeishuCommandName.DAILY_BRIEF report = service.daily_brief() elif any(keyword in command_text for keyword in PROJECT_WEEKLY_KEYWORDS): command = FeishuCommandName.PROJECT_WEEKLY report = service.project_weekly() elif any(keyword in command_text for keyword in ATTENDANCE_KEYWORDS): command = FeishuCommandName.ATTENDANCE_SUMMARY report = service.attendance_summary() elif any(keyword in command_text for keyword in RISK_KEYWORDS): command = FeishuCommandName.RISK_SUMMARY report = service.risk_progress() else: return None response = ( send_card_if_configured( self.feishu, chat_id, report[ReportResponseKey.TITLE], report[ReportResponseKey.LINES], actor, ) if auto_reply else None ) return command_result( command, FeishuReplyType.CARD, report[ReportResponseKey.TITLE], report[ReportResponseKey.CONTENT], response, report[ReportResponseKey.LINES], ) def _handle_ai_command( self, command_text: str, lowered: str, chat_id: str | None, actor: str, auto_reply: bool, ) -> dict[str, Any]: 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 ) response = ( send_text_if_configured(self.feishu, chat_id, content, actor) if auto_reply else None ) return command_result( FeishuCommandName.AI_ASK if is_explicit_ai else FeishuCommandName.FALLBACK_AI, FeishuReplyType.TEXT, FEISHU_AI_REPLY_TITLE, content, response, )