""" 互动执行服务 """ import logging import random from typing import Optional, List, Dict, Any from datetime import datetime, timedelta from sqlalchemy.orm import Session from sqlalchemy import and_, func from app.models.virtual_user import VirtualUser, ActivityLevel, UserStatus from app.models.interaction import InteractionRecord, InteractionType, InteractionStatus from app.models.token_usage import TokenUsage from app.models.news_cache import NewsCache from app.services.huihui_api_service import huihui_api_service from app.services.ai_service import ai_service from app.core.config import settings logger = logging.getLogger(__name__) class InteractionService: """互动执行服务类""" def __init__(self, db: Session): self.db = db async def execute_interaction( self, virtual_user_id: int, interaction_type: Optional[InteractionType] = None, news_id: Optional[str] = None ) -> Optional[InteractionRecord]: """ 执行单次互动 :param virtual_user_id: 虚拟用户 ID :param interaction_type: 互动类型(不传则随机) :param news_id: 新闻 ID(不传则随机选择) :return: 互动记录 """ # 获取虚拟用户 user = self.db.query(VirtualUser).filter(VirtualUser.id == virtual_user_id).first() if not user: logger.error(f"Virtual user not found: {virtual_user_id}") return None # 检查用户状态 if user.status != UserStatus.ACTIVE: logger.warning(f"Virtual user is not active: {virtual_user_id}") return None # 检查是否已登录 if not user.is_logged_in or not user.session_token: logger.warning(f"Virtual user not logged in: {virtual_user_id}") # TODO: 自动登录 return None # 检查今日限额 if not self._check_daily_limit(user, interaction_type): logger.warning(f"Daily limit reached for user {virtual_user_id}") return None # 选择新闻 if not news_id: news_id = await self._select_news(user) if not news_id: logger.warning("No news available for interaction") return None # 获取新闻详情 news = self.db.query(NewsCache).filter(NewsCache.news_id == news_id).first() if not news: # 从 API 获取 news_detail = await huihui_api_service.get_news_detail(news_id) if news_detail: news = self._cache_news(news_detail) if not news: logger.error(f"Cannot get news detail: {news_id}") return None # 确定互动类型 if not interaction_type: interaction_type = self._random_interaction_type() # 创建互动记录 record = InteractionRecord( virtual_user_id=virtual_user_id, news_id=news_id, news_title=news.title if news else "", interaction_type=interaction_type, status=InteractionStatus.PENDING ) self.db.add(record) self.db.commit() self.db.refresh(record) try: # 执行互动 if interaction_type == InteractionType.COMMENT: result = await self._execute_comment(user, news, record) elif interaction_type == InteractionType.REPLY: result = await self._execute_reply(user, news, record) elif interaction_type == InteractionType.LIKE: result = await self._execute_like(user, news, record) elif interaction_type == InteractionType.FAVORITE: result = await self._execute_favorite(user, news, record) elif interaction_type == InteractionType.SHARE: result = await self._execute_share(user, news, record) else: logger.error(f"Unknown interaction type: {interaction_type}") return None if result: record.status = InteractionStatus.SUCCESS record.api_response = str(result) # 更新用户统计 user.total_interactions += 1 if interaction_type == InteractionType.COMMENT: user.today_comments += 1 elif interaction_type == InteractionType.REPLY: user.today_replies += 1 user.last_interaction_time = datetime.now() self.db.commit() logger.info(f"Interaction executed successfully: user={user.id}, type={interaction_type}") return record else: record.status = InteractionStatus.FAILED record.error_message = "API call failed" self.db.commit() return None except Exception as e: logger.error(f"Execute interaction error: {e}") record.status = InteractionStatus.FAILED record.error_message = str(e) self.db.commit() return None async def _execute_comment( self, user: VirtualUser, news: NewsCache, record: InteractionRecord ) -> Optional[Dict[str, Any]]: """执行评论""" # AI 生成评论内容 ai_result = await ai_service.generate_comment( news_content=news.content or news.summary or news.title, writing_style=user.writing_style or "普通", persona_description=user.persona_description ) if not ai_result or not ai_result.get("content"): logger.error("AI generate comment failed") return None # 记录 Token 使用 self._record_token_usage( virtual_user_id=user.id, interaction_id=record.id, tokens_used=ai_result.get("tokens_used", 0), ai_model=ai_result.get("model", "unknown"), action_type="generate_comment", tokens_prompt=ai_result.get("tokens_prompt", 0), tokens_completion=ai_result.get("tokens_completion", 0) ) # 调用接口提交评论 result = await huihui_api_service.create_comment( news_id=news.news_id, content=ai_result["content"], session_token=user.session_token ) if result: record.content = ai_result["content"] record.tokens_used = ai_result.get("tokens_used", 0) record.ai_model_used = ai_result.get("model") return result async def _execute_reply( self, user: VirtualUser, news: NewsCache, record: InteractionRecord ) -> Optional[Dict[str, Any]]: """执行回复""" # 获取评论列表 comments = await huihui_api_service.get_comments(news_id=news.news_id) if not comments or len(comments) == 0: logger.warning(f"No comments available for news {news.news_id}") return None # 随机选择一条评论进行回复 target_comment = random.choice(comments) record.target_comment_id = target_comment.get("id") # AI 生成回复内容 ai_result = await ai_service.generate_reply( original_comment=target_comment.get("content", ""), news_content=news.content or news.summary or news.title, writing_style=user.writing_style or "普通", persona_description=user.persona_description ) if not ai_result or not ai_result.get("content"): logger.error("AI generate reply failed") return None # 记录 Token 使用 self._record_token_usage( virtual_user_id=user.id, interaction_id=record.id, tokens_used=ai_result.get("tokens_used", 0), ai_model=ai_result.get("model", "unknown"), action_type="generate_reply" ) # 调用接口提交回复 result = await huihui_api_service.create_reply( comment_id=target_comment.get("id"), content=ai_result["content"], session_token=user.session_token ) if result: record.content = ai_result["content"] record.tokens_used = ai_result.get("tokens_used", 0) return result async def _execute_like( self, user: VirtualUser, news: NewsCache, record: InteractionRecord ) -> Optional[Dict[str, Any]]: """执行点赞""" # 获取评论列表 comments = await huihui_api_service.get_comments(news_id=news.news_id) if not comments or len(comments) == 0: logger.warning(f"No comments available for like") return None # 随机选择一条评论点赞 target_comment = random.choice(comments) record.target_comment_id = target_comment.get("id") result = await huihui_api_service.like_comment( comment_id=target_comment.get("id"), session_token=user.session_token ) return result async def _execute_favorite( self, user: VirtualUser, news: NewsCache, record: InteractionRecord ) -> Optional[Dict[str, Any]]: """执行收藏""" result = await huihui_api_service.favorite_news( news_id=news.news_id, session_token=user.session_token ) return result async def _execute_share( self, user: VirtualUser, news: NewsCache, record: InteractionRecord ) -> Optional[Dict[str, Any]]: """执行转发""" result = await huihui_api_service.share_news( news_id=news.news_id, session_token=user.session_token ) return result def _check_daily_limit( self, user: VirtualUser, interaction_type: Optional[InteractionType] ) -> bool: """检查每日限额""" today = datetime.now().date() # 统计今日互动 today_records = self.db.query(InteractionRecord).filter( and_( InteractionRecord.virtual_user_id == user.id, func.date(InteractionRecord.execution_time) == today, InteractionRecord.status == InteractionStatus.SUCCESS ) ).all() today_comments = sum(1 for r in today_records if r.interaction_type == InteractionType.COMMENT) today_replies = sum(1 for r in today_records if r.interaction_type == InteractionType.REPLY) # 检查评论限额 if interaction_type == InteractionType.COMMENT: if today_comments >= settings.MAX_COMMENTS_PER_USER_PER_DAY: return False # 检查回复限额 if interaction_type == InteractionType.REPLY: if today_replies >= settings.MAX_REPLIES_PER_USER_PER_DAY: return False return True def _random_interaction_type(self) -> InteractionType: """随机选择互动类型""" rand = random.random() # 根据概率决定互动类型 if rand < settings.LIKE_PROBABILITY: return InteractionType.LIKE elif rand < settings.LIKE_PROBABILITY + settings.FAVORITE_PROBABILITY: return InteractionType.FAVORITE elif rand < settings.LIKE_PROBABILITY + settings.FAVORITE_PROBABILITY + settings.SHARE_PROBABILITY: return InteractionType.SHARE else: return InteractionType.COMMENT async def _select_news(self, user: VirtualUser) -> Optional[str]: """选择新闻""" # 优先选择未互动过的新闻 cached_news = self.db.query(NewsCache).order_by( NewsCache.created_at.desc() ).limit(50).all() if not cached_news: # 从 API 获取 news_list = await huihui_api_service.get_news_list(page=1, page_size=20) if news_list: for news_data in news_list: self._cache_news(news_data) cached_news = self.db.query(NewsCache).order_by( NewsCache.created_at.desc() ).limit(50).all() if not cached_news: return None # 随机选择一篇 return random.choice(cached_news).news_id def _cache_news(self, news_data: Dict[str, Any]) -> Optional[NewsCache]: """缓存新闻""" news = NewsCache( news_id=str(news_data.get("id")), title=news_data.get("title", ""), summary=news_data.get("summary", ""), content=news_data.get("content", ""), source=news_data.get("source", ""), author=news_data.get("author", ""), category=news_data.get("category", "") ) self.db.add(news) self.db.commit() self.db.refresh(news) return news def _record_token_usage( self, virtual_user_id: int, interaction_id: int, tokens_used: int, ai_model: str, action_type: str, tokens_prompt: int = 0, tokens_completion: int = 0 ): """记录 Token 使用""" usage = TokenUsage( virtual_user_id=virtual_user_id, interaction_id=interaction_id, tokens_used=tokens_used, tokens_prompt=tokens_prompt, tokens_completion=tokens_completion, ai_model=ai_model, action_type=action_type ) self.db.add(usage) self.db.commit() logger.info(f"Token usage recorded: {tokens_used} tokens for user {virtual_user_id}") # 工厂函数 def get_interaction_service(db: Session) -> InteractionService: """获取互动服务实例""" return InteractionService(db)