base.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  1. # -*- coding: utf-8 -*-
  2. """
  3. 平台发布基类
  4. 提供通用的发布接口和工具方法
  5. """
  6. import asyncio
  7. import json
  8. import os
  9. from abc import ABC, abstractmethod
  10. from dataclasses import dataclass, field
  11. from datetime import datetime
  12. from typing import List, Optional, Callable, Dict, Any
  13. from playwright.async_api import async_playwright, Browser, BrowserContext, Page
  14. @dataclass
  15. class PublishParams:
  16. """发布参数"""
  17. title: str
  18. video_path: str
  19. description: str = ""
  20. cover_path: Optional[str] = None
  21. tags: List[str] = field(default_factory=list)
  22. publish_date: Optional[datetime] = None
  23. location: str = "重庆市"
  24. def __post_init__(self):
  25. if not self.description:
  26. self.description = self.title
  27. @dataclass
  28. class PublishResult:
  29. """发布结果"""
  30. success: bool
  31. platform: str
  32. video_id: str = ""
  33. video_url: str = ""
  34. message: str = ""
  35. error: str = ""
  36. need_captcha: bool = False # 是否需要验证码
  37. captcha_type: str = "" # 验证码类型: phone, slider, image
  38. screenshot_base64: str = "" # 页面截图(Base64)
  39. page_url: str = "" # 当前页面 URL
  40. status: str = "" # 状态: uploading, processing, success, failed, need_captcha, need_action
  41. @dataclass
  42. class WorkItem:
  43. """作品数据"""
  44. work_id: str
  45. title: str
  46. cover_url: str = ""
  47. video_url: str = ""
  48. duration: int = 0 # 秒
  49. status: str = "published" # published, reviewing, rejected, draft
  50. publish_time: str = ""
  51. play_count: int = 0
  52. like_count: int = 0
  53. comment_count: int = 0
  54. share_count: int = 0
  55. collect_count: int = 0
  56. def to_dict(self) -> Dict[str, Any]:
  57. return {
  58. "work_id": self.work_id,
  59. "title": self.title,
  60. "cover_url": self.cover_url,
  61. "video_url": self.video_url,
  62. "duration": self.duration,
  63. "status": self.status,
  64. "publish_time": self.publish_time,
  65. "play_count": self.play_count,
  66. "like_count": self.like_count,
  67. "comment_count": self.comment_count,
  68. "share_count": self.share_count,
  69. "collect_count": self.collect_count,
  70. }
  71. @dataclass
  72. class CommentItem:
  73. """评论数据"""
  74. comment_id: str
  75. work_id: str
  76. content: str
  77. author_id: str = ""
  78. author_name: str = ""
  79. author_avatar: str = ""
  80. like_count: int = 0
  81. reply_count: int = 0
  82. create_time: str = ""
  83. is_author: bool = False # 是否是作者的评论
  84. replies: List['CommentItem'] = field(default_factory=list)
  85. def to_dict(self) -> Dict[str, Any]:
  86. return {
  87. "comment_id": self.comment_id,
  88. "work_id": self.work_id,
  89. "content": self.content,
  90. "author_id": self.author_id,
  91. "author_name": self.author_name,
  92. "author_avatar": self.author_avatar,
  93. "like_count": self.like_count,
  94. "reply_count": self.reply_count,
  95. "create_time": self.create_time,
  96. "is_author": self.is_author,
  97. "replies": [r.to_dict() for r in self.replies],
  98. }
  99. @dataclass
  100. class WorksResult:
  101. """作品列表结果"""
  102. success: bool
  103. platform: str
  104. works: List[WorkItem] = field(default_factory=list)
  105. total: int = 0
  106. has_more: bool = False
  107. error: str = ""
  108. def to_dict(self) -> Dict[str, Any]:
  109. return {
  110. "success": self.success,
  111. "platform": self.platform,
  112. "works": [w.to_dict() for w in self.works],
  113. "total": self.total,
  114. "has_more": self.has_more,
  115. "error": self.error,
  116. }
  117. @dataclass
  118. class CommentsResult:
  119. """评论列表结果"""
  120. success: bool
  121. platform: str
  122. work_id: str
  123. comments: List[CommentItem] = field(default_factory=list)
  124. total: int = 0
  125. has_more: bool = False
  126. error: str = ""
  127. def to_dict(self) -> Dict[str, Any]:
  128. return {
  129. "success": self.success,
  130. "platform": self.platform,
  131. "work_id": self.work_id,
  132. "comments": [c.to_dict() for c in self.comments],
  133. "total": self.total,
  134. "has_more": self.has_more,
  135. "error": self.error,
  136. }
  137. class BasePublisher(ABC):
  138. """
  139. 平台发布基类
  140. 所有平台发布器都需要继承此类
  141. """
  142. platform_name: str = "base"
  143. login_url: str = ""
  144. publish_url: str = ""
  145. cookie_domain: str = ""
  146. def __init__(self, headless: bool = True):
  147. self.headless = headless
  148. self.browser: Optional[Browser] = None
  149. self.context: Optional[BrowserContext] = None
  150. self.page: Optional[Page] = None
  151. self.on_progress: Optional[Callable[[int, str], None]] = None
  152. def set_progress_callback(self, callback: Callable[[int, str], None]):
  153. """设置进度回调"""
  154. self.on_progress = callback
  155. def report_progress(self, progress: int, message: str):
  156. """报告进度"""
  157. print(f"[{self.platform_name}] [{progress}%] {message}")
  158. if self.on_progress:
  159. self.on_progress(progress, message)
  160. @staticmethod
  161. def parse_cookies(cookies_str: str) -> list:
  162. """解析 cookie 字符串为列表"""
  163. try:
  164. cookies = json.loads(cookies_str)
  165. if isinstance(cookies, list):
  166. return cookies
  167. except json.JSONDecodeError:
  168. pass
  169. # 字符串格式: name=value; name2=value2
  170. cookies = []
  171. for item in cookies_str.split(';'):
  172. item = item.strip()
  173. if '=' in item:
  174. name, value = item.split('=', 1)
  175. cookies.append({
  176. 'name': name.strip(),
  177. 'value': value.strip(),
  178. 'domain': '',
  179. 'path': '/'
  180. })
  181. return cookies
  182. @staticmethod
  183. def cookies_to_string(cookies: list) -> str:
  184. """将 cookie 列表转换为字符串"""
  185. return '; '.join([f"{c['name']}={c['value']}" for c in cookies])
  186. async def init_browser(self, storage_state: str = None):
  187. """初始化浏览器"""
  188. print(f"[{self.platform_name}] init_browser: headless={self.headless}", flush=True)
  189. playwright = await async_playwright().start()
  190. self.browser = await playwright.chromium.launch(headless=self.headless)
  191. if storage_state and os.path.exists(storage_state):
  192. self.context = await self.browser.new_context(storage_state=storage_state)
  193. else:
  194. self.context = await self.browser.new_context()
  195. self.page = await self.context.new_page()
  196. return self.page
  197. async def set_cookies(self, cookies: list):
  198. """设置 cookies"""
  199. if not self.context:
  200. raise Exception("Browser context not initialized")
  201. # 设置默认域名
  202. for cookie in cookies:
  203. if 'domain' not in cookie or not cookie['domain']:
  204. cookie['domain'] = self.cookie_domain
  205. await self.context.add_cookies(cookies)
  206. async def close_browser(self):
  207. """关闭浏览器"""
  208. if self.context:
  209. await self.context.close()
  210. if self.browser:
  211. await self.browser.close()
  212. async def save_cookies(self, file_path: str):
  213. """保存 cookies 到文件"""
  214. if self.context:
  215. await self.context.storage_state(path=file_path)
  216. async def capture_screenshot(self) -> str:
  217. """截取当前页面截图,返回 Base64 编码"""
  218. import base64
  219. if not self.page:
  220. return ""
  221. try:
  222. screenshot_bytes = await self.page.screenshot(type="jpeg", quality=80)
  223. return base64.b64encode(screenshot_bytes).decode('utf-8')
  224. except Exception as e:
  225. print(f"[{self.platform_name}] 截图失败: {e}")
  226. return ""
  227. async def ai_check_captcha(self, screenshot_base64: str = None) -> dict:
  228. """
  229. 使用 AI 分析截图检测验证码
  230. Args:
  231. screenshot_base64: 截图的 Base64 编码,如果为空则自动获取当前页面截图
  232. Returns:
  233. dict: {
  234. "has_captcha": bool, # 是否有验证码
  235. "captcha_type": str, # 验证码类型: slider, image, phone, rotate, puzzle
  236. "captcha_description": str, # 验证码描述
  237. "confidence": float, # 置信度 0-100
  238. "need_headful": bool # 是否需要切换到有头浏览器
  239. }
  240. """
  241. import os
  242. import requests
  243. try:
  244. # 获取截图
  245. if not screenshot_base64:
  246. screenshot_base64 = await self.capture_screenshot()
  247. if not screenshot_base64:
  248. print(f"[{self.platform_name}] AI验证码检测: 无法获取截图")
  249. return {
  250. "has_captcha": False,
  251. "captcha_type": "",
  252. "captcha_description": "",
  253. "confidence": 0,
  254. "need_headful": False
  255. }
  256. # 获取 AI 配置
  257. ai_api_key = os.environ.get('DASHSCOPE_API_KEY', '')
  258. ai_base_url = os.environ.get('DASHSCOPE_BASE_URL', 'https://dashscope.aliyuncs.com/compatible-mode/v1')
  259. ai_vision_model = os.environ.get('AI_VISION_MODEL', 'qwen-vl-plus')
  260. if not ai_api_key:
  261. print(f"[{self.platform_name}] AI验证码检测: 未配置 AI API Key,使用传统方式检测")
  262. return await self._traditional_captcha_check()
  263. # 构建 AI 请求
  264. prompt = """请分析这张网页截图,判断页面上是否存在验证码。
  265. 请检查以下类型的验证码:
  266. 1. 滑块验证码(需要滑动滑块到指定位置)
  267. 2. 图片验证码(需要选择正确的图片、点击图片上的文字等)
  268. 3. 旋转验证码(需要旋转图片到正确角度)
  269. 4. 拼图验证码(需要拖动拼图块到正确位置)
  270. 5. 手机验证码(需要输入手机收到的验证码)
  271. 6. 计算验证码(需要输入计算结果)
  272. 请以 JSON 格式返回结果:
  273. ```json
  274. {
  275. "has_captcha": true/false,
  276. "captcha_type": "slider/image/phone/rotate/puzzle/calculate/none",
  277. "captcha_description": "验证码的具体描述",
  278. "confidence": 0-100
  279. }
  280. ```
  281. 注意:
  282. - 如果页面有明显的验证码弹窗或验证区域,has_captcha 为 true
  283. - 如果只是普通的登录页面或表单,没有特殊的验证步骤,has_captcha 为 false
  284. - confidence 表示你对判断结果的信心,100 表示非常确定"""
  285. headers = {
  286. 'Authorization': f'Bearer {ai_api_key}',
  287. 'Content-Type': 'application/json'
  288. }
  289. payload = {
  290. "model": ai_vision_model,
  291. "messages": [
  292. {
  293. "role": "user",
  294. "content": [
  295. {
  296. "type": "image_url",
  297. "image_url": {
  298. "url": f"data:image/jpeg;base64,{screenshot_base64}"
  299. }
  300. },
  301. {
  302. "type": "text",
  303. "text": prompt
  304. }
  305. ]
  306. }
  307. ],
  308. "max_tokens": 500
  309. }
  310. print(f"[{self.platform_name}] AI验证码检测: 正在分析截图...")
  311. response = requests.post(
  312. f"{ai_base_url}/chat/completions",
  313. headers=headers,
  314. json=payload,
  315. timeout=30
  316. )
  317. if response.status_code != 200:
  318. print(f"[{self.platform_name}] AI验证码检测: API 返回错误 {response.status_code}")
  319. return await self._traditional_captcha_check()
  320. result = response.json()
  321. ai_response = result.get('choices', [{}])[0].get('message', {}).get('content', '')
  322. print(f"[{self.platform_name}] AI验证码检测响应: {ai_response[:200]}...")
  323. # 解析 AI 响应
  324. import re
  325. json_match = re.search(r'```json\s*([\s\S]*?)\s*```', ai_response)
  326. if json_match:
  327. json_str = json_match.group(1)
  328. else:
  329. # 尝试直接解析
  330. json_match = re.search(r'\{[\s\S]*\}', ai_response)
  331. if json_match:
  332. json_str = json_match.group(0)
  333. else:
  334. json_str = '{}'
  335. try:
  336. ai_result = json.loads(json_str)
  337. except:
  338. ai_result = {}
  339. has_captcha = ai_result.get('has_captcha', False)
  340. captcha_type = ai_result.get('captcha_type', '')
  341. captcha_description = ai_result.get('captcha_description', '')
  342. confidence = ai_result.get('confidence', 0)
  343. # 如果检测到验证码,需要切换到有头浏览器
  344. need_headful = has_captcha and captcha_type not in ['none', '']
  345. print(f"[{self.platform_name}] AI验证码检测结果: has_captcha={has_captcha}, type={captcha_type}, confidence={confidence}")
  346. return {
  347. "has_captcha": has_captcha,
  348. "captcha_type": captcha_type if captcha_type != 'none' else '',
  349. "captcha_description": captcha_description,
  350. "confidence": confidence,
  351. "need_headful": need_headful
  352. }
  353. except Exception as e:
  354. print(f"[{self.platform_name}] AI验证码检测异常: {e}")
  355. import traceback
  356. traceback.print_exc()
  357. return await self._traditional_captcha_check()
  358. async def _traditional_captcha_check(self) -> dict:
  359. """传统方式检测验证码(基于 DOM 元素)"""
  360. if not self.page:
  361. return {
  362. "has_captcha": False,
  363. "captcha_type": "",
  364. "captcha_description": "",
  365. "confidence": 0,
  366. "need_headful": False
  367. }
  368. try:
  369. # 检查常见的验证码选择器
  370. captcha_selectors = [
  371. # 滑块验证码
  372. ('[class*="slider"]', 'slider', '滑块验证码'),
  373. ('[class*="slide-verify"]', 'slider', '滑块验证码'),
  374. ('text="滑动"', 'slider', '滑块验证码'),
  375. ('text="拖动"', 'slider', '滑块验证码'),
  376. # 图片验证码
  377. ('[class*="captcha"]', 'image', '图片验证码'),
  378. ('[class*="verify-img"]', 'image', '图片验证码'),
  379. ('text="点击"', 'image', '图片验证码'),
  380. ('text="选择"', 'image', '图片验证码'),
  381. # 手机验证码
  382. ('text="验证码"', 'phone', '手机验证码'),
  383. ('text="获取验证码"', 'phone', '手机验证码'),
  384. ('[class*="sms-code"]', 'phone', '手机验证码'),
  385. # 旋转验证码
  386. ('text="旋转"', 'rotate', '旋转验证码'),
  387. ('[class*="rotate"]', 'rotate', '旋转验证码'),
  388. ]
  389. for selector, captcha_type, description in captcha_selectors:
  390. try:
  391. count = await self.page.locator(selector).count()
  392. if count > 0:
  393. # 检查是否可见
  394. element = self.page.locator(selector).first
  395. if await element.is_visible():
  396. print(f"[{self.platform_name}] 传统检测: 发现验证码 - {selector}")
  397. return {
  398. "has_captcha": True,
  399. "captcha_type": captcha_type,
  400. "captcha_description": description,
  401. "confidence": 80,
  402. "need_headful": True
  403. }
  404. except:
  405. pass
  406. return {
  407. "has_captcha": False,
  408. "captcha_type": "",
  409. "captcha_description": "",
  410. "confidence": 80,
  411. "need_headful": False
  412. }
  413. except Exception as e:
  414. print(f"[{self.platform_name}] 传统验证码检测异常: {e}")
  415. return {
  416. "has_captcha": False,
  417. "captcha_type": "",
  418. "captcha_description": "",
  419. "confidence": 0,
  420. "need_headful": False
  421. }
  422. async def get_page_url(self) -> str:
  423. """获取当前页面 URL"""
  424. if not self.page:
  425. return ""
  426. try:
  427. return self.page.url
  428. except:
  429. return ""
  430. async def check_publish_status(self) -> dict:
  431. """
  432. 检查发布状态
  433. 返回: {status, screenshot_base64, page_url, message}
  434. """
  435. if not self.page:
  436. return {"status": "error", "message": "页面未初始化"}
  437. try:
  438. screenshot = await self.capture_screenshot()
  439. page_url = await self.get_page_url()
  440. # 检查常见的成功/失败标志
  441. page_content = await self.page.content()
  442. # 检查成功标志
  443. success_keywords = ['发布成功', '上传成功', '发表成功', '提交成功']
  444. for keyword in success_keywords:
  445. if keyword in page_content:
  446. return {
  447. "status": "success",
  448. "screenshot_base64": screenshot,
  449. "page_url": page_url,
  450. "message": "发布成功"
  451. }
  452. # 检查验证码标志
  453. captcha_keywords = ['验证码', '身份验证', '请完成验证', '滑动验证', '图形验证']
  454. for keyword in captcha_keywords:
  455. if keyword in page_content:
  456. return {
  457. "status": "need_captcha",
  458. "screenshot_base64": screenshot,
  459. "page_url": page_url,
  460. "message": f"检测到{keyword}"
  461. }
  462. # 检查失败标志
  463. fail_keywords = ['发布失败', '上传失败', '提交失败', '操作失败']
  464. for keyword in fail_keywords:
  465. if keyword in page_content:
  466. return {
  467. "status": "failed",
  468. "screenshot_base64": screenshot,
  469. "page_url": page_url,
  470. "message": keyword
  471. }
  472. # 默认返回处理中
  473. return {
  474. "status": "processing",
  475. "screenshot_base64": screenshot,
  476. "page_url": page_url,
  477. "message": "处理中"
  478. }
  479. except Exception as e:
  480. return {
  481. "status": "error",
  482. "screenshot_base64": "",
  483. "page_url": "",
  484. "message": str(e)
  485. }
  486. async def wait_for_upload_complete(self, success_selector: str, timeout: int = 300):
  487. """等待上传完成"""
  488. if not self.page:
  489. raise Exception("Page not initialized")
  490. for _ in range(timeout // 3):
  491. try:
  492. count = await self.page.locator(success_selector).count()
  493. if count > 0:
  494. return True
  495. except:
  496. pass
  497. await asyncio.sleep(3)
  498. self.report_progress(30, "正在上传视频...")
  499. return False
  500. @abstractmethod
  501. async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
  502. """
  503. 发布视频 - 子类必须实现
  504. Args:
  505. cookies: cookie 字符串或 JSON
  506. params: 发布参数
  507. Returns:
  508. PublishResult: 发布结果
  509. """
  510. pass
  511. async def get_works(self, cookies: str, page: int = 0, page_size: int = 20) -> WorksResult:
  512. """
  513. 获取作品列表 - 子类可覆盖实现
  514. Args:
  515. cookies: cookie 字符串或 JSON
  516. page: 页码(从0开始)
  517. page_size: 每页数量
  518. Returns:
  519. WorksResult: 作品列表结果
  520. """
  521. return WorksResult(
  522. success=False,
  523. platform=self.platform_name,
  524. error="该平台暂不支持获取作品列表"
  525. )
  526. async def get_comments(self, cookies: str, work_id: str, cursor: str = "") -> CommentsResult:
  527. """
  528. 获取作品评论 - 子类可覆盖实现
  529. Args:
  530. cookies: cookie 字符串或 JSON
  531. work_id: 作品ID
  532. cursor: 分页游标
  533. Returns:
  534. CommentsResult: 评论列表结果
  535. """
  536. return CommentsResult(
  537. success=False,
  538. platform=self.platform_name,
  539. work_id=work_id,
  540. error="该平台暂不支持获取评论"
  541. )
  542. async def run(self, cookies: str, params: PublishParams) -> PublishResult:
  543. """
  544. 运行发布任务
  545. 包装了 publish 方法,添加了异常处理和资源清理
  546. """
  547. try:
  548. return await self.publish(cookies, params)
  549. except Exception as e:
  550. import traceback
  551. traceback.print_exc()
  552. return PublishResult(
  553. success=False,
  554. platform=self.platform_name,
  555. error=str(e)
  556. )
  557. finally:
  558. await self.close_browser()
  559. async def run_get_works(self, cookies: str, page: int = 0, page_size: int = 20) -> WorksResult:
  560. """
  561. 运行获取作品任务
  562. """
  563. try:
  564. return await self.get_works(cookies, page, page_size)
  565. except Exception as e:
  566. import traceback
  567. traceback.print_exc()
  568. return WorksResult(
  569. success=False,
  570. platform=self.platform_name,
  571. error=str(e)
  572. )
  573. finally:
  574. await self.close_browser()
  575. async def run_get_comments(self, cookies: str, work_id: str, cursor: str = "") -> CommentsResult:
  576. """
  577. 运行获取评论任务
  578. """
  579. try:
  580. return await self.get_comments(cookies, work_id, cursor)
  581. except Exception as e:
  582. import traceback
  583. traceback.print_exc()
  584. return CommentsResult(
  585. success=False,
  586. platform=self.platform_name,
  587. work_id=work_id,
  588. error=str(e)
  589. )
  590. finally:
  591. await self.close_browser()
  592. async def check_login_status(self, cookies: str) -> dict:
  593. """
  594. 检查 Cookie 登录状态(通过浏览器访问后台页面检测)
  595. Args:
  596. cookies: cookie 字符串或 JSON
  597. Returns:
  598. dict: {
  599. "success": True,
  600. "valid": True/False,
  601. "need_login": True/False,
  602. "message": "状态描述"
  603. }
  604. """
  605. try:
  606. await self.init_browser()
  607. cookie_list = self.parse_cookies(cookies)
  608. await self.set_cookies(cookie_list)
  609. if not self.page:
  610. raise Exception("Page not initialized")
  611. # 访问平台后台首页
  612. home_url = self.login_url
  613. print(f"[{self.platform_name}] 访问后台页面: {home_url}")
  614. await self.page.goto(home_url, wait_until='domcontentloaded', timeout=30000)
  615. await asyncio.sleep(3)
  616. # 检查当前 URL 是否被重定向到登录页
  617. current_url = self.page.url
  618. print(f"[{self.platform_name}] 当前 URL: {current_url}")
  619. # 登录页特征
  620. login_indicators = ['login', 'passport', 'signin', 'auth']
  621. is_login_page = any(indicator in current_url.lower() for indicator in login_indicators)
  622. # 检查页面是否有登录弹窗
  623. need_login = is_login_page
  624. if not need_login:
  625. # 检查页面内容是否有登录提示
  626. login_selectors = [
  627. 'text="请先登录"',
  628. 'text="登录后继续"',
  629. 'text="请登录"',
  630. '[class*="login-modal"]',
  631. '[class*="login-dialog"]',
  632. '[class*="login-popup"]',
  633. ]
  634. for selector in login_selectors:
  635. try:
  636. if await self.page.locator(selector).count() > 0:
  637. need_login = True
  638. print(f"[{self.platform_name}] 检测到登录弹窗: {selector}")
  639. break
  640. except:
  641. pass
  642. if need_login:
  643. return {
  644. "success": True,
  645. "valid": False,
  646. "need_login": True,
  647. "message": "Cookie 已过期,需要重新登录"
  648. }
  649. else:
  650. return {
  651. "success": True,
  652. "valid": True,
  653. "need_login": False,
  654. "message": "登录状态有效"
  655. }
  656. except Exception as e:
  657. import traceback
  658. traceback.print_exc()
  659. return {
  660. "success": False,
  661. "valid": False,
  662. "need_login": True,
  663. "error": str(e)
  664. }
  665. finally:
  666. await self.close_browser()