Compare commits

...

1 Commits

Author SHA1 Message Date
Kevin Wong
3a3df41904 优化界面 2026-01-23 10:38:03 +08:00
5 changed files with 133 additions and 88 deletions

View File

@@ -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):
"""监控登录状态"""

View File

@@ -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:

View File

@@ -544,7 +544,7 @@ class DouyinUploader(BaseUploader):
if publish_success:
return {
"success": True,
"message": "发布成功",
"message": "发布成功,待审核",
"url": None
}
if is_timeout:

View File

@@ -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
}

View File

@@ -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>