Add Docker Compose one-click deployment

This commit is contained in:
Rixuan Shao
2026-05-30 00:39:14 +08:00
parent 11a6c949db
commit 132f799d3c
12 changed files with 634 additions and 49 deletions
+19
View File
@@ -0,0 +1,19 @@
APP_ROOT=/opt/douyin-sparkflow
TZ=Asia/Shanghai
WEB_PORT=8787
LOGIN_DESKTOP_WEB_PORT=8788
PROXY_HTTP_PORT=7890
PROXY_CONTROLLER_PORT=9090
# Optional: paste a Mihomo/Clash subscription URL here, then run ./refresh_proxy.sh.
PROXY_SUB_URL=
PROXY_USER_AGENT=clash-verge/1.7.7
# Optional build-time proxy. Leave empty unless your Docker build needs a proxy.
HTTP_PROXY_BUILD=
HTTPS_PROXY_BUILD=
ALL_PROXY_BUILD=
PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
PIP_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn
+1
View File
@@ -1,6 +1,7 @@
.env .env
state/ state/
logs/ logs/
proxy/config.yaml
*.bak-* *.bak-*
*.tar.gz *.tar.gz
+5 -3
View File
@@ -25,7 +25,8 @@ ENV PIP_TRUSTED_HOST=${PIP_TRUSTED_HOST}
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
RUN sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \ RUN set -eux; \
sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& sed -i 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \ && sed -i 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime \ && ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime \
&& echo ${TZ} > /etc/timezone \ && echo ${TZ} > /etc/timezone \
@@ -39,13 +40,14 @@ RUN sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \
websockify \ websockify \
x11vnc \ x11vnc \
xfonts-intl-chinese \ xfonts-intl-chinese \
&& curl -fsSL -x http://127.0.0.1:7890 https://download.docker.com/linux/static/stable/x86_64/docker-25.0.3.tgz -o docker.tgz \ && download() { url="$1"; output="$2"; proxy_url="${HTTPS_PROXY:-${https_proxy:-${HTTP_PROXY:-${http_proxy:-}}}}"; if [ -n "$proxy_url" ]; then curl -fsSL -x "$proxy_url" "$url" -o "$output"; else curl -fsSL "$url" -o "$output"; fi; } \
&& download https://download.docker.com/linux/static/stable/x86_64/docker-25.0.3.tgz docker.tgz \
&& tar xzvf docker.tgz \ && tar xzvf docker.tgz \
&& mv docker/docker /usr/bin/docker \ && mv docker/docker /usr/bin/docker \
&& chmod +x /usr/bin/docker \ && chmod +x /usr/bin/docker \
&& rm -rf docker docker.tgz \ && rm -rf docker docker.tgz \
&& mkdir -p /usr/local/lib/docker/cli-plugins \ && mkdir -p /usr/local/lib/docker/cli-plugins \
&& curl -SL -x http://127.0.0.1:7890 https://github.com/docker/compose/releases/download/v2.24.5/docker-compose-linux-x86_64 -o /usr/local/lib/docker/cli-plugins/docker-compose \ && download https://github.com/docker/compose/releases/download/v2.24.5/docker-compose-linux-x86_64 /usr/local/lib/docker/cli-plugins/docker-compose \
&& chmod +x /usr/local/lib/docker/cli-plugins/docker-compose \ && chmod +x /usr/local/lib/docker/cli-plugins/docker-compose \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
+88
View File
@@ -0,0 +1,88 @@
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
def expand_field(field, minimum, maximum, current):
values = set()
for part in str(field).split(","):
part = part.strip()
if not part:
continue
step = 1
if "/" in part:
part, raw_step = part.split("/", 1)
step = max(1, int(raw_step))
if part == "*":
start, end = minimum, maximum
elif "-" in part:
raw_start, raw_end = part.split("-", 1)
start, end = int(raw_start), int(raw_end)
else:
start = end = int(part)
values.update(range(max(minimum, start), min(maximum, end) + 1, step))
return current in values
def cron_dow(now):
return (now.weekday() + 1) % 7
def field_matches(field, minimum, maximum, current, *, allow_sunday_alias=False):
if allow_sunday_alias and current == 0:
return expand_field(field, minimum, maximum, 0) or expand_field(field, minimum, maximum, 7)
return expand_field(field, minimum, maximum, current)
def line_should_run(line, now):
parts = line.split(maxsplit=5)
if len(parts) < 6:
return False, ""
minute, hour, day, month, weekday, command = parts
try:
matched = (
field_matches(minute, 0, 59, now.minute)
and field_matches(hour, 0, 23, now.hour)
and field_matches(day, 1, 31, now.day)
and field_matches(month, 1, 12, now.month)
and field_matches(weekday, 0, 7, cron_dow(now), allow_sunday_alias=True)
)
except ValueError:
return False, ""
return matched, command
def read_crontab(path):
if not path.exists():
return []
lines = []
for raw_line in path.read_text(encoding="utf-8", errors="replace").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" in line.split(maxsplit=1)[0]:
continue
lines.append(line)
return lines
def run_loop(crontab_path):
crontab_path.parent.mkdir(parents=True, exist_ok=True)
last_minute_key = None
print(f"[cron_runner] watching {crontab_path}", flush=True)
while True:
now = datetime.now()
minute_key = now.strftime("%Y-%m-%d %H:%M")
if minute_key != last_minute_key:
last_minute_key = minute_key
for line in read_crontab(crontab_path):
matched, command = line_should_run(line, now)
if matched and command:
print(f"[cron_runner] {now.isoformat(timespec='seconds')} run: {command}", flush=True)
subprocess.Popen(["/bin/bash", "-lc", command])
time.sleep(15)
if __name__ == "__main__":
target = Path(sys.argv[1] if len(sys.argv) > 1 else "/host-spool-cron/root")
run_loop(target)
+4 -2
View File
@@ -1,5 +1,6 @@
import json import json
import logging import logging
import os
import traceback import traceback
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
@@ -132,13 +133,14 @@ def _target_sent_today(account, target_name):
def login_desktop_api_url(): def login_desktop_api_url():
settings = get_app_settings(force_reload=True) settings = get_app_settings(force_reload=True)
return str(settings.get("login_desktop_api_url") or "http://127.0.0.1:18090").rstrip("/") return str(os.getenv("SPARKFLOW_LOGIN_DESKTOP_API_URL") or settings.get("login_desktop_api_url") or "http://127.0.0.1:18090").rstrip("/")
def login_desktop_public_url(request: Request) -> str: def login_desktop_public_url(request: Request) -> str:
host = request.url.hostname or "127.0.0.1" host = request.url.hostname or "127.0.0.1"
scheme = request.url.scheme or "http" scheme = request.url.scheme or "http"
return f"{scheme}://{host}:8788/vnc.html?autoconnect=1&resize=scale&view_only=0" port = str(os.getenv("LOGIN_DESKTOP_PUBLIC_PORT") or "8788").strip() or "8788"
return f"{scheme}://{host}:{port}/vnc.html?autoconnect=1&resize=scale&view_only=0"
def call_login_desktop(path: str, *, method: str = "GET", payload: dict | None = None, timeout: int = 20) -> dict: def call_login_desktop(path: str, *, method: str = "GET", payload: dict | None = None, timeout: int = 20) -> dict:
+79 -24
View File
@@ -1,41 +1,96 @@
# douyin-sparkflow # douyin-sparkflow
这个仓库是一个围绕 `DouYinSparkFlow/` 组织的部署仓库,用于保存核心应用源码、代理配置和容器编排文件,方便在本地或服务器上统一维护。当前整理目标是“私有内用、可放入 GitHub、避免提交运行态和敏感数据” Douyin SparkFlow 是一个用于自动续火花的 Web 管理版部署包。推荐用 Docker Compose 部署,启动后在浏览器里完成管理员密码、扫码登录、目标好友勾选和发送窗口设置
## 仓库结构 ## 一键部署到服务器
- `DouYinSparkFlow/`: 核心应用源码,包含 Web UI、任务调度、账号登录与消息发送逻辑。 适合 Ubuntu/Debian/CentOS 类服务器:
- `proxy/`: 代理容器配置目录,当前包含 `mihomo` 配置文件。
- `docker-compose.yml`: 统一的容器编排入口。
- `refresh_proxy.sh`: 代理刷新脚本。
## 运行方式概览 ```bash
curl -fsSL https://raw.githubusercontent.com/halfwaystudent/douyin-sparkflow/main/deploy/install-server.sh | bash
```
- 支持本地运行核心应用,也支持通过 `docker-compose.yml` 在服务器上部署。 也可以指定安装目录或代理订阅:
- Web 管理入口、交互式登录桌面和定时任务都由仓库内现有脚本与配置驱动。
- 定时发送、账号状态和消息模板属于运行时行为,不在本仓库中直接携带账号数据。
## 配置与敏感文件 ```bash
curl -fsSL https://raw.githubusercontent.com/halfwaystudent/douyin-sparkflow/main/deploy/install-server.sh | APP_ROOT=/opt/douyin-sparkflow PROXY_SUB_URL='你的 Mihomo/Clash 订阅链接' bash
```
以下内容不会进入当前 Git 仓库,需要在实际部署环境中自行补齐 部署完成后打开
- Web 面板:`http://服务器IP:8787`
- 扫码登录桌面:Web 面板里的“登录桌面”入口,默认端口 `8788`
首次使用流程:
1. 创建管理员账号密码。
2. 打开登录桌面,扫码登录抖音。
3. 保存登录态。
4. 刷新好友列表,勾选要续火花的目标好友。
5. 设置发送窗口,例如 `10:00-18:00/10m`
## 本地部署
Windows 本地需要先启动 Docker Desktop,然后运行:
```powershell
.\deploy\install-local.ps1
```
带代理订阅:
```powershell
.\deploy\install-local.ps1 -ProxySubUrl "你的 Mihomo/Clash 订阅链接"
```
脚本会创建 `.env`、运行态目录和默认代理配置,然后执行 `docker compose up -d --build` 并打开 Web 面板。
Linux/macOS 本地可以运行:
```bash
./deploy/install-local.sh
```
## 常用命令
```bash
docker compose ps
docker compose logs -f web
docker compose logs -f scheduler
docker compose up -d --build
docker compose down
```
刷新代理订阅:
```bash
./refresh_proxy.sh
docker compose restart proxy
```
如果 `.env` 里的 `PROXY_SUB_URL` 为空,系统会使用 `proxy/config.example.yaml` 生成一个直连配置。
## 目录结构
- `DouYinSparkFlow/`:核心应用、Web UI、登录桌面和发送任务。
- `docker-compose.yml`:统一容器编排入口,包含 Web、登录桌面、定时器、任务和代理服务。
- `deploy/`:服务器和本地一键部署脚本。
- `proxy/config.example.yaml`:安全的代理配置模板。
- `.env.example`:部署环境变量模板。
## 不提交的运行态文件
这些内容包含账号、登录态、日志或本机配置,不应提交到 GitHub:
- `.env` - `.env`
- `state/` - `state/`
- `logs/` - `logs/`
- `proxy/config.yaml`
- `DouYinSparkFlow/logs/`
- `DouYinSparkFlow/usersData.json` - `DouYinSparkFlow/usersData.json`
- `DouYinSparkFlow/webui_settings.json` - `DouYinSparkFlow/webui_settings.json`
- `DouYinSparkFlow/.im_sdk_cache/` - `DouYinSparkFlow/.im_sdk_cache/`
如果需要复现运行环境,建议在目标机器上重新生成这些文件,而不是从仓库恢复。 ## 许可
## 使用边界 核心应用采用 MIT 协议,详见 [DouYinSparkFlow/LICENSE](DouYinSparkFlow/LICENSE)。
- 本仓库按内部项目资料整理,主要用于源码管理、部署维护和环境迁移。
- 使用者需要自行评估平台规则、账号风险和运行后果。
- 不建议把真实账号数据、浏览器登录态、日志或运行缓存提交到仓库。
## 许可证
核心应用当前采用 MIT 协议,许可证文件位于 [DouYinSparkFlow/LICENSE](DouYinSparkFlow/LICENSE)。
如需查看源码级说明,请优先阅读 [DouYinSparkFlow/README.md](DouYinSparkFlow/README.md)。
+124
View File
@@ -0,0 +1,124 @@
param(
[string]$ProxySubUrl = "",
[switch]$NoOpen
)
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
Set-Location $repoRoot
function Require-Command {
param([string]$Name)
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
throw "$Name is required. Please install Docker Desktop and make sure Docker Compose is available."
}
}
function Set-EnvValue {
param(
[string]$Path,
[string]$Key,
[string]$Value
)
$line = "$Key=$Value"
if (-not (Test-Path $Path)) {
Set-Content -Path $Path -Value $line -Encoding utf8
return
}
$content = Get-Content -Path $Path -ErrorAction SilentlyContinue
$found = $false
$escapedKey = [regex]::Escape($Key)
$next = foreach ($item in $content) {
if ($item -match "^$escapedKey=") {
$found = $true
$line
} else {
$item
}
}
if (-not $found) {
$next = @($next) + $line
}
Set-Content -Path $Path -Value $next -Encoding utf8
}
function Get-EnvValue {
param(
[string]$Path,
[string]$Key,
[string]$DefaultValue
)
if (Test-Path $Path) {
$escapedKey = [regex]::Escape($Key)
$match = Get-Content -Path $Path | Where-Object { $_ -match "^$escapedKey=" } | Select-Object -First 1
if ($match) {
return ($match -replace "^$escapedKey=", "")
}
}
return $DefaultValue
}
function Set-ProxyConfigLine {
param(
[string]$Path,
[string]$Key,
[string]$Value
)
$line = "${Key}: $Value"
$content = Get-Content -Path $Path -ErrorAction SilentlyContinue
$escapedKey = [regex]::Escape($Key)
$found = $false
$next = foreach ($item in $content) {
if ($item -match "^${escapedKey}:") {
$found = $true
$line
} else {
$item
}
}
if (-not $found) {
$next = @($next) + $line
}
Set-Content -Path $Path -Value $next -Encoding utf8
}
function Refresh-ProxyConfig {
param([string]$Url)
if (-not $Url) {
return
}
$userAgent = Get-EnvValue -Path ".env" -Key "PROXY_USER_AGENT" -DefaultValue "clash-verge/1.7.7"
Invoke-WebRequest -Uri $Url -Headers @{ "User-Agent" = $userAgent } -OutFile "proxy/config.yaml"
Set-ProxyConfigLine -Path "proxy/config.yaml" -Key "mixed-port" -Value "7890"
Set-ProxyConfigLine -Path "proxy/config.yaml" -Key "allow-lan" -Value "true"
Set-ProxyConfigLine -Path "proxy/config.yaml" -Key "bind-address" -Value "'*'"
Set-ProxyConfigLine -Path "proxy/config.yaml" -Key "external-controller" -Value "'0.0.0.0:9090'"
}
Require-Command docker
docker compose version | Out-Null
if (-not (Test-Path ".env")) {
Copy-Item ".env.example" ".env"
}
if ($ProxySubUrl) {
Set-EnvValue -Path ".env" -Key "PROXY_SUB_URL" -Value $ProxySubUrl
}
New-Item -ItemType Directory -Force -Path "proxy", "state/cron", "state/login-profile", "DouYinSparkFlow/logs" | Out-Null
if (-not (Test-Path "proxy/config.yaml")) {
Copy-Item "proxy/config.example.yaml" "proxy/config.yaml"
}
Refresh-ProxyConfig -Url $ProxySubUrl
docker compose up -d --build
$webPort = Get-EnvValue -Path ".env" -Key "WEB_PORT" -DefaultValue "8787"
$url = "http://localhost:$webPort"
Write-Host "Douyin SparkFlow is running: $url"
Write-Host "Next: create the admin password, open the login desktop, scan the QR code, select target friends, and set the send window."
if (-not $NoOpen) {
Start-Process $url
}
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT"
if ! command -v docker >/dev/null 2>&1 || ! docker compose version >/dev/null 2>&1; then
echo "Docker with the Compose plugin is required." >&2
exit 1
fi
set_env_value() {
local file="$1"
local key="$2"
local value="$3"
if grep -q "^${key}=" "$file"; then
local tmp_file
tmp_file="$(mktemp)"
awk -v key="$key" -v value="$value" '
BEGIN { replaced = 0 }
$0 ~ "^" key "=" { print key "=" value; replaced = 1; next }
{ print }
END { if (!replaced) print key "=" value }
' "$file" > "$tmp_file"
mv "$tmp_file" "$file"
else
printf '%s=%s\n' "$key" "$value" >> "$file"
fi
}
if [ ! -f ".env" ]; then
cp ".env.example" ".env"
fi
proxy_sub_url="${PROXY_SUB_URL:-${1:-}}"
if [ -n "$proxy_sub_url" ]; then
set_env_value ".env" "PROXY_SUB_URL" "$proxy_sub_url"
fi
mkdir -p proxy state/cron state/login-profile DouYinSparkFlow/logs
if [ ! -f "proxy/config.yaml" ]; then
cp "proxy/config.example.yaml" "proxy/config.yaml"
fi
bash ./refresh_proxy.sh
docker compose up -d --build
web_port="$(grep '^WEB_PORT=' .env | sed 's/^WEB_PORT=//')"
web_port="${web_port:-8787}"
url="http://localhost:${web_port}"
echo "Douyin SparkFlow is running: $url"
echo "Next: create the admin password, open the login desktop, scan the QR code, select target friends, and set the send window."
if command -v xdg-open >/dev/null 2>&1; then
xdg-open "$url" >/dev/null 2>&1 || true
elif command -v open >/dev/null 2>&1; then
open "$url" >/dev/null 2>&1 || true
fi
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_URL="${REPO_URL:-https://github.com/halfwaystudent/douyin-sparkflow.git}"
BRANCH="${BRANCH:-main}"
APP_ROOT="${APP_ROOT:-/opt/douyin-sparkflow}"
if [ "$(id -u)" -ne 0 ]; then
SUDO="sudo"
else
SUDO=""
fi
run_root() {
if [ -n "$SUDO" ]; then
sudo "$@"
else
"$@"
fi
}
install_base_tools() {
if command -v apt-get >/dev/null 2>&1; then
run_root apt-get update
run_root apt-get install -y ca-certificates curl git gnupg
elif command -v yum >/dev/null 2>&1; then
run_root yum install -y ca-certificates curl git
fi
}
install_docker_debian() {
. /etc/os-release
local docker_id="${ID}"
if [ "$docker_id" = "debian" ] || [ "$docker_id" = "ubuntu" ]; then
run_root install -m 0755 -d /etc/apt/keyrings
curl -fsSL "https://download.docker.com/linux/${docker_id}/gpg" | run_root tee /etc/apt/keyrings/docker.asc >/dev/null
run_root chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${docker_id} ${VERSION_CODENAME} stable" | run_root tee /etc/apt/sources.list.d/docker.list >/dev/null
run_root apt-get update
run_root apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
else
run_root apt-get install -y docker.io docker-compose-plugin
fi
}
ensure_docker() {
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
return
fi
if command -v apt-get >/dev/null 2>&1; then
install_docker_debian
elif command -v yum >/dev/null 2>&1; then
run_root yum install -y docker docker-compose-plugin
else
echo "Docker is not installed. Please install Docker with the Compose plugin first." >&2
exit 1
fi
run_root systemctl enable --now docker || true
}
prepare_repo() {
run_root mkdir -p "$(dirname "$APP_ROOT")"
if [ -d "$APP_ROOT/.git" ]; then
run_root git -C "$APP_ROOT" fetch origin "$BRANCH"
run_root git -C "$APP_ROOT" checkout "$BRANCH"
run_root git -C "$APP_ROOT" pull --ff-only origin "$BRANCH"
else
run_root git clone --branch "$BRANCH" "$REPO_URL" "$APP_ROOT"
fi
}
set_env_value() {
local file="$1"
local key="$2"
local value="$3"
if grep -q "^${key}=" "$file"; then
local tmp_file
tmp_file="$(mktemp)"
awk -v key="$key" -v value="$value" '
BEGIN { replaced = 0 }
$0 ~ "^" key "=" { print key "=" value; replaced = 1; next }
{ print }
END { if (!replaced) print key "=" value }
' "$file" > "$tmp_file"
run_root cp "$tmp_file" "$file"
rm -f "$tmp_file"
else
printf '%s=%s\n' "$key" "$value" | run_root tee -a "$file" >/dev/null
fi
}
prepare_runtime_files() {
if [ ! -f "$APP_ROOT/.env" ]; then
run_root cp "$APP_ROOT/.env.example" "$APP_ROOT/.env"
fi
set_env_value "$APP_ROOT/.env" "APP_ROOT" "$APP_ROOT"
for key in TZ WEB_PORT LOGIN_DESKTOP_WEB_PORT PROXY_HTTP_PORT PROXY_CONTROLLER_PORT PROXY_SUB_URL HTTP_PROXY_BUILD HTTPS_PROXY_BUILD ALL_PROXY_BUILD; do
if [ -n "${!key:-}" ]; then
set_env_value "$APP_ROOT/.env" "$key" "${!key}"
fi
done
local current_sub
current_sub="$(grep '^PROXY_SUB_URL=' "$APP_ROOT/.env" | sed 's/^PROXY_SUB_URL=//' || true)"
if [ -z "$current_sub" ] && [ -t 0 ]; then
printf 'Proxy subscription URL, optional and hidden: '
read -r -s input_sub || true
printf '\n'
if [ -n "${input_sub:-}" ]; then
set_env_value "$APP_ROOT/.env" "PROXY_SUB_URL" "$input_sub"
fi
fi
run_root mkdir -p "$APP_ROOT/proxy" "$APP_ROOT/state/cron" "$APP_ROOT/state/login-profile" "$APP_ROOT/DouYinSparkFlow/logs"
if [ ! -f "$APP_ROOT/proxy/config.yaml" ]; then
run_root cp "$APP_ROOT/proxy/config.example.yaml" "$APP_ROOT/proxy/config.yaml"
fi
}
main() {
install_base_tools
ensure_docker
prepare_repo
prepare_runtime_files
cd "$APP_ROOT"
run_root bash "$APP_ROOT/refresh_proxy.sh"
run_root docker compose up -d --build
local web_port login_port host_ip
web_port="$(grep '^WEB_PORT=' "$APP_ROOT/.env" | sed 's/^WEB_PORT=//')"
login_port="$(grep '^LOGIN_DESKTOP_WEB_PORT=' "$APP_ROOT/.env" | sed 's/^LOGIN_DESKTOP_WEB_PORT=//')"
host_ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
host_ip="${host_ip:-127.0.0.1}"
echo
echo "Douyin SparkFlow is running."
echo "Web UI: http://${host_ip}:${web_port:-8787}"
echo "Login desktop: http://${host_ip}:${login_port:-8788}/vnc.html?autoconnect=1&resize=scale&view_only=0"
echo "Next: create the admin password, open the login desktop, scan the QR code, select target friends, and set the send window."
}
main "$@"
+38 -13
View File
@@ -9,10 +9,10 @@ services:
max-size: "20m" max-size: "20m"
max-file: "3" max-file: "3"
ports: ports:
- "7890:7890" - "${PROXY_HTTP_PORT:-7890}:7890"
- "9090:9090" - "${PROXY_CONTROLLER_PORT:-9090}:9090"
volumes: volumes:
- ./proxy/config.yaml:/root/.config/mihomo/config.yaml - ./proxy/config.yaml:/root/.config/mihomo/config.yaml:ro
web: web:
build: build:
@@ -20,9 +20,14 @@ services:
dockerfile: Dockerfile.server dockerfile: Dockerfile.server
network: host network: host
args: args:
HTTP_PROXY: http://127.0.0.1:7890 HTTP_PROXY: ${HTTP_PROXY_BUILD:-}
HTTPS_PROXY: http://127.0.0.1:7890 HTTPS_PROXY: ${HTTPS_PROXY_BUILD:-}
ALL_PROXY: socks5://127.0.0.1:7890 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_TRUSTED_HOST: ${PIP_TRUSTED_HOST:-pypi.tuna.tsinghua.edu.cn}
image: douyin-sparkflow:local image: douyin-sparkflow:local
container_name: douyin-web container_name: douyin-web
restart: unless-stopped restart: unless-stopped
@@ -30,20 +35,22 @@ services:
- proxy - proxy
- login-desktop - login-desktop
environment: environment:
TZ: Asia/Shanghai TZ: ${TZ:-Asia/Shanghai}
HTTP_PROXY: http://proxy:7890 HTTP_PROXY: http://proxy:7890
HTTPS_PROXY: http://proxy:7890 HTTPS_PROXY: http://proxy:7890
ALL_PROXY: socks5://proxy:7890 ALL_PROXY: socks5://proxy:7890
NO_PROXY: localhost,127.0.0.1,login-desktop,douyin.com,amemv.com,snssdk.com,bytedance.com,pstatp.com,volccdn.com,bytescm.com,byted.net,douyinstatic.com,bytecdn.cn,byteimg.com,bytegoofy.com,toutiaostatic.com NO_PROXY: localhost,127.0.0.1,login-desktop,douyin.com,amemv.com,snssdk.com,bytedance.com,pstatp.com,volccdn.com,bytescm.com,byted.net,douyinstatic.com,bytecdn.cn,byteimg.com,bytegoofy.com,toutiaostatic.com
SPARKFLOW_LOGIN_DESKTOP_API_URL: http://login-desktop:18090
LOGIN_DESKTOP_PUBLIC_PORT: ${LOGIN_DESKTOP_WEB_PORT:-8788}
ports: ports:
- "8787:8787" - "${WEB_PORT:-8787}:8787"
command: python main.py --web --host 0.0.0.0 --port 8787 command: python main.py --web --host 0.0.0.0 --port 8787
volumes: volumes:
- ./DouYinSparkFlow:/app - ./DouYinSparkFlow:/app
- ./DouYinSparkFlow/logs:/app/logs - ./DouYinSparkFlow/logs:/app/logs
- ./state/cron:/host-spool-cron
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- /opt/douyin-sparkflow:/opt/douyin-sparkflow - .:/opt/douyin-sparkflow
- /var/spool/cron/root:/var/spool/cron/crontabs/root
login-desktop: login-desktop:
image: douyin-sparkflow:local image: douyin-sparkflow:local
@@ -52,27 +59,45 @@ services:
depends_on: depends_on:
- proxy - proxy
environment: environment:
TZ: Asia/Shanghai TZ: ${TZ:-Asia/Shanghai}
DISPLAY: :99 DISPLAY: :99
LOGIN_DESKTOP_API_PORT: "18090"
LOGIN_DESKTOP_WEB_PORT: "6080"
HTTP_PROXY: http://proxy:7890 HTTP_PROXY: http://proxy:7890
HTTPS_PROXY: http://proxy:7890 HTTPS_PROXY: http://proxy:7890
ALL_PROXY: socks5://proxy:7890 ALL_PROXY: socks5://proxy:7890
NO_PROXY: localhost,127.0.0.1,login-desktop,douyin.com,amemv.com,snssdk.com,bytedance.com,pstatp.com,volccdn.com,bytescm.com,byted.net,douyinstatic.com,bytecdn.cn,byteimg.com,bytegoofy.com,toutiaostatic.com NO_PROXY: localhost,127.0.0.1,login-desktop,douyin.com,amemv.com,snssdk.com,bytedance.com,pstatp.com,volccdn.com,bytescm.com,byted.net,douyinstatic.com,bytecdn.cn,byteimg.com,bytegoofy.com,toutiaostatic.com
ports: ports:
- "8788:6080" - "${LOGIN_DESKTOP_WEB_PORT:-8788}:6080"
command: bash /app/scripts/start_login_desktop.sh command: bash /app/scripts/start_login_desktop.sh
volumes: volumes:
- ./DouYinSparkFlow:/app - ./DouYinSparkFlow:/app
- ./DouYinSparkFlow/logs:/app/logs - ./DouYinSparkFlow/logs:/app/logs
- ./state/login-profile:/data/login-profile - ./state/login-profile:/data/login-profile
scheduler:
image: douyin-sparkflow:local
container_name: douyin-scheduler
restart: unless-stopped
depends_on:
- web
environment:
TZ: ${TZ:-Asia/Shanghai}
PYTHONUNBUFFERED: "1"
command: python /app/scripts/cron_runner.py /host-spool-cron/root
volumes:
- ./DouYinSparkFlow:/app
- ./DouYinSparkFlow/logs:/app/logs
- ./state/cron:/host-spool-cron
- /var/run/docker.sock:/var/run/docker.sock
task: task:
image: douyin-sparkflow:local image: douyin-sparkflow:local
container_name: douyin-task container_name: douyin-task
depends_on: depends_on:
- proxy - proxy
environment: environment:
TZ: Asia/Shanghai TZ: ${TZ:-Asia/Shanghai}
HTTP_PROXY: http://proxy:7890 HTTP_PROXY: http://proxy:7890
HTTPS_PROXY: http://proxy:7890 HTTPS_PROXY: http://proxy:7890
ALL_PROXY: socks5://proxy:7890 ALL_PROXY: socks5://proxy:7890
+9
View File
@@ -0,0 +1,9 @@
mixed-port: 7890
allow-lan: true
bind-address: '*'
mode: rule
log-level: info
external-controller: '0.0.0.0:9090'
rules:
- MATCH,DIRECT
Regular → Executable
+67 -7
View File
@@ -1,9 +1,69 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
APP_ROOT=/opt/douyin-sparkflow
SUB_URL='https://liangxin.xyz/api/v1/liangxin?OwO=6981c5a8452d44e8521c78f9f7bf1eea' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
curl -fsSL -A 'clash-verge/1.7.7' "$SUB_URL" -o "$APP_ROOT/proxy/config.yaml" APP_ROOT_OVERRIDE="${APP_ROOT:-}"
if grep -q '^allow-lan:' "$APP_ROOT/proxy/config.yaml"; then sed -i 's/^allow-lan:.*/allow-lan: true/' "$APP_ROOT/proxy/config.yaml"; else echo 'allow-lan: true' >> "$APP_ROOT/proxy/config.yaml"; fi ENV_FILE="$SCRIPT_DIR/.env"
if grep -q '^bind-address:' "$APP_ROOT/proxy/config.yaml"; then sed -i "s#^bind-address:.*#bind-address: '*'#" "$APP_ROOT/proxy/config.yaml"; else echo "bind-address: '*'" >> "$APP_ROOT/proxy/config.yaml"; fi
if grep -q '^external-controller:' "$APP_ROOT/proxy/config.yaml"; then sed -i "s#^external-controller:.*#external-controller: '0.0.0.0:9090'#" "$APP_ROOT/proxy/config.yaml"; else echo "external-controller: '0.0.0.0:9090'" >> "$APP_ROOT/proxy/config.yaml"; fi read_env_value() {
docker compose -f "$APP_ROOT/docker-compose.yml" restart proxy local key="$1"
if [ ! -f "$ENV_FILE" ]; then
return 0
fi
local raw
raw="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d= -f2- || true)"
raw="${raw%\"}"
raw="${raw#\"}"
raw="${raw%\'}"
raw="${raw#\'}"
printf '%s' "$raw"
}
APP_ROOT="${APP_ROOT_OVERRIDE:-$SCRIPT_DIR}"
PROXY_SUB_URL="${PROXY_SUB_URL:-$(read_env_value PROXY_SUB_URL)}"
PROXY_USER_AGENT="${PROXY_USER_AGENT:-$(read_env_value PROXY_USER_AGENT)}"
CONFIG_DIR="$APP_ROOT/proxy"
CONFIG_FILE="$CONFIG_DIR/config.yaml"
EXAMPLE_CONFIG="$CONFIG_DIR/config.example.yaml"
USER_AGENT="${PROXY_USER_AGENT:-clash-verge/1.7.7}"
mkdir -p "$CONFIG_DIR"
if [ -n "${PROXY_SUB_URL:-}" ]; then
tmp_file="$(mktemp)"
curl -fsSL -A "$USER_AGENT" "$PROXY_SUB_URL" -o "$tmp_file"
mv "$tmp_file" "$CONFIG_FILE"
echo "Proxy subscription refreshed: $CONFIG_FILE"
elif [ ! -f "$CONFIG_FILE" ]; then
if [ ! -f "$EXAMPLE_CONFIG" ]; then
echo "Missing $CONFIG_FILE and $EXAMPLE_CONFIG. Set PROXY_SUB_URL or provide a Mihomo config." >&2
exit 1
fi
cp "$EXAMPLE_CONFIG" "$CONFIG_FILE"
echo "PROXY_SUB_URL is empty. Created a DIRECT-only proxy config from config.example.yaml."
else
echo "PROXY_SUB_URL is empty. Keeping existing proxy/config.yaml."
fi
ensure_line() {
local key="$1"
local value="$2"
if grep -q "^${key}:" "$CONFIG_FILE"; then
sed -i "s#^${key}:.*#${key}: ${value}#" "$CONFIG_FILE"
else
printf '%s: %s\n' "$key" "$value" >> "$CONFIG_FILE"
fi
}
ensure_line "mixed-port" "7890"
ensure_line "allow-lan" "true"
ensure_line "bind-address" "'*'"
ensure_line "external-controller" "'0.0.0.0:9090'"
if command -v docker >/dev/null 2>&1 && [ -f "$APP_ROOT/docker-compose.yml" ]; then
proxy_id="$(docker compose -f "$APP_ROOT/docker-compose.yml" ps -q proxy 2>/dev/null || true)"
if [ -n "$proxy_id" ]; then
docker compose -f "$APP_ROOT/docker-compose.yml" restart proxy
fi
fi