merge: bring all updates into main

This commit is contained in:
Rixuan Shao
2026-08-28 00:36:45 +08:00
7 changed files with 171 additions and 38 deletions
+56 -11
View File
@@ -52,13 +52,26 @@ def _browser_args():
]
def _douyin_browser_proxy():
"""Return an explicit proxy for Douyin browser traffic, or None for direct."""
def _douyin_network_mode():
settings = get_app_settings(force_reload=True)
mode = str(
return str(
os.getenv("SPARKFLOW_DOUYIN_NETWORK_MODE")
or settings.get("douyin_network_mode", "direct")
).strip().lower()
def douyin_network_modes():
# Direct is the default; Mihomo is used only when explicitly selected.
mode = _douyin_network_mode()
if mode == "mihomo":
return ("mihomo",)
return ("direct",)
def _douyin_browser_proxy(network_mode=None):
# Return an explicit proxy URL for Douyin traffic, or None for direct.
settings = get_app_settings(force_reload=True)
mode = str(network_mode or _douyin_network_mode()).strip().lower()
if mode != "mihomo":
return None
return str(
@@ -67,13 +80,45 @@ def _douyin_browser_proxy():
).strip() or None
def _browser_launch_options(GUI=False):
def _browser_launch_options(GUI=False, network_mode=None):
args = _browser_args()
proxy = _douyin_browser_proxy()
proxy = _douyin_browser_proxy(network_mode=network_mode)
if proxy:
return {"headless": _headless_for(GUI), "args": args, "proxy": {"server": proxy}}
return {
"headless": _headless_for(GUI),
"args": args,
"proxy": {"server": proxy},
}
args.append("--no-proxy-server")
return {"headless": _headless_for(GUI), "args": args}
return {
"headless": _headless_for(GUI),
"args": args,
}
async def select_douyin_network_mode(target_url):
# Select the first route that can load the target before a task starts.
failures = []
for network_mode in douyin_network_modes():
playwright = browser = page = None
try:
playwright, browser = await get_browser(network_mode=network_mode)
page = await browser.new_page()
response = await page.goto(target_url, wait_until="commit", timeout=30000)
status = response.status if response is not None else None
if status is not None and status < 500:
return network_mode
failures.append(f"{network_mode}: HTTP {status}")
except Exception as exc:
failures.append(f"{network_mode}: {exc}")
finally:
if page:
await page.close()
if browser:
await browser.close()
if playwright:
await playwright.stop()
raise RuntimeError(f"Douyin network preflight failed: {'; '.join(failures)}")
def sanitize_profile_name(value):
@@ -102,12 +147,12 @@ async def install_browser():
console.print(f"[bold red]Browser install failed: {exc}[/bold red]")
async def get_browser(GUI=False):
async def get_browser(GUI=False, network_mode=None):
configure_playwright_environment()
try:
playwright = await async_playwright().start()
browser = await playwright.chromium.launch(**_browser_launch_options(GUI))
browser = await playwright.chromium.launch(**_browser_launch_options(GUI, network_mode=network_mode))
return playwright, browser
except Exception as exc:
if "Executable doesn't exist" in str(exc) and get_environment() != Environment.GITHUBACTION:
@@ -118,7 +163,7 @@ async def get_browser(GUI=False):
raise
async def get_persistent_browser_context(profile_name, GUI=False, root=None):
async def get_persistent_browser_context(profile_name, GUI=False, root=None, network_mode=None):
configure_playwright_environment()
profile_dir = browser_profile_root(root) / sanitize_profile_name(profile_name)
@@ -126,7 +171,7 @@ async def get_persistent_browser_context(profile_name, GUI=False, root=None):
try:
playwright = await async_playwright().start()
launch_options = _browser_launch_options(GUI)
launch_options = _browser_launch_options(GUI, network_mode=network_mode)
launch_options["viewport"] = {"width": 1600, "height": 1000}
context = await playwright.chromium.launch_persistent_context(
str(profile_dir),
+43 -7
View File
@@ -1,5 +1,10 @@
import asyncio
from core.browser import get_browser
import logging
from core.browser import douyin_network_modes, get_browser
logger = logging.getLogger(__name__)
CHAT_PAGE_URL = "https://creator.douyin.com/creator-micro/data/following/chat"
@@ -218,19 +223,15 @@ async def collect_friend_names(page):
return found_names
async def fetch_account_friends(account):
async def _fetch_account_friends_once(account, network_mode):
cookies = list(account.get("cookies") or [])
if not cookies:
raise RuntimeError("账号没有可用 cookies,请重新扫码登录")
playwright = browser = context = page = None
try:
playwright, browser = await get_browser(GUI=False)
playwright, browser = await get_browser(GUI=False, network_mode=network_mode)
context = await browser.new_context()
context.set_default_navigation_timeout(120000)
context.set_default_timeout(120000)
page = await context.new_page()
await context.add_cookies(cookies)
await page.goto(CHAT_PAGE_URL, wait_until="commit", timeout=30000)
await asyncio.sleep(1)
@@ -250,3 +251,38 @@ async def fetch_account_friends(account):
await browser.close()
if playwright:
await playwright.stop()
async def fetch_account_friends(account):
cookies = list(account.get("cookies") or [])
if not cookies:
raise RuntimeError("account has no cookies; scan login QR code first")
modes = douyin_network_modes()
last_error = None
for index, network_mode in enumerate(modes):
try:
friends = await _fetch_account_friends_once(account, network_mode)
logger.info(
"Friend refresh route=%s count=%s attempt=%s/%s",
network_mode,
len(friends),
index + 1,
len(modes),
)
if friends or index == len(modes) - 1:
return friends
logger.warning(
"Friend refresh route=%s returned zero friends; trying next route",
network_mode,
)
except RuntimeError as exc:
text = str(exc).lower()
if any(marker in text for marker in ("login", "cookie", "scan", "登录", "扫码")):
raise
last_error = exc
logger.warning("Friend refresh route=%s failed; trying next route: %s", network_mode, exc)
except Exception as exc:
last_error = exc
logger.warning("Friend refresh route=%s failed; trying next route: %s", network_mode, exc)
raise RuntimeError(f"friend refresh failed after routes {modes}: {last_error}")
+15 -6
View File
@@ -12,7 +12,12 @@ from datetime import datetime, timedelta, timezone
from pathlib import Path
from zoneinfo import ZoneInfo
from core.browser import get_browser, get_persistent_browser_context, sanitize_profile_name
from core.browser import (
get_browser,
get_persistent_browser_context,
sanitize_profile_name,
select_douyin_network_mode,
)
from core.msg_builder import build_message, build_message_candidates
from core.protocol_dispatch import run_protocol_tasks
from core.send_state import parse_sent_at, target_is_strong_confirmed_today
@@ -2354,6 +2359,8 @@ async def run_browser_tasks(active_config, browser_user_data):
send_strategy = _normalize_send_strategy(active_config)
friend_scan_config = _normalize_friend_list_scan_config(active_config)
profile_config = _normalize_persistent_profile_config(active_config)
network_mode = await select_douyin_network_mode(CREATOR_HOME_URL)
logger.info("Selected Douyin task network route=%s", network_mode)
semaphore = asyncio.Semaphore(active_config["taskCount"] if active_config["multiTask"] else 1)
tasks = []
@@ -2371,11 +2378,11 @@ async def run_browser_tasks(active_config, browser_user_data):
user.get("username", "unknown"),
len(user["targets"]),
)
tasks.append(do_user_task(None, user, semaphore, send_strategy, profile_config, friend_scan_config))
tasks.append(do_user_task(None, user, semaphore, send_strategy, profile_config, friend_scan_config, network_mode))
await asyncio.gather(*tasks)
return
playwright, browser = await get_browser()
playwright, browser = await get_browser(network_mode=network_mode)
try:
for user in browser_user_data:
logger.info(
@@ -2383,7 +2390,7 @@ async def run_browser_tasks(active_config, browser_user_data):
user.get("username", "unknown"),
len(user["targets"]),
)
tasks.append(do_user_task(browser, user, semaphore, send_strategy, profile_config, friend_scan_config))
tasks.append(do_user_task(browser, user, semaphore, send_strategy, profile_config, friend_scan_config, network_mode))
await asyncio.gather(*tasks)
finally:
@@ -2391,7 +2398,7 @@ async def run_browser_tasks(active_config, browser_user_data):
await browser.close()
async def do_user_task(browser, user, semaphore, send_strategy, profile_config, friend_scan_config):
async def do_user_task(browser, user, semaphore, send_strategy, profile_config, friend_scan_config, network_mode):
async with semaphore:
account_name = user.get("username", "unknown")
account_lock_handle = None
@@ -2416,6 +2423,7 @@ async def do_user_task(browser, user, semaphore, send_strategy, profile_config,
profile_config,
friend_scan_config,
account_name,
network_mode,
),
timeout=timeout_seconds,
)
@@ -2437,7 +2445,7 @@ async def do_user_task(browser, user, semaphore, send_strategy, profile_config,
_release_browser_account_lock(account_lock_handle, account_lock_path, account_name)
async def _do_user_task_locked(browser, user, send_strategy, profile_config, friend_scan_config, account_name):
async def _do_user_task_locked(browser, user, send_strategy, profile_config, friend_scan_config, account_name, network_mode):
cookies = user["cookies"]
targets = user["targets"]
start_delay = _random_delay_seconds(
@@ -2453,6 +2461,7 @@ async def _do_user_task_locked(browser, user, send_strategy, profile_config, fri
owned_playwright, context, profile_dir = await get_persistent_browser_context(
_account_profile_name(user),
root=profile_config["root"],
network_mode=network_mode,
)
logger.info("Opened persistent browser profile for %s at %s", account_name, profile_dir)
if profile_config["syncStoredCookiesBeforeRun"]: