mirror of
https://github.com/halfwaystudent/douyin-sparkflow.git
synced 2026-08-29 03:57:07 +08:00
Add Docker Compose one-click deployment
This commit is contained in:
@@ -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,6 +1,7 @@
|
||||
.env
|
||||
state/
|
||||
logs/
|
||||
proxy/config.yaml
|
||||
*.bak-*
|
||||
*.tar.gz
|
||||
|
||||
|
||||
@@ -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/*
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Executable
+59
@@ -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
|
||||
Executable
+141
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user