409 lines
14 KiB
Python
409 lines
14 KiB
Python
"""
|
||
互动执行服务
|
||
"""
|
||
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)
|