"""
小红书相关数据模型
定义小红书笔记、用户、搜索结果等数据结构
"""
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, field_validator
# 尝试相对导入,失败则使用绝对导入
try:
from ..utils.text_utils import validate_note_content, parse_topics_string, parse_file_paths_string, smart_parse_file_paths
except ImportError:
from src.utils.text_utils import validate_note_content, parse_topics_string, parse_file_paths_string, smart_parse_file_paths
class XHSNote(BaseModel):
"""小红书笔记数据模型"""
title: str
content: str
images: Optional[List[str]] = None
videos: Optional[List[str]] = None
topics: Optional[List[str]] = None
location: Optional[str] = None
@field_validator('title')
@classmethod
def validate_title(cls, v):
"""验证标题"""
if not v or not v.strip():
raise ValueError("标题不能为空")
if len(v.strip()) > 50:
raise ValueError("标题长度不能超过50个字符")
return v.strip()
@field_validator('content')
@classmethod
def validate_content(cls, v):
"""验证内容"""
if not v or not v.strip():
raise ValueError("内容不能为空")
if len(v.strip()) > 1000:
raise ValueError("内容长度不能超过1000个字符")
return v.strip()
@field_validator('images')
@classmethod
def validate_images(cls, v):
"""验证图片列表"""
if v is None:
return v
# 限制图片数量
if len(v) > 9:
raise ValueError("图片数量不能超过9张")
# 检查路径格式
import os
for image_path in v:
if not os.path.isabs(image_path):
raise ValueError(f"图片路径必须是绝对路径: {image_path}")
if not os.path.exists(image_path):
raise ValueError(f"图片文件不存在: {image_path}")
return v
@field_validator('videos')
@classmethod
def validate_videos(cls, v):
"""验证视频列表"""
if v is None:
return v
# 小红书只支持单个视频
if len(v) > 1:
raise ValueError("小红书只支持发布1个视频文件")
# 检查路径格式和文件存在性
import os
for video_path in v:
if not os.path.isabs(video_path):
raise ValueError(f"视频路径必须是绝对路径: {video_path}")
if not os.path.exists(video_path):
raise ValueError(f"视频文件不存在: {video_path}")
# 检查文件扩展名
valid_extensions = ['.mp4', '.mov', '.avi', '.mkv', '.flv', '.wmv', '.m4v']
_, ext = os.path.splitext(video_path.lower())
if ext not in valid_extensions:
raise ValueError(f"不支持的视频格式: {ext},支持的格式: {valid_extensions}")
return v
@field_validator('topics')
@classmethod
def validate_topics(cls, v):
"""验证话题列表"""
if v is None:
return v
# 限制话题数量
if len(v) > 10:
raise ValueError("话题数量不能超过10个")
# 检查话题长度
for topic in v:
if len(topic) > 20:
raise ValueError(f"话题长度不能超过20个字符: {topic}")
return v
@field_validator('images', 'videos')
@classmethod
def validate_media_conflict(cls, v, info):
"""验证图片和视频不能同时存在"""
# 在after模式下验证,确保能访问到其他字段
return v
def __init__(self, **data):
"""初始化时验证图片和视频冲突"""
super().__init__(**data)
# 检查图片和视频不能同时存在
if self.images and self.videos:
raise ValueError("不能同时上传图片和视频,请选择其中一种")
# 至少需要图片或视频中的一种
if not self.images and not self.videos:
raise ValueError("必须上传至少1张图片或1个视频")
@classmethod
def from_strings(cls, title: str, content: str, topics_str: str = "",
location: str = "", images_str: str = "", videos_str: str = "") -> 'XHSNote':
"""
从字符串参数创建笔记对象
Args:
title: 笔记标题
content: 笔记内容
topics_str: 话题字符串(逗号分隔)
location: 位置信息
images_str: 图片路径字符串(支持多种格式:逗号分隔、数组字符串、JSON数组等)
videos_str: 视频路径字符串(支持多种格式:逗号分隔、数组字符串、JSON数组等)
Returns:
XHSNote实例
"""
topic_list = parse_topics_string(topics_str) if topics_str else None
image_list = smart_parse_file_paths(images_str) if images_str else None
video_list = smart_parse_file_paths(videos_str) if videos_str else None
return cls(
title=title,
content=content,
images=image_list,
videos=video_list,
topics=topic_list,
location=location if location else None
)
@classmethod
def smart_create(cls, title: str, content: str, topics=None,
location: str = "", images=None, videos=None) -> 'XHSNote':
"""
智能创建笔记对象,支持多种输入格式
Args:
title: 笔记标题
content: 笔记内容
topics: 话题(支持字符串、列表等多种格式)
location: 位置信息
images: 图片路径(支持字符串、列表、JSON等多种格式)
videos: 视频路径(支持字符串、列表、JSON等多种格式)
Returns:
XHSNote实例
"""
# 智能解析话题
if topics:
if isinstance(topics, str):
topic_list = parse_topics_string(topics)
elif isinstance(topics, (list, tuple)):
topic_list = [str(topic).strip() for topic in topics if str(topic).strip()]
else:
topic_list = parse_topics_string(str(topics))
else:
topic_list = None
# 智能解析图片路径
image_list = smart_parse_file_paths(images) if images else None
# 智能解析视频路径
video_list = smart_parse_file_paths(videos) if videos else None
return cls(
title=title,
content=content,
images=image_list,
videos=video_list,
topics=topic_list,
location=location if location else None
)
@classmethod
async def async_smart_create(cls, title: str, content: str, topics=None,
location: str = "", images=None, videos=None) -> 'XHSNote':
"""
异步智能创建笔记对象,支持多种输入格式(包括URL)
Args:
title: 笔记标题
content: 笔记内容
topics: 话题(支持字符串、列表等多种格式)
location: 位置信息
images: 图片,支持格式:
- 本地路径:"image.jpg" 或 ["/path/to/image.jpg"]
- 网络地址:"https://example.com/image.jpg"
- 混合数组:["local.jpg", "https://example.com/img.jpg"]
videos: 视频路径(目前仅支持本地文件)
Returns:
XHSNote实例
"""
# 智能解析话题
if topics:
if isinstance(topics, str):
topic_list = parse_topics_string(topics)
elif isinstance(topics, (list, tuple)):
topic_list = [str(topic).strip() for topic in topics if str(topic).strip()]
else:
topic_list = parse_topics_string(str(topics))
else:
topic_list = None
# 处理图片(支持URL)
processed_images = None
if images:
from ..utils.image_processor import ImageProcessor
processor = ImageProcessor()
processed_images = await processor.process_images(images)
# 智能解析视频路径(暂时只支持本地文件)
video_list = smart_parse_file_paths(videos) if videos else None
return cls(
title=title,
content=content,
images=processed_images,
videos=video_list,
topics=topic_list,
location=location if location else None
)
class XHSSearchResult(BaseModel):
"""搜索结果数据模型"""
note_id: str
title: str
author: str
likes: int
url: str
thumbnail: Optional[str] = None
@field_validator('note_id')
@classmethod
def validate_note_id(cls, v):
"""验证笔记ID"""
if not v or not v.strip():
raise ValueError("笔记ID不能为空")
return v.strip()
@field_validator('likes')
@classmethod
def validate_likes(cls, v):
"""验证点赞数"""
if v < 0:
raise ValueError("点赞数不能为负数")
return v
class XHSUser(BaseModel):
"""小红书用户数据模型"""
user_id: Optional[str] = None
nickname: Optional[str] = None
avatar: Optional[str] = None
followers: Optional[int] = None
following: Optional[int] = None
notes_count: Optional[int] = None
@field_validator('followers', 'following', 'notes_count')
@classmethod
def validate_counts(cls, v):
"""验证计数字段"""
if v is not None and v < 0:
raise ValueError("计数不能为负数")
return v
class XHSPublishResult(BaseModel):
"""发布结果数据模型"""
success: bool
message: str
note_title: Optional[str] = None
final_url: Optional[str] = None
error_type: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""转换为字典格式"""
return {
"success": self.success,
"message": self.message,
"note_title": self.note_title,
"final_url": self.final_url,
"error_type": self.error_type
}
class CookieInfo(BaseModel):
"""Cookie信息数据模型"""
name: str
value: str
domain: str
path: str = "/"
secure: bool = False
expiry: Optional[int] = None
@field_validator('name', 'value', 'domain')
@classmethod
def validate_required_fields(cls, v):
"""验证必需字段"""
if not v or not v.strip():
raise ValueError("Cookie的name、value、domain字段不能为空")
return v.strip()
class CookiesData(BaseModel):
"""Cookies数据容器"""
cookies: List[CookieInfo]
saved_at: str
domain: str = "creator.xiaohongshu.com"
critical_cookies_found: List[str] = []
version: str = "2.0"
@field_validator('cookies')
@classmethod
def validate_cookies_list(cls, v):
"""验证cookies列表"""
if not v:
raise ValueError("cookies列表不能为空")
return v
def get_critical_cookies(self) -> List[str]:
"""获取关键cookies列表"""
critical_names = [
'web_session', 'a1', 'gid', 'webId',
'customer-sso-sid', 'x-user-id-creator.xiaohongshu.com',
'access-token-creator.xiaohongshu.com', 'galaxy_creator_session_id',
'galaxy.creator.beaker.session.id'
]
found_critical = []
for cookie in self.cookies:
if cookie.name in critical_names:
found_critical.append(cookie.name)
return found_critical
def is_valid(self) -> bool:
"""检查cookies是否有效"""
critical_cookies = self.get_critical_cookies()
# 至少需要前4个基础cookies中的3个
required_basic = ['web_session', 'a1', 'gid', 'webId']
found_basic = [name for name in critical_cookies if name in required_basic]
return len(found_basic) >= 3
# 创作者中心关键cookies
CRITICAL_CREATOR_COOKIES = [
'web_session', 'a1', 'gid', 'webId',
'customer-sso-sid', 'x-user-id-creator.xiaohongshu.com',
'access-token-creator.xiaohongshu.com', 'galaxy_creator_session_id',
'galaxy.creator.beaker.session.id'
]