diff --git a/browser_gateway/server/http.py b/browser_gateway/server/http.py
index 91a160f..f19750b 100644
--- a/browser_gateway/server/http.py
+++ b/browser_gateway/server/http.py
@@ -172,12 +172,14 @@ class Gateway:
response = self.browser.get(alias, input["url"])
self._require_douyin_generation(alias, input)
except DouyinError as exc:
- LOG.warning(
- "Douyin GET failed alias=%s reason=%s",
+ LOG.exception(
+ "Douyin GET failed alias=%s binding_version=%s url=%s reason=%s",
alias,
+ input["binding_version"],
+ input["url"],
str(exc),
)
- raise RequestError("restricted browser operation failed") from exc
+ raise RequestError(f"restricted browser operation failed: {exc}") from exc
return {
"status": response.status,
"body": response.body,
diff --git a/browser_gateway/test_gateway.py b/browser_gateway/test_gateway.py
index a8144ae..2dd3650 100644
--- a/browser_gateway/test_gateway.py
+++ b/browser_gateway/test_gateway.py
@@ -16,6 +16,7 @@ from unittest.mock import Mock, patch
import websocket
+from .browser.cdp import BrowserError
from .platform.douyin import (
BrowserResponse,
CDPConnection,
@@ -443,6 +444,19 @@ class GatewayBusinessMethodTests(unittest.TestCase):
]
self.assertEqual(self.gateway.list_browsers(), [{"alias": "same", "state": "stopped"}])
+ def test_get_douyin_preserves_failure_reason_and_logs_traceback(self) -> None:
+ url = "https://www.douyin.com/aweme/v1/web/user/profile/other/?aid=6383&device_platform=webapp&user_id=2328120603967913"
+ reason = "CDP command timed out: Runtime.evaluate"
+ self.browser.get.side_effect = DouyinError(reason)
+ with self.assertLogs("creatorhub.gateway", level="ERROR") as captured:
+ with self.assertRaisesRegex(RequestError, reason):
+ self.gateway.get_douyin("safe", {**self.generation, "url": url})
+ record = captured.records[0]
+ self.assertIn(url, record.getMessage())
+ self.assertIn("safe", record.getMessage())
+ self.assertIn(f"binding_version={self.generation['binding_version']}", record.getMessage())
+ self.assertIsNotNone(record.exc_info)
+
def test_lifecycle_info_and_douyin_operations(self) -> None:
self.runtimes.create.return_value = {"state": "running"}
self.assertEqual(self.gateway.info()["node_id"], "node-a")
@@ -830,6 +844,7 @@ class BrowserTests(unittest.TestCase):
{"origin": "https://www.douyin.com", "qr_detected": True},
]
cdp.command.side_effect = [
+ {},
{"frameId": "frame-1"},
{"data": screenshot},
{"data": screenshot},
@@ -844,17 +859,32 @@ class BrowserTests(unittest.TestCase):
self.assertEqual(
[call.args[0] for call in cdp.command.call_args_list],
[
+ "Page.enable",
"Page.navigate",
"Page.captureScreenshot",
"Page.captureScreenshot",
],
)
self.assertEqual(
- cdp.command.call_args_list[0].args,
+ cdp.command.call_args_list[1].args,
("Page.navigate", {"url": LOGIN_PAGE_URL}),
)
+ cdp.wait_event.assert_called_once()
+ self.assertEqual(cdp.wait_event.call_args.args[0], "Page.domContentEventFired")
+ methods = [call[0] for call in cdp.mock_calls]
+ self.assertLess(methods.index('wait_event'), methods.index('evaluate'))
self.assertFalse(any("cookie" in expression.lower() for expression in cdp.evaluate.call_args.args))
+ def test_login_qr_does_not_capture_an_unloaded_page(self) -> None:
+ cdp = Mock()
+ cdp.command.side_effect = [{}, {"frameId": "frame-1"}]
+ cdp.wait_event.side_effect = BrowserError("Douyin DOM did not load")
+ browser = DouyinBrowser()
+ self._with_connection(browser, cast(BrowserCDP, cdp))
+ with self.assertRaisesRegex(BrowserError, 'Douyin DOM did not load'):
+ browser.login_qr('safe')
+ self.assertNotIn('Page.captureScreenshot', [call.args[0] for call in cdp.command.call_args_list])
+
def test_connect_selects_configured_target_from_large_mixed_list(self) -> None:
targets = [
{"type": "service", "url": "http://127.0.0.1:9222/json"} for _ in range(40)
diff --git a/web/src/components/accounts/MonitoringAccountList.tsx b/web/src/components/accounts/MonitoringAccountList.tsx
index 2a26026..15e7dd6 100644
--- a/web/src/components/accounts/MonitoringAccountList.tsx
+++ b/web/src/components/accounts/MonitoringAccountList.tsx
@@ -1,7 +1,7 @@
// 监控账号列表:表格呈现账号画像、作品数据与同步状态。
import { useCallback, useEffect, useState } from 'react';
import { history } from '@umijs/max';
-import { Alert, App, Avatar, Button, Card, Flex, Form, Modal, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd';
+import { Alert, App, Avatar, Button, Card, Flex, Form, Modal, Popconfirm, Select, Space, Table, Tag, Tooltip, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { CloudUploadOutlined, ReloadOutlined } from '@ant-design/icons';
import { getList, remove, creatorAction, creatorUpdate } from '@/services/api';
@@ -165,11 +165,14 @@ export default function MonitoringAccountList() {
const meta = syncStatusMeta[account.sync_status ?? ''] || { label: account.sync_status || '—' };
return (