Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a3df41904 |
@@ -139,84 +139,72 @@ class QRLoginService:
|
||||
|
||||
async def _extract_qr_code(self, page: Page, selectors: List[str]) -> Optional[str]:
|
||||
"""
|
||||
提取二维码图片 (借鉴 SuperIPAgent 的方式)
|
||||
提取二维码图片 (优化策略顺序)
|
||||
根据日志分析:抖音和B站使用 Text 策略成功率最高
|
||||
"""
|
||||
qr_element = None
|
||||
|
||||
# 策略1: 使用 get_by_role (最可靠, SuperIPAgent 使用此方法)
|
||||
if self.platform == "douyin":
|
||||
try:
|
||||
logger.debug(f"[{self.platform}] 策略1(Role): 尝试 get_by_role('img', name='二维码')...")
|
||||
img = page.get_by_role("img", name="二维码")
|
||||
await img.wait_for(state="visible", timeout=10000)
|
||||
if await img.count() > 0:
|
||||
# 获取 src 属性,如果是 data:image 则直接用,否则截图
|
||||
src = await img.get_attribute("src")
|
||||
if src and src.startswith("data:image"):
|
||||
logger.info(f"[{self.platform}] 策略1(Role): 获取到 data URI")
|
||||
# 提取 base64 部分
|
||||
return src.split(",")[1] if "," in src else src
|
||||
else:
|
||||
logger.info(f"[{self.platform}] 策略1(Role): 截图获取")
|
||||
screenshot = await img.screenshot()
|
||||
return base64.b64encode(screenshot).decode()
|
||||
except Exception as e:
|
||||
logger.warning(f"[{self.platform}] 策略1(Role) 失败: {e}")
|
||||
|
||||
# 策略2: CSS 选择器
|
||||
try:
|
||||
combined_selector = ", ".join(selectors)
|
||||
logger.debug(f"[{self.platform}] 策略2(CSS): 开始等待...")
|
||||
el = await page.wait_for_selector(combined_selector, state="visible", timeout=8000)
|
||||
if el:
|
||||
logger.info(f"[{self.platform}] 策略2(CSS): 匹配成功")
|
||||
qr_element = el
|
||||
except Exception as e:
|
||||
logger.warning(f"[{self.platform}] 策略2(CSS) 失败: {e}")
|
||||
|
||||
# 策略3: 基于文本查找附近图片
|
||||
if not qr_element:
|
||||
try:
|
||||
logger.debug(f"[{self.platform}] 策略3(Text): 开始搜索...")
|
||||
keywords = ["扫码登录", "二维码", "打开抖音", "抖音APP"]
|
||||
# 针对抖音和B站:优先使用 Text 策略 (成功率最高,速度最快)
|
||||
if self.platform in ("douyin", "bilibili"):
|
||||
# 尝试最多2次 (首次 + 1次重试)
|
||||
for attempt in range(2):
|
||||
if attempt > 0:
|
||||
logger.info(f"[{self.platform}] 等待页面加载后重试...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
for kw in keywords:
|
||||
# 策略1: Text (优先,成功率最高)
|
||||
qr_element = await self._try_text_strategy(page)
|
||||
if qr_element:
|
||||
try:
|
||||
text_el = page.get_by_text(kw, exact=False).first
|
||||
await text_el.wait_for(state="visible", timeout=2000)
|
||||
|
||||
# 向上查找图片
|
||||
parent = text_el
|
||||
for _ in range(5):
|
||||
parent = parent.locator("..")
|
||||
imgs = parent.locator("img")
|
||||
|
||||
for i in range(await imgs.count()):
|
||||
img = imgs.nth(i)
|
||||
if await img.is_visible():
|
||||
bbox = await img.bounding_box()
|
||||
if bbox and bbox['width'] > 100:
|
||||
logger.info(f"[{self.platform}] 策略3(Text): 成功")
|
||||
qr_element = img
|
||||
break
|
||||
if qr_element:
|
||||
break
|
||||
if qr_element:
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.warning(f"[{self.platform}] 策略3(Text) 失败: {e}")
|
||||
|
||||
# 如果找到元素,截图返回
|
||||
if qr_element:
|
||||
screenshot = await qr_element.screenshot()
|
||||
return base64.b64encode(screenshot).decode()
|
||||
except Exception as e:
|
||||
logger.warning(f"[{self.platform}] Text策略截图失败: {e}")
|
||||
qr_element = None
|
||||
|
||||
# 策略2: CSS (备用)
|
||||
if not qr_element:
|
||||
try:
|
||||
combined_selector = ", ".join(selectors)
|
||||
logger.debug(f"[{self.platform}] 策略2(CSS): 开始等待...")
|
||||
# 增加超时到5秒,抖音页面加载较慢
|
||||
el = await page.wait_for_selector(combined_selector, state="visible", timeout=5000)
|
||||
if el:
|
||||
logger.info(f"[{self.platform}] 策略2(CSS): 匹配成功")
|
||||
screenshot = await el.screenshot()
|
||||
return base64.b64encode(screenshot).decode()
|
||||
except Exception as e:
|
||||
logger.warning(f"[{self.platform}] 策略2(CSS) 失败: {e}")
|
||||
|
||||
# 如果已成功,退出循环
|
||||
if qr_element:
|
||||
break
|
||||
else:
|
||||
# 其他平台 (小红书等):保持原顺序 CSS -> Text
|
||||
# 策略1: CSS 选择器
|
||||
try:
|
||||
screenshot = await qr_element.screenshot()
|
||||
return base64.b64encode(screenshot).decode()
|
||||
combined_selector = ", ".join(selectors)
|
||||
logger.debug(f"[{self.platform}] 策略1(CSS): 开始等待...")
|
||||
el = await page.wait_for_selector(combined_selector, state="visible", timeout=5000)
|
||||
if el:
|
||||
logger.info(f"[{self.platform}] 策略1(CSS): 匹配成功")
|
||||
qr_element = el
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.platform}] 截图失败: {e}")
|
||||
logger.warning(f"[{self.platform}] 策略1(CSS) 失败: {e}")
|
||||
|
||||
# 策略2: Text
|
||||
if not qr_element:
|
||||
qr_element = await self._try_text_strategy(page)
|
||||
|
||||
# 如果找到元素,截图返回
|
||||
if qr_element:
|
||||
try:
|
||||
screenshot = await qr_element.screenshot()
|
||||
return base64.b64encode(screenshot).decode()
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.platform}] 截图失败: {e}")
|
||||
|
||||
# 所有策略失败 - 不使用全页截图,直接返回 None
|
||||
# 所有策略失败
|
||||
logger.error(f"[{self.platform}] 所有QR码提取策略失败")
|
||||
|
||||
# 保存调试截图
|
||||
@@ -224,7 +212,37 @@ class QRLoginService:
|
||||
debug_dir.mkdir(exist_ok=True)
|
||||
await page.screenshot(path=str(debug_dir / f"{self.platform}_debug.png"))
|
||||
|
||||
return None # 不再回退到全页截图
|
||||
return None
|
||||
|
||||
async def _try_text_strategy(self, page: Page) -> Optional[Any]:
|
||||
"""基于文本查找二维码图片"""
|
||||
try:
|
||||
logger.debug(f"[{self.platform}] 策略Text: 开始搜索...")
|
||||
keywords = ["扫码登录", "二维码", "打开抖音", "抖音APP", "使用APP扫码"]
|
||||
|
||||
for kw in keywords:
|
||||
try:
|
||||
text_el = page.get_by_text(kw, exact=False).first
|
||||
await text_el.wait_for(state="visible", timeout=2000)
|
||||
|
||||
# 向上查找图片
|
||||
parent = text_el
|
||||
for _ in range(5):
|
||||
parent = parent.locator("..")
|
||||
imgs = parent.locator("img")
|
||||
|
||||
for i in range(await imgs.count()):
|
||||
img = imgs.nth(i)
|
||||
if await img.is_visible():
|
||||
bbox = await img.bounding_box()
|
||||
if bbox and bbox['width'] > 100:
|
||||
logger.info(f"[{self.platform}] 策略Text: 成功")
|
||||
return img
|
||||
except Exception:
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.warning(f"[{self.platform}] 策略Text 失败: {e}")
|
||||
return None
|
||||
|
||||
async def _monitor_login_status(self, page: Page, success_url: str):
|
||||
"""监控登录状态"""
|
||||
|
||||
@@ -136,14 +136,14 @@ class BilibiliUploader(BaseUploader):
|
||||
logger.success(f"[B站] 上传成功: {bvid}")
|
||||
return {
|
||||
"success": True,
|
||||
"message": "上传成功" if data.dtime == 0 else "已设置定时发布",
|
||||
"message": "发布成功,待审核" if data.dtime == 0 else "已设置定时发布",
|
||||
"url": f"https://www.bilibili.com/video/{bvid}"
|
||||
}
|
||||
elif aid:
|
||||
logger.success(f"[B站] 上传成功: av{aid}")
|
||||
return {
|
||||
"success": True,
|
||||
"message": "上传成功" if data.dtime == 0 else "已设置定时发布",
|
||||
"message": "发布成功,待审核" if data.dtime == 0 else "已设置定时发布",
|
||||
"url": f"https://www.bilibili.com/video/av{aid}"
|
||||
}
|
||||
else:
|
||||
@@ -151,7 +151,7 @@ class BilibiliUploader(BaseUploader):
|
||||
logger.warning(f"[B站] 上传返回code=0但无bvid/aid: {ret}")
|
||||
return {
|
||||
"success": True,
|
||||
"message": "上传成功(无法获取视频链接)",
|
||||
"message": "发布成功,待审核",
|
||||
"url": None
|
||||
}
|
||||
else:
|
||||
|
||||
@@ -544,7 +544,7 @@ class DouyinUploader(BaseUploader):
|
||||
if publish_success:
|
||||
return {
|
||||
"success": True,
|
||||
"message": "发布成功",
|
||||
"message": "发布成功,待审核",
|
||||
"url": None
|
||||
}
|
||||
if is_timeout:
|
||||
|
||||
@@ -171,7 +171,7 @@ class XiaohongshuUploader(BaseUploader):
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "上传成功" if self.publish_date == 0 else "已设置定时发布",
|
||||
"message": "发布成功,待审核" if self.publish_date == 0 else "已设置定时发布",
|
||||
"url": None
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ export default function PublishPage() {
|
||||
const [publishTime, setPublishTime] = useState<string>("");
|
||||
const [qrCodeImage, setQrCodeImage] = useState<string | null>(null);
|
||||
const [qrPlatform, setQrPlatform] = useState<string | null>(null);
|
||||
const [isLoadingQR, setIsLoadingQR] = useState(false);
|
||||
|
||||
// 加载账号和视频列表
|
||||
useEffect(() => {
|
||||
@@ -111,6 +112,12 @@ export default function PublishPage() {
|
||||
|
||||
const result = await res.json();
|
||||
setPublishResults((prev) => [...prev, result]);
|
||||
// 发布成功后10秒自动清除结果
|
||||
if (result.success) {
|
||||
setTimeout(() => {
|
||||
setPublishResults((prev) => prev.filter((r) => r !== result));
|
||||
}, 10000);
|
||||
}
|
||||
} catch (error) {
|
||||
setPublishResults((prev) => [
|
||||
...prev,
|
||||
@@ -155,6 +162,9 @@ export default function PublishPage() {
|
||||
}, [qrPlatform]);
|
||||
|
||||
const handleLogin = async (platform: string) => {
|
||||
setIsLoadingQR(true);
|
||||
setQrPlatform(platform); // 立即显示加载弹窗
|
||||
setQrCodeImage(null); // 清空旧二维码
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/publish/login/${platform}`, {
|
||||
method: 'POST'
|
||||
@@ -163,13 +173,16 @@ export default function PublishPage() {
|
||||
|
||||
if (result.success && result.qr_code) {
|
||||
setQrCodeImage(result.qr_code);
|
||||
setQrPlatform(platform);
|
||||
// SWR hook will automatically start polling since qrPlatform is set
|
||||
} else {
|
||||
setQrPlatform(null); // 失败时关闭弹窗
|
||||
alert(result.message || '登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
setQrPlatform(null); // 失败时关闭弹窗
|
||||
alert(`登录失败: ${error}`);
|
||||
} finally {
|
||||
setIsLoadingQR(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -202,20 +215,29 @@ export default function PublishPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-purple-900 to-gray-900">
|
||||
{/* QR码弹窗 */}
|
||||
{qrCodeImage && (
|
||||
{qrPlatform && (
|
||||
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-2xl p-8 max-w-md">
|
||||
<div className="bg-white rounded-2xl p-8 max-w-md min-w-[320px]">
|
||||
<h2 className="text-2xl font-bold mb-4 text-center">🔐 扫码登录 {qrPlatform}</h2>
|
||||
<img
|
||||
src={`data:image/png;base64,${qrCodeImage}`}
|
||||
alt="QR Code"
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
<p className="text-center text-gray-600 mt-4">
|
||||
请使用手机扫码登录
|
||||
</p>
|
||||
{isLoadingQR ? (
|
||||
<div className="flex flex-col items-center py-8">
|
||||
<div className="animate-spin w-16 h-16 border-4 border-purple-500 border-t-transparent rounded-full" />
|
||||
<p className="text-gray-600 mt-4">正在获取二维码...</p>
|
||||
</div>
|
||||
) : qrCodeImage ? (
|
||||
<>
|
||||
<img
|
||||
src={`data:image/png;base64,${qrCodeImage}`}
|
||||
alt="QR Code"
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
<p className="text-center text-gray-600 mt-4">
|
||||
请使用手机扫码登录
|
||||
</p>
|
||||
</>
|
||||
) : null}
|
||||
<button
|
||||
onClick={() => setQrCodeImage(null)}
|
||||
onClick={() => { setQrCodeImage(null); setQrPlatform(null); }}
|
||||
className="w-full mt-4 px-4 py-2 bg-gray-200 rounded-lg hover:bg-gray-300"
|
||||
>
|
||||
取消
|
||||
@@ -464,6 +486,11 @@ export default function PublishPage() {
|
||||
<span className="text-white">
|
||||
{platformIcons[result.platform]} {result.message}
|
||||
</span>
|
||||
{result.success && (
|
||||
<p className="text-green-400/80 text-sm mt-1">
|
||||
⏳ 审核一般需要几分钟,请耐心等待
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user