import re from typing import Any from fastapi import HTTPException from sqlalchemy.orm import Session from app.application.feishu.delivery import send_text_if_configured from app.application.feishu.results import command_result from app.core.config import get_settings from app.modules.feishu.constants import FeishuCommandName, FeishuReplyType from app.modules.feishu.service import FeishuService from app.modules.market.chart import render_market_chart from app.modules.market.service import MarketService STOCK_ANALYSIS_PATTERN = re.compile( r"^(?:股票分析|估值分析|财报分析)\s+([0-9]{6}(?:\.(?:SH|SZ|BJ))?)$", re.IGNORECASE ) WATCHLIST_ADD_PATTERN = re.compile(r"^加入自选\s+([0-9]{6}(?:\.(?:SH|SZ|BJ))?)$", re.IGNORECASE) MARKET_COMMANDS = { "市场分析", "今日收盘分析", "本周市场分析", "宏观金融分析", "最新公告", } INDUSTRY_ANALYSIS_PATTERN = re.compile(r"^行业分析\s+(.+)$") STOCK_COMPARE_PATTERN = re.compile( r"^股票对比\s+([0-9]{6}(?:\.(?:SH|SZ|BJ))?)\s+" r"([0-9]{6}(?:\.(?:SH|SZ|BJ))?)$", re.IGNORECASE, ) def handle_market_command( db: Session, feishu: FeishuService, text: str, chat_id: str | None, actor: str, auto_reply: bool, ) -> dict[str, Any] | None: """Handle market analysis and watchlist commands.""" stock = STOCK_ANALYSIS_PATTERN.fullmatch(text) add = WATCHLIST_ADD_PATTERN.fullmatch(text) industry = INDUSTRY_ANALYSIS_PATTERN.fullmatch(text) comparison = STOCK_COMPARE_PATTERN.fullmatch(text) if ( text not in MARKET_COMMANDS and text != "查看自选" and not stock and not add and not industry and not comparison ): return None if not get_settings().market_analysis_enabled: return _text_result( feishu, FeishuCommandName.MARKET_OVERVIEW, "市场分析", "市场分析尚未启用,请配置市场数据源后启用。", chat_id, actor, auto_reply, ) service = MarketService(db) if add: if get_settings().read_only_mode: return _text_result( feishu, FeishuCommandName.WATCHLIST_ADD, "自选股", "当前为只读模式,不能修改自选股。请由管理员启用操作后重试。", chat_id, actor, auto_reply, ) item = service.add_watchlist(actor, add.group(1)) return _text_result( feishu, FeishuCommandName.WATCHLIST_ADD, "自选股", f"已加入自选:{item['symbol']}", chat_id, actor, auto_reply, ) if text == "查看自选": items = service.watchlist(actor) content = "自选股:" + ("、".join(item["symbol"] for item in items) or "暂无") return _text_result( feishu, FeishuCommandName.WATCHLIST_LIST, "自选股", content, chat_id, actor, auto_reply, ) if text == "最新公告": items = service.announcements(limit=10)["items"] content = ( "最新公告:\n" + "\n".join( f"- {item['announcement_date']} {item['symbol'] or '市场'}:{item['title']}" for item in items ) if items else "公告元数据尚未接入。" ) return _text_result( feishu, FeishuCommandName.MARKET_ANNOUNCEMENTS, "最新公告", content, chat_id, actor, auto_reply, ) if industry: try: data = service.industry_analysis(industry.group(1).strip()) content = ( f"{data['industry']} 平均涨跌 {data['average_pct_change']}%\n" + "\n".join( f"- {item['name']}({item['symbol']}):{item['pct_change']}%" for item in data["items"][:10] ) ) except HTTPException: content = "未找到该行业的最新市场数据。" return _text_result( feishu, FeishuCommandName.MARKET_OVERVIEW, "行业分析", content, chat_id, actor, auto_reply, ) if comparison: try: content = service.compare_stocks([comparison.group(1), comparison.group(2)])["content"] except HTTPException: content = "至少一只股票缺少可用行情,暂时无法比较。" return _text_result( feishu, FeishuCommandName.STOCK_ANALYSIS, "股票对比", content, chat_id, actor, auto_reply, ) return _analysis_result(service, feishu, text, stock.group(1) if stock else None, chat_id, actor, auto_reply) def _analysis_result( service: MarketService, feishu: FeishuService, text: str, symbol: str | None, chat_id: str | None, actor: str, auto_reply: bool, ) -> dict[str, Any]: command = FeishuCommandName.STOCK_ANALYSIS if symbol else FeishuCommandName.MARKET_OVERVIEW if text == "宏观金融分析": command = FeishuCommandName.MARKET_MACRO try: if symbol: report = service.stock_analysis(symbol, True, actor) elif text == "本周市场分析": report = service.weekly_overview(include_ai=True, actor=actor) elif text == "宏观金融分析": report = service.macro_analysis(include_ai=True, actor=actor) else: report = service.market_overview(include_ai=True, actor=actor) except HTTPException: return _text_result( feishu, command, "股票分析", "未找到该股票的可用行情,请确认代码或先执行行情同步。", chat_id, actor, auto_reply, ) ai = report.get("ai_analysis") or {} if not report.get("data_available"): content = "市场数据未接入,暂不生成分析报告。" elif not ai.get("ok"): content = "AI 当前不可用,本次市场分析报告未发送。" else: content = report["content"] response = _send_analysis(feishu, text, report, content, chat_id, actor) if auto_reply else None return command_result( command, FeishuReplyType.CARD if ai.get("ok") and text != "宏观金融分析" else FeishuReplyType.TEXT, report["title"], content, response, report["lines"] if ai.get("ok") else None, ) def _send_analysis( feishu: FeishuService, text: str, report: dict[str, Any], content: str, chat_id: str | None, actor: str, ) -> dict[str, Any] | None: ai = report.get("ai_analysis") or {} if not ai.get("ok") or text == "宏观金融分析": return send_text_if_configured(feishu, chat_id, content, actor) image = feishu.upload_image(render_market_chart(report), actor) image_key = (image.get("data") or {}).get("image_key") if not image_key: raise ValueError("Feishu image upload did not return image_key") card = FeishuService.build_basic_card( report["title"], report["lines"], image_key=image_key, image_alt=report["title"], ) return feishu.send_card(card, receive_id=chat_id, actor=actor) def _text_result( feishu: FeishuService, command: FeishuCommandName, title: str, content: str, chat_id: str | None, actor: str, auto_reply: bool, ) -> dict[str, Any]: response = send_text_if_configured(feishu, chat_id, content, actor) if auto_reply else None return command_result(command, FeishuReplyType.TEXT, title, content, response)