From 132f799d3c63f2f78638e1ee80071c6e10a3a7f6 Mon Sep 17 00:00:00 2001 From: Rixuan Shao <2023311022@bipt.edu.cn> Date: Sat, 30 May 2026 00:39:14 +0800 Subject: [PATCH] Add Docker Compose one-click deployment --- .env.example | 19 ++++ .gitignore | 1 + DouYinSparkFlow/Dockerfile.server | 8 +- DouYinSparkFlow/scripts/cron_runner.py | 88 +++++++++++++++ DouYinSparkFlow/webui/app.py | 6 +- README.md | 103 +++++++++++++----- deploy/install-local.ps1 | 124 ++++++++++++++++++++++ deploy/install-local.sh | 59 +++++++++++ deploy/install-server.sh | 141 +++++++++++++++++++++++++ docker-compose.yml | 51 ++++++--- proxy/config.example.yaml | 9 ++ refresh_proxy.sh | 74 +++++++++++-- 12 files changed, 634 insertions(+), 49 deletions(-) create mode 100644 .env.example create mode 100644 DouYinSparkFlow/scripts/cron_runner.py create mode 100644 deploy/install-local.ps1 create mode 100755 deploy/install-local.sh create mode 100755 deploy/install-server.sh create mode 100644 proxy/config.example.yaml mode change 100644 => 100755 refresh_proxy.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f67f7a5 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore index e9ebafb..e49275a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .env state/ logs/ +proxy/config.yaml *.bak-* *.tar.gz diff --git a/DouYinSparkFlow/Dockerfile.server b/DouYinSparkFlow/Dockerfile.server index cda29ba..a4d8eaa 100644 --- a/DouYinSparkFlow/Dockerfile.server +++ b/DouYinSparkFlow/Dockerfile.server @@ -25,7 +25,8 @@ ENV PIP_TRUSTED_HOST=${PIP_TRUSTED_HOST} COPY 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 \ && ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime \ && echo ${TZ} > /etc/timezone \ @@ -39,13 +40,14 @@ RUN sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \ websockify \ x11vnc \ 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 \ && mv docker/docker /usr/bin/docker \ && chmod +x /usr/bin/docker \ && rm -rf docker docker.tgz \ && 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 \ && rm -rf /var/lib/apt/lists/* diff --git a/DouYinSparkFlow/scripts/cron_runner.py b/DouYinSparkFlow/scripts/cron_runner.py new file mode 100644 index 0000000..40a5213 --- /dev/null +++ b/DouYinSparkFlow/scripts/cron_runner.py @@ -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) diff --git a/DouYinSparkFlow/webui/app.py b/DouYinSparkFlow/webui/app.py index 2d4eda9..7efcf50 100644 --- a/DouYinSparkFlow/webui/app.py +++ b/DouYinSparkFlow/webui/app.py @@ -1,5 +1,6 @@ import json import logging +import os import traceback from datetime import datetime, timedelta, timezone from pathlib import Path @@ -132,13 +133,14 @@ def _target_sent_today(account, target_name): def login_desktop_api_url(): 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: host = request.url.hostname or "127.0.0.1" 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: diff --git a/README.md b/README.md index 0ee93d2..a4df03e 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,96 @@ # douyin-sparkflow -这个仓库是一个围绕 `DouYinSparkFlow/` 组织的部署仓库,用于保存核心应用源码、代理配置和容器编排文件,方便在本地或服务器上统一维护。当前整理目标是“私有内用、可放入 GitHub、避免提交运行态和敏感数据”。 +Douyin SparkFlow 是一个用于自动续火花的 Web 管理版部署包。推荐用 Docker Compose 部署,启动后在浏览器里完成管理员密码、扫码登录、目标好友勾选和发送窗口设置。 -## 仓库结构 +## 一键部署到服务器 -- `DouYinSparkFlow/`: 核心应用源码,包含 Web UI、任务调度、账号登录与消息发送逻辑。 -- `proxy/`: 代理容器配置目录,当前包含 `mihomo` 配置文件。 -- `docker-compose.yml`: 统一的容器编排入口。 -- `refresh_proxy.sh`: 代理刷新脚本。 +适合 Ubuntu/Debian/CentOS 类服务器: -## 运行方式概览 +```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` - `state/` - `logs/` +- `proxy/config.yaml` +- `DouYinSparkFlow/logs/` - `DouYinSparkFlow/usersData.json` - `DouYinSparkFlow/webui_settings.json` - `DouYinSparkFlow/.im_sdk_cache/` -如果需要复现运行环境,建议在目标机器上重新生成这些文件,而不是从仓库恢复。 +## 许可 -## 使用边界 - -- 本仓库按内部项目资料整理,主要用于源码管理、部署维护和环境迁移。 -- 使用者需要自行评估平台规则、账号风险和运行后果。 -- 不建议把真实账号数据、浏览器登录态、日志或运行缓存提交到仓库。 - -## 许可证 - -核心应用当前采用 MIT 协议,许可证文件位于 [DouYinSparkFlow/LICENSE](DouYinSparkFlow/LICENSE)。 - -如需查看源码级说明,请优先阅读 [DouYinSparkFlow/README.md](DouYinSparkFlow/README.md)。 +核心应用采用 MIT 协议,详见 [DouYinSparkFlow/LICENSE](DouYinSparkFlow/LICENSE)。 diff --git a/deploy/install-local.ps1 b/deploy/install-local.ps1 new file mode 100644 index 0000000..62ae903 --- /dev/null +++ b/deploy/install-local.ps1 @@ -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 +} diff --git a/deploy/install-local.sh b/deploy/install-local.sh new file mode 100755 index 0000000..c38c63b --- /dev/null +++ b/deploy/install-local.sh @@ -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 diff --git a/deploy/install-server.sh b/deploy/install-server.sh new file mode 100755 index 0000000..bb442d4 --- /dev/null +++ b/deploy/install-server.sh @@ -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 "$@" diff --git a/docker-compose.yml b/docker-compose.yml index 7eb922e..71567c9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,10 +9,10 @@ services: max-size: "20m" max-file: "3" ports: - - "7890:7890" - - "9090:9090" + - "${PROXY_HTTP_PORT:-7890}:7890" + - "${PROXY_CONTROLLER_PORT:-9090}:9090" volumes: - - ./proxy/config.yaml:/root/.config/mihomo/config.yaml + - ./proxy/config.yaml:/root/.config/mihomo/config.yaml:ro web: build: @@ -20,9 +20,14 @@ services: dockerfile: Dockerfile.server network: host args: - HTTP_PROXY: http://127.0.0.1:7890 - HTTPS_PROXY: http://127.0.0.1:7890 - ALL_PROXY: socks5://127.0.0.1:7890 + 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_TRUSTED_HOST: ${PIP_TRUSTED_HOST:-pypi.tuna.tsinghua.edu.cn} image: douyin-sparkflow:local container_name: douyin-web restart: unless-stopped @@ -30,20 +35,22 @@ services: - proxy - login-desktop environment: - TZ: Asia/Shanghai + TZ: ${TZ:-Asia/Shanghai} HTTP_PROXY: http://proxy:7890 HTTPS_PROXY: http://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 + SPARKFLOW_LOGIN_DESKTOP_API_URL: http://login-desktop:18090 + LOGIN_DESKTOP_PUBLIC_PORT: ${LOGIN_DESKTOP_WEB_PORT:-8788} ports: - - "8787:8787" + - "${WEB_PORT:-8787}:8787" command: python main.py --web --host 0.0.0.0 --port 8787 volumes: - ./DouYinSparkFlow:/app - ./DouYinSparkFlow/logs:/app/logs + - ./state/cron:/host-spool-cron - /var/run/docker.sock:/var/run/docker.sock - - /opt/douyin-sparkflow:/opt/douyin-sparkflow - - /var/spool/cron/root:/var/spool/cron/crontabs/root + - .:/opt/douyin-sparkflow login-desktop: image: douyin-sparkflow:local @@ -52,27 +59,45 @@ services: depends_on: - proxy environment: - TZ: Asia/Shanghai + TZ: ${TZ:-Asia/Shanghai} DISPLAY: :99 + LOGIN_DESKTOP_API_PORT: "18090" + LOGIN_DESKTOP_WEB_PORT: "6080" HTTP_PROXY: http://proxy:7890 HTTPS_PROXY: http://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 ports: - - "8788:6080" + - "${LOGIN_DESKTOP_WEB_PORT:-8788}:6080" command: bash /app/scripts/start_login_desktop.sh volumes: - ./DouYinSparkFlow:/app - ./DouYinSparkFlow/logs:/app/logs - ./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: image: douyin-sparkflow:local container_name: douyin-task depends_on: - proxy environment: - TZ: Asia/Shanghai + TZ: ${TZ:-Asia/Shanghai} HTTP_PROXY: http://proxy:7890 HTTPS_PROXY: http://proxy:7890 ALL_PROXY: socks5://proxy:7890 diff --git a/proxy/config.example.yaml b/proxy/config.example.yaml new file mode 100644 index 0000000..77a9abb --- /dev/null +++ b/proxy/config.example.yaml @@ -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 diff --git a/refresh_proxy.sh b/refresh_proxy.sh old mode 100644 new mode 100755 index d600b80..626745f --- a/refresh_proxy.sh +++ b/refresh_proxy.sh @@ -1,9 +1,69 @@ #!/usr/bin/env bash set -euo pipefail -APP_ROOT=/opt/douyin-sparkflow -SUB_URL='https://liangxin.xyz/api/v1/liangxin?OwO=6981c5a8452d44e8521c78f9f7bf1eea' -curl -fsSL -A 'clash-verge/1.7.7' "$SUB_URL" -o "$APP_ROOT/proxy/config.yaml" -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 -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 -docker compose -f "$APP_ROOT/docker-compose.yml" restart proxy + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_ROOT_OVERRIDE="${APP_ROOT:-}" +ENV_FILE="$SCRIPT_DIR/.env" + +read_env_value() { + 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