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
+2
View File
@@ -15,11 +15,13 @@ LOGIN_DESKTOP_PIDS_LIMIT=256
LOGIN_DESKTOP_IDLE_TIMEOUT_SECONDS=1800 LOGIN_DESKTOP_IDLE_TIMEOUT_SECONDS=1800
LOGIN_DESKTOP_STOP_AFTER_EXPORT_SECONDS=60 LOGIN_DESKTOP_STOP_AFTER_EXPORT_SECONDS=60
LOGIN_DESKTOP_STATUS_CACHE_SECONDS=15 LOGIN_DESKTOP_STATUS_CACHE_SECONDS=15
# Login browser uses direct access by default; Mihomo is an advanced option. # Login browser uses direct access by default; Mihomo is an advanced option.
LOGIN_DESKTOP_PROXY_MODE=direct LOGIN_DESKTOP_PROXY_MODE=direct
LOGIN_DESKTOP_PROXY=http://proxy:7890 LOGIN_DESKTOP_PROXY=http://proxy:7890
LOGIN_DESKTOP_PREFLIGHT_TIMEOUT_SECONDS=15 LOGIN_DESKTOP_PREFLIGHT_TIMEOUT_SECONDS=15
LOGIN_DESKTOP_NETWORK_CACHE_SECONDS=30 LOGIN_DESKTOP_NETWORK_CACHE_SECONDS=30
PROXY_BIND_ADDRESS=127.0.0.1 PROXY_BIND_ADDRESS=127.0.0.1
PROXY_HTTP_PORT=7890 PROXY_HTTP_PORT=7890
PROXY_CONTROLLER_PORT=9090 PROXY_CONTROLLER_PORT=9090
+56 -11
View File
@@ -52,13 +52,26 @@ def _browser_args():
] ]
def _douyin_browser_proxy(): def _douyin_network_mode():
"""Return an explicit proxy for Douyin browser traffic, or None for direct."""
settings = get_app_settings(force_reload=True) settings = get_app_settings(force_reload=True)
mode = str( return str(
os.getenv("SPARKFLOW_DOUYIN_NETWORK_MODE") os.getenv("SPARKFLOW_DOUYIN_NETWORK_MODE")
or settings.get("douyin_network_mode", "direct") or settings.get("douyin_network_mode", "direct")
).strip().lower() ).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": if mode != "mihomo":
return None return None
return str( return str(
@@ -67,13 +80,45 @@ def _douyin_browser_proxy():
).strip() or None ).strip() or None
def _browser_launch_options(GUI=False): def _browser_launch_options(GUI=False, network_mode=None):
args = _browser_args() args = _browser_args()
proxy = _douyin_browser_proxy() proxy = _douyin_browser_proxy(network_mode=network_mode)
if proxy: 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") 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): def sanitize_profile_name(value):
@@ -102,12 +147,12 @@ async def install_browser():
console.print(f"[bold red]Browser install failed: {exc}[/bold red]") 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() configure_playwright_environment()
try: try:
playwright = await async_playwright().start() 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 return playwright, browser
except Exception as exc: except Exception as exc:
if "Executable doesn't exist" in str(exc) and get_environment() != Environment.GITHUBACTION: if "Executable doesn't exist" in str(exc) and get_environment() != Environment.GITHUBACTION:
@@ -118,7 +163,7 @@ async def get_browser(GUI=False):
raise 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() configure_playwright_environment()
profile_dir = browser_profile_root(root) / sanitize_profile_name(profile_name) 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: try:
playwright = await async_playwright().start() 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} launch_options["viewport"] = {"width": 1600, "height": 1000}
context = await playwright.chromium.launch_persistent_context( context = await playwright.chromium.launch_persistent_context(
str(profile_dir), str(profile_dir),
+43 -7
View File
@@ -1,5 +1,10 @@
import asyncio 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" 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 return found_names
async def fetch_account_friends(account): async def _fetch_account_friends_once(account, network_mode):
cookies = list(account.get("cookies") or []) cookies = list(account.get("cookies") or [])
if not cookies:
raise RuntimeError("账号没有可用 cookies,请重新扫码登录")
playwright = browser = context = page = None playwright = browser = context = page = None
try: 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 = await browser.new_context()
context.set_default_navigation_timeout(120000) context.set_default_navigation_timeout(120000)
context.set_default_timeout(120000) context.set_default_timeout(120000)
page = await context.new_page() page = await context.new_page()
await context.add_cookies(cookies) await context.add_cookies(cookies)
await page.goto(CHAT_PAGE_URL, wait_until="commit", timeout=30000) await page.goto(CHAT_PAGE_URL, wait_until="commit", timeout=30000)
await asyncio.sleep(1) await asyncio.sleep(1)
@@ -250,3 +251,38 @@ async def fetch_account_friends(account):
await browser.close() await browser.close()
if playwright: if playwright:
await playwright.stop() 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 pathlib import Path
from zoneinfo import ZoneInfo 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.msg_builder import build_message, build_message_candidates
from core.protocol_dispatch import run_protocol_tasks from core.protocol_dispatch import run_protocol_tasks
from core.send_state import parse_sent_at, target_is_strong_confirmed_today 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) send_strategy = _normalize_send_strategy(active_config)
friend_scan_config = _normalize_friend_list_scan_config(active_config) friend_scan_config = _normalize_friend_list_scan_config(active_config)
profile_config = _normalize_persistent_profile_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) semaphore = asyncio.Semaphore(active_config["taskCount"] if active_config["multiTask"] else 1)
tasks = [] tasks = []
@@ -2371,11 +2378,11 @@ async def run_browser_tasks(active_config, browser_user_data):
user.get("username", "unknown"), user.get("username", "unknown"),
len(user["targets"]), 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) await asyncio.gather(*tasks)
return return
playwright, browser = await get_browser() playwright, browser = await get_browser(network_mode=network_mode)
try: try:
for user in browser_user_data: for user in browser_user_data:
logger.info( logger.info(
@@ -2383,7 +2390,7 @@ async def run_browser_tasks(active_config, browser_user_data):
user.get("username", "unknown"), user.get("username", "unknown"),
len(user["targets"]), 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) await asyncio.gather(*tasks)
finally: finally:
@@ -2391,7 +2398,7 @@ async def run_browser_tasks(active_config, browser_user_data):
await browser.close() 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: async with semaphore:
account_name = user.get("username", "unknown") account_name = user.get("username", "unknown")
account_lock_handle = None account_lock_handle = None
@@ -2416,6 +2423,7 @@ async def do_user_task(browser, user, semaphore, send_strategy, profile_config,
profile_config, profile_config,
friend_scan_config, friend_scan_config,
account_name, account_name,
network_mode,
), ),
timeout=timeout_seconds, 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) _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"] cookies = user["cookies"]
targets = user["targets"] targets = user["targets"]
start_delay = _random_delay_seconds( 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( owned_playwright, context, profile_dir = await get_persistent_browser_context(
_account_profile_name(user), _account_profile_name(user),
root=profile_config["root"], root=profile_config["root"],
network_mode=network_mode,
) )
logger.info("Opened persistent browser profile for %s at %s", account_name, profile_dir) logger.info("Opened persistent browser profile for %s at %s", account_name, profile_dir)
if profile_config["syncStoredCookiesBeforeRun"]: if profile_config["syncStoredCookiesBeforeRun"]:
@@ -0,0 +1,32 @@
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
SOURCE_ROOT = REPO_ROOT / "DouYinSparkFlow"
class NetworkFallbackContractTests(unittest.TestCase):
def test_browser_exposes_direct_first_routes_and_preflight(self):
browser = (SOURCE_ROOT / "core" / "browser.py").read_text(encoding="utf-8")
self.assertIn("def douyin_network_modes", browser)
self.assertIn("return (\"direct\", \"mihomo\")", browser)
self.assertIn("async def select_douyin_network_mode", browser)
self.assertIn("get_browser(network_mode=network_mode)", browser)
def test_friend_refresh_can_try_the_next_route(self):
friends = (SOURCE_ROOT / "core" / "friends.py").read_text(encoding="utf-8")
self.assertIn("for index, network_mode in enumerate(modes)", friends)
self.assertIn("returned zero friends; trying next route", friends)
self.assertIn("get_browser(GUI=False, network_mode=network_mode)", friends)
def test_tasks_select_route_before_browser_creation(self):
tasks = (SOURCE_ROOT / "core" / "tasks.py").read_text(encoding="utf-8")
self.assertIn("select_douyin_network_mode(CREATOR_HOME_URL)", tasks)
self.assertIn("get_browser(network_mode=network_mode)", tasks)
self.assertIn("network_mode=network_mode", tasks)
def test_login_defaults_to_auto_in_compose(self):
compose = (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8")
self.assertIn("LOGIN_DESKTOP_PROXY_MODE: ${LOGIN_DESKTOP_PROXY_MODE:-auto}", compose)
if __name__ == "__main__":
unittest.main()
+21 -8
View File
@@ -91,12 +91,17 @@
git clone https://github.com/halfwaystudent/douyin-sparkflow.git git clone https://github.com/halfwaystudent/douyin-sparkflow.git
cd douyin-sparkflow cd douyin-sparkflow
# 2. 配置环境变量 # 2. 创建本地环境变量
cp .env.example .env cp .env.example .env
nano .env # 根据需要修改配置 nano .env # 根据需要修改配置
# 可选:在本地 .env 中填写 PROXY_SUB_URL,不要提交真实订阅地址
# 3. 启动服务 # 3. 初始化运行时文件并启动服务
docker compose up -d # 会创建 proxy/config.yaml;没有订阅时使用 DIRECT-only 配置
bash ./deploy/install-local.sh
# Windows PowerShell 使用:
# powershell -ExecutionPolicy Bypass -File .\deploy\install-local.ps1
# 4. 访问 Web 界面 # 4. 访问 Web 界面
# 浏览器打开 http://localhost:8787 # 浏览器打开 http://localhost:8787
@@ -185,7 +190,8 @@ douyin-sparkflow/
│ └── login_desktop_server.py # 登录桌面服务 │ └── login_desktop_server.py # 登录桌面服务
├── .github/workflows/ # GitHub Actions 定时任务 ├── .github/workflows/ # GitHub Actions 定时任务
├── proxy/ # 代理配置 ├── proxy/ # 代理配置
── config.yaml # Mihomo 代理配置 ── config.example.yaml # Git 跟踪的安全模板
│ └── config.yaml # 本地生成,Git 忽略
├── docker-compose.yml # 容器编排配置 ├── docker-compose.yml # 容器编排配置
├── .env.example # 环境变量模板 ├── .env.example # 环境变量模板
├── refresh_proxy.sh # 代理刷新脚本 ├── refresh_proxy.sh # 代理刷新脚本
@@ -231,19 +237,24 @@ WEB_PORT=8787
LOGIN_DESKTOP_BIND_ADDRESS=127.0.0.1 LOGIN_DESKTOP_BIND_ADDRESS=127.0.0.1
LOGIN_DESKTOP_WEB_PORT=8788 LOGIN_DESKTOP_WEB_PORT=8788
LOGIN_DESKTOP_PUBLIC_URL=/login-desktop/proxy/vnc.html?autoconnect=1&resize=scale&view_only=0&path=login-desktop/proxy/websockify LOGIN_DESKTOP_PUBLIC_URL=/login-desktop/proxy/vnc.html?autoconnect=1&resize=scale&view_only=0&path=login-desktop/proxy/websockify
# 登录浏览器默认直连;Mihomo 仅作为高级选项 # 登录浏览器默认直连;Mihomo 仅作为高级选项
LOGIN_DESKTOP_PROXY_MODE=direct LOGIN_DESKTOP_PROXY_MODE=direct
LOGIN_DESKTOP_PROXY=http://proxy:7890 LOGIN_DESKTOP_PROXY=http://proxy:7890
# Mihomo 代理和控制端口默认仅绑定本机 # Mihomo 代理和控制端口默认仅绑定本机
PROXY_BIND_ADDRESS=127.0.0.1 PROXY_BIND_ADDRESS=127.0.0.1
PROXY_HTTP_PORT=7890 PROXY_HTTP_PORT=7890
PROXY_CONTROLLER_PORT=9090 PROXY_CONTROLLER_PORT=9090
# 可选:Mihomo/Clash 订阅地址。通常包含敏感 token,只写入本地 .env。
PROXY_SUB_URL= PROXY_SUB_URL=
``` ```
默认抖音业务网络使用直连。登录、好友刷新和浏览器发送会显式禁用环境代理,避免未配置的 Mihomo 影响正常使用。高级用户可在 Web UI「系统设置」中选择 Mihomo 并填写代理地址;登录浏览器仍可通过 `LOGIN_DESKTOP_PROXY_MODE=proxy` 强制使用代理。 默认抖音业务网络使用直连。登录、好友刷新和浏览器发送会显式禁用环境代理,避免未配置的 Mihomo 影响正常使用。高级用户可在 Web UI「系统设置」中选择 Mihomo 并填写代理地址;登录浏览器仍可通过 `LOGIN_DESKTOP_PROXY_MODE=proxy` 强制使用代理。
#### `config.example.json` 与 `config.json` - 应用配置 #### `config.example.json` 与 `config.json` - 应用配置
仓库跟踪 `DouYinSparkFlow/config.example.json`;首次运行会生成被 Git 忽略的 `DouYinSparkFlow/config.json`。常用配置示例: 仓库跟踪 `DouYinSparkFlow/config.example.json`;首次运行会生成被 Git 忽略的 `DouYinSparkFlow/config.json`。常用配置示例:
@@ -299,8 +310,8 @@ PROXY_SUB_URL=
# 1. 准备环境变量 # 1. 准备环境变量
cp .env.example .env cp .env.example .env
# 2. 启动所有服务 # 2. 初始化 proxy/config.yaml 并启动所有服务
docker compose up -d bash ./deploy/install-local.sh
# 3. 查看日志 # 3. 查看日志
docker compose logs -f docker compose logs -f
@@ -372,15 +383,17 @@ server {
### 代理配置 ### 代理配置
项目支持通过代理访问抖音服务,配置文件位于 `proxy/config.yaml` 项目支持通过代理访问抖音服务。仓库提供 `proxy/config.example.yaml` 作为安全模板,部署脚本会在启动前生成本地的 `proxy/config.yaml`
```yaml ```yaml
mixed-port: 7890 mixed-port: 7890
allow-lan: true allow-lan: true
mode: rule mode: rule
# ... 更多配置见配置文件 # ... 更多配置见 proxy/config.example.yaml
``` ```
如果 `PROXY_SUB_URL` 不为空,`refresh_proxy.sh` 会下载订阅并更新本地配置;如果为空,则生成 DIRECT-only 配置。不要在 Git 中提交包含订阅 token 的 `proxy/config.yaml`。首次部署不要跳过初始化步骤直接执行 `docker compose up -d`,否则 Docker 可能把缺失的配置文件创建成目录。
### 默认网络安全 ### 默认网络安全
+2 -6
View File
@@ -22,12 +22,6 @@ services:
args: args:
PLAYWRIGHT_BASE_IMAGE: ${PLAYWRIGHT_BASE_IMAGE:-swr.cn-north-4.myhuaweicloud.com/ddn-k8s/mcr.microsoft.com/playwright/python:v1.56.0-jammy} PLAYWRIGHT_BASE_IMAGE: ${PLAYWRIGHT_BASE_IMAGE:-swr.cn-north-4.myhuaweicloud.com/ddn-k8s/mcr.microsoft.com/playwright/python:v1.56.0-jammy}
NODE_RUNTIME_IMAGE: ${NODE_RUNTIME_IMAGE:-node:22-bookworm-slim} NODE_RUNTIME_IMAGE: ${NODE_RUNTIME_IMAGE:-node:22-bookworm-slim}
HTTP_PROXY: ${HTTP_PROXY_BUILD:-}
HTTPS_PROXY: ${HTTPS_PROXY_BUILD:-}
ALL_PROXY: ${ALL_PROXY_BUILD:-}
http_proxy: ${HTTP_PROXY_BUILD:-}
https_proxy: ${HTTPS_PROXY_BUILD:-}
all_proxy: ${ALL_PROXY_BUILD:-}
PIP_INDEX_URL: ${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple} PIP_INDEX_URL: ${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple}
PIP_TRUSTED_HOST: ${PIP_TRUSTED_HOST:-pypi.tuna.tsinghua.edu.cn} PIP_TRUSTED_HOST: ${PIP_TRUSTED_HOST:-pypi.tuna.tsinghua.edu.cn}
image: douyin-sparkflow:local image: douyin-sparkflow:local
@@ -69,10 +63,12 @@ services:
LOGIN_DESKTOP_IDLE_TIMEOUT_SECONDS: ${LOGIN_DESKTOP_IDLE_TIMEOUT_SECONDS:-1800} LOGIN_DESKTOP_IDLE_TIMEOUT_SECONDS: ${LOGIN_DESKTOP_IDLE_TIMEOUT_SECONDS:-1800}
LOGIN_DESKTOP_STOP_AFTER_EXPORT_SECONDS: ${LOGIN_DESKTOP_STOP_AFTER_EXPORT_SECONDS:-60} LOGIN_DESKTOP_STOP_AFTER_EXPORT_SECONDS: ${LOGIN_DESKTOP_STOP_AFTER_EXPORT_SECONDS:-60}
LOGIN_DESKTOP_STATUS_CACHE_SECONDS: ${LOGIN_DESKTOP_STATUS_CACHE_SECONDS:-15} LOGIN_DESKTOP_STATUS_CACHE_SECONDS: ${LOGIN_DESKTOP_STATUS_CACHE_SECONDS:-15}
LOGIN_DESKTOP_PROXY_MODE: ${LOGIN_DESKTOP_PROXY_MODE:-direct} LOGIN_DESKTOP_PROXY_MODE: ${LOGIN_DESKTOP_PROXY_MODE:-direct}
LOGIN_DESKTOP_PROXY: ${LOGIN_DESKTOP_PROXY:-http://proxy:7890} LOGIN_DESKTOP_PROXY: ${LOGIN_DESKTOP_PROXY:-http://proxy:7890}
LOGIN_DESKTOP_PREFLIGHT_TIMEOUT_SECONDS: ${LOGIN_DESKTOP_PREFLIGHT_TIMEOUT_SECONDS:-15} LOGIN_DESKTOP_PREFLIGHT_TIMEOUT_SECONDS: ${LOGIN_DESKTOP_PREFLIGHT_TIMEOUT_SECONDS:-15}
LOGIN_DESKTOP_NETWORK_CACHE_SECONDS: ${LOGIN_DESKTOP_NETWORK_CACHE_SECONDS:-30} LOGIN_DESKTOP_NETWORK_CACHE_SECONDS: ${LOGIN_DESKTOP_NETWORK_CACHE_SECONDS:-30}
ports: ports:
- "${LOGIN_DESKTOP_BIND_ADDRESS:-127.0.0.1}:${LOGIN_DESKTOP_WEB_PORT:-8788}:6080" - "${LOGIN_DESKTOP_BIND_ADDRESS:-127.0.0.1}:${LOGIN_DESKTOP_WEB_PORT:-8788}:6080"
command: bash /app/scripts/start_login_desktop.sh command: bash /app/scripts/start_login_desktop.sh