feat(search): 添加AI推荐功能并优化相关逻辑

This commit is contained in:
PKC278
2026-01-15 02:49:29 +08:00
parent 91354295f2
commit 95f2ac3811
10 changed files with 543 additions and 72 deletions

View File

@@ -1,14 +1,16 @@
from typing import List, Any, Optional
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Body
from app import schemas
from app.chain.media import MediaChain
from app.chain.search import SearchChain
from app.chain.ai_recommend import AIRecommendChain
from app.core.config import settings
from app.core.event import eventmanager
from app.core.metainfo import MetaInfo
from app.core.security import verify_token
from app.log import logger
from app.schemas import MediaRecognizeConvertEventData
from app.schemas.types import MediaType, ChainEventType
@@ -36,6 +38,9 @@ async def search_by_id(mediaid: str,
"""
根据TMDBID/豆瓣ID精确搜索站点资源 tmdb:/douban:/bangumi:
"""
# 取消正在运行的AI推荐会清除数据库缓存
AIRecommendChain().cancel_ai_recommend()
if mtype:
media_type = MediaType(mtype)
else:
@@ -159,6 +164,9 @@ async def search_by_title(keyword: Optional[str] = None,
"""
根据名称模糊搜索站点资源,支持分页,关键词为空是返回首页资源
"""
# 取消正在运行的AI推荐并清除数据库缓存
AIRecommendChain().cancel_ai_recommend()
torrents = await SearchChain().async_search_by_title(
title=keyword, page=page,
sites=[int(site) for site in sites.split(",") if site] if sites else None,
@@ -167,3 +175,82 @@ async def search_by_title(keyword: Optional[str] = None,
if not torrents:
return schemas.Response(success=False, message="未搜索到任何资源")
return schemas.Response(success=True, data=[torrent.to_dict() for torrent in torrents])
@router.post("/recommend", summary="AI推荐资源", response_model=schemas.Response)
async def recommend_search_results(
filtered_indices: Optional[List[int]] = Body(None, embed=True, description="筛选后的索引列表"),
check_only: bool = Body(False, embed=True, description="仅检查状态,不启动新任务"),
force: bool = Body(False, embed=True, description="强制重新推荐,清除旧结果"),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
AI推荐资源 - 轮询接口
前端轮询此接口,发送筛选后的索引(如果有筛选)
后端根据请求变化自动取消旧任务并启动新任务
参数:
- filtered_indices: 筛选后的索引列表(可选,为空或不提供时使用所有结果)
- check_only: 仅检查状态(首次打开页面时使用,避免触发不必要的重新推理)
- force: 强制重新推荐(清除旧结果并重新启动)
返回数据结构:
{
"success": bool,
"message": string, // 错误信息(仅在错误时存在)
"data": {
"status": string, // 状态: disabled | idle | running | completed | error
"results": array // 推荐结果仅status=completed时存在
}
}
"""
# 从缓存获取上次搜索结果
results = await SearchChain().async_last_search_results() or []
if not results:
return schemas.Response(success=False, message="没有可用的搜索结果", data={
"status": "error"
})
recommend_chain = AIRecommendChain()
# 如果是强制模式,先取消并清除旧结果,然后直接启动新任务
if force:
logger.info("收到新推荐请求,清除旧结果并启动新任务")
recommend_chain.cancel_ai_recommend()
recommend_chain.start_recommend_task(filtered_indices, len(results), results)
# 直接返回运行中状态
return schemas.Response(success=True, data={
"status": "running"
})
# 如果是仅检查模式,不传递 filtered_indices避免触发请求变化检测
if check_only:
# 返回当前运行状态,不做任何任务启动或取消操作
current_status = recommend_chain.get_current_status_only()
# 如果有错误将错误信息放到message中
if current_status.get("status") == "error":
error_msg = current_status.pop("error", "未知错误")
return schemas.Response(success=False, message=error_msg, data=current_status)
return schemas.Response(success=True, data=current_status)
# 获取当前状态(会检测请求是否变化)
status_data = recommend_chain.get_status(filtered_indices, len(results))
# 如果功能未启用,直接返回禁用状态
if status_data.get("status") == "disabled":
return schemas.Response(success=True, data=status_data)
# 如果是空闲状态,启动新任务
if status_data["status"] == "idle":
recommend_chain.start_recommend_task(filtered_indices, len(results), results)
# 立即返回运行中状态
return schemas.Response(success=True, data={
"status": "running"
})
# 如果有错误将错误信息放到message中
if status_data.get("status") == "error":
error_msg = status_data.pop("error", "未知错误")
return schemas.Response(success=False, message=error_msg, data=status_data)
# 返回当前状态
return schemas.Response(success=True, data=status_data)

View File

@@ -130,18 +130,40 @@ async def cache_img(
def get_global_setting(token: str):
"""
查询非敏感系统设置(默认鉴权)
仅包含登录前UI初始化必需的字段
"""
if token != "moviepilot":
raise HTTPException(status_code=403, detail="Forbidden")
# 白名单模式,仅包含前端业务逻辑必需的字段
# 白名单模式,仅包含登录前UI初始化必需的字段
info = settings.model_dump(
include={
"TMDB_IMAGE_DOMAIN",
"GLOBAL_IMAGE_CACHE",
"ADVANCED_MODE",
}
)
# 追加版本信息(用于版本检查)
info.update({
"FRONTEND_VERSION": SystemChain.get_frontend_version(),
"BACKEND_VERSION": APP_VERSION
})
return schemas.Response(success=True,
data=info)
@router.get("/global/user", summary="查询用户相关系统设置", response_model=schemas.Response)
async def get_user_global_setting(_: User = Depends(get_current_active_user_async)):
"""
查询用户相关系统设置(登录后获取)
包含业务功能相关的配置和用户权限信息
"""
# 业务功能相关的配置字段
info = settings.model_dump(
include={
"RECOGNIZE_SOURCE",
"SEARCH_SOURCE"
"SEARCH_SOURCE",
"AI_RECOMMEND_ENABLED"
}
)
# 追加用户唯一ID和订阅分享管理权限
@@ -150,8 +172,6 @@ def get_global_setting(token: str):
"USER_UNIQUE_ID": SubscribeHelper().get_user_uuid(),
"SUBSCRIBE_SHARE_MANAGE": share_admin,
"WORKFLOW_SHARE_MANAGE": share_admin,
"FRONTEND_VERSION": SystemChain.get_frontend_version(),
"BACKEND_VERSION": APP_VERSION
})
return schemas.Response(success=True,
data=info)