合并应用宝 Worker 容器
This commit is contained in:
+2
-2
@@ -9,8 +9,8 @@ logs/
|
|||||||
*.db
|
*.db
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# 前端依赖和构建产物(Docker 内重新构建)
|
# Node 依赖和前端构建产物(Docker 内重新构建)
|
||||||
web/frontend/node_modules/
|
**/node_modules/
|
||||||
web/frontend/dist/
|
web/frontend/dist/
|
||||||
|
|
||||||
# geetest 临时图片
|
# geetest 临时图片
|
||||||
|
|||||||
+7
-7
@@ -18,12 +18,12 @@ ADMIN_PASSWORD=admin123
|
|||||||
# Docker 对外端口(容器内仍监听 8800;服务器当前使用 8000)
|
# Docker 对外端口(容器内仍监听 8800;服务器当前使用 8000)
|
||||||
APP_PORT=8000
|
APP_PORT=8000
|
||||||
|
|
||||||
# Docker 构建镜像源(服务器保持 true;本地测试可改为 false)
|
# Docker 构建镜像源(网络可访问官方源时保持 false;国内服务器可改为 true)
|
||||||
USE_CHINA_MIRRORS=true
|
USE_CHINA_MIRRORS=false
|
||||||
|
|
||||||
# MySQL 配置(生产环境必须设置密码;默认只映射到宿主机 127.0.0.1,供本地 dev.sh 使用)
|
# MySQL 配置(生产环境必须设置密码;默认只映射到宿主机 127.0.0.1,供本地 dev.sh 使用)
|
||||||
# 服务器部署默认使用国内镜像;./dev.sh 默认临时改用 mysql:8.4,避免本地测试走国内镜像。
|
# 默认使用官方镜像;国内服务器可按需覆盖为可用镜像加速地址。
|
||||||
# MYSQL_IMAGE=docker.m.daocloud.io/library/mysql:8.4
|
# MYSQL_IMAGE=mysql:8.4
|
||||||
MYSQL_DATABASE=douyu_login
|
MYSQL_DATABASE=douyu_login
|
||||||
MYSQL_USER=douyu_login
|
MYSQL_USER=douyu_login
|
||||||
MYSQL_PASSWORD=
|
MYSQL_PASSWORD=
|
||||||
@@ -60,11 +60,11 @@ COOKIE_SECURE=false
|
|||||||
# CORS 允许的源(逗号分隔,不设则默认开发环境)
|
# CORS 允许的源(逗号分隔,不设则默认开发环境)
|
||||||
# CORS_ORIGINS=https://example.com,https://www.example.com
|
# CORS_ORIGINS=https://example.com,https://www.example.com
|
||||||
|
|
||||||
# 应用宝充值 Worker(Docker Compose 内网服务;生产环境必须设置为随机长密钥)
|
# 应用宝充值 Worker(与 Web 合并在同一容器;生产环境必须设置为随机长密钥)
|
||||||
# 生成方式: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
|
# 生成方式: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||||
YYB_WORKER_KEY=
|
YYB_WORKER_KEY=
|
||||||
# Docker 部署使用默认值;dev.sh 自动覆盖为 http://127.0.0.1:${YYB_WORKER_PORT:-8810}
|
# Docker 与 dev.sh 均通过本机 Worker 访问,无需设置 YYB_WORKER_URL。
|
||||||
# YYB_WORKER_URL=http://yyb-worker:8810
|
# YYB_WORKER_URL=http://127.0.0.1:8810
|
||||||
# YYB_WORKER_TIMEOUT=30
|
# YYB_WORKER_TIMEOUT=30
|
||||||
# 本地调试 Worker(仅 dev.sh 使用,默认只绑定 127.0.0.1)
|
# 本地调试 Worker(仅 dev.sh 使用,默认只绑定 127.0.0.1)
|
||||||
# YYB_WORKER_PORT=8810
|
# YYB_WORKER_PORT=8810
|
||||||
|
|||||||
+2
-2
@@ -20,8 +20,8 @@ bg.jpg
|
|||||||
fullbg.jpg
|
fullbg.jpg
|
||||||
slice.jpg
|
slice.jpg
|
||||||
|
|
||||||
# 前端
|
# Node 依赖
|
||||||
web/frontend/node_modules/
|
**/node_modules/
|
||||||
web/frontend/dist/
|
web/frontend/dist/
|
||||||
data/web.db
|
data/web.db
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
+23
-6
@@ -10,7 +10,17 @@ RUN npm config set registry https://registry.npmmirror.com && \
|
|||||||
COPY web/frontend/ ./
|
COPY web/frontend/ ./
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# ===== 阶段2: Python 运行时 =====
|
# ===== 阶段2: 构建应用宝 Worker 的 Node 依赖 =====
|
||||||
|
FROM node:20-slim AS yyb-node-builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
COPY services/yyb-worker/package.json services/yyb-worker/package-lock.json ./
|
||||||
|
# 优先 npmmirror,失败时回退官方 registry,避免国内构建时依赖下载中断。
|
||||||
|
RUN npm config set registry https://registry.npmmirror.com && \
|
||||||
|
npm ci --omit=dev || \
|
||||||
|
(npm config set registry https://registry.npmjs.org && npm ci --omit=dev)
|
||||||
|
|
||||||
|
# ===== 阶段3: Python 运行时 =====
|
||||||
FROM python:3.12-slim AS runtime
|
FROM python:3.12-slim AS runtime
|
||||||
|
|
||||||
ARG USE_CHINA_MIRRORS=true
|
ARG USE_CHINA_MIRRORS=true
|
||||||
@@ -61,11 +71,17 @@ COPY utils/ ./utils/
|
|||||||
COPY web/backend/ ./web/backend/
|
COPY web/backend/ ./web/backend/
|
||||||
COPY scripts/ ./scripts/
|
COPY scripts/ ./scripts/
|
||||||
|
|
||||||
|
# 将应用宝 Worker 合并进同一应用镜像;其 Python 依赖已纳入项目 uv.lock。
|
||||||
|
COPY services/yyb-worker/runtime/ ./services/yyb-worker/runtime/
|
||||||
|
COPY --from=yyb-node-builder /build/node_modules ./services/yyb-worker/runtime/node_modules
|
||||||
|
COPY docker-entrypoint.sh ./docker-entrypoint.sh
|
||||||
|
|
||||||
# 复制前端构建产物
|
# 复制前端构建产物
|
||||||
COPY --from=frontend-builder /build/dist ./web/frontend/dist
|
COPY --from=frontend-builder /build/dist ./web/frontend/dist
|
||||||
|
|
||||||
# 创建数据和日志目录
|
# 创建数据和日志目录
|
||||||
RUN mkdir -p /app/data /app/logs
|
RUN mkdir -p /app/data /app/logs /app/data/yyb-worker-jobs && \
|
||||||
|
chmod +x /app/docker-entrypoint.sh
|
||||||
|
|
||||||
# 环境变量
|
# 环境变量
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
@@ -73,12 +89,13 @@ ENV PYTHONDONTWRITEBYTECODE=1
|
|||||||
ENV PATH="/app/.venv/bin:$PATH"
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
# 生产镜像默认关闭文件监控重载,避免 data/logs 写入触发重启循环。
|
# 生产镜像默认关闭文件监控重载,避免 data/logs 写入触发重启循环。
|
||||||
ENV UVICORN_RELOAD=false
|
ENV UVICORN_RELOAD=false
|
||||||
|
ENV YYB_WORKER_URL=http://127.0.0.1:8810
|
||||||
|
|
||||||
EXPOSE 8800
|
EXPOSE 8800
|
||||||
|
|
||||||
# 健康检查
|
# 健康检查同时覆盖 Web API 与同容器 Worker。
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8800/api/health')" || exit 1
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8800/api/health'); urllib.request.urlopen('http://127.0.0.1:8810/health')" || exit 1
|
||||||
|
|
||||||
# 启动命令
|
# 同一容器内启动 Web 与本机 Worker;入口脚本负责联动退出和信号转发。
|
||||||
CMD ["python", "-m", "uvicorn", "web.backend.main:app", "--host", "0.0.0.0", "--port", "8800", "--workers", "1"]
|
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ douyu_login_py/
|
|||||||
|
|
||||||
### 应用宝充值 Worker
|
### 应用宝充值 Worker
|
||||||
|
|
||||||
应用宝充值使用同仓库内独立的 `yyb-worker` Docker 服务,后台不直接运行逆向脚本。两个服务使用 Compose 默认内网通信,不共享目录、Cookie 或数据库。
|
应用宝充值 Worker 已合并到 Web 应用容器中,内部仅监听 `127.0.0.1:8810`,不发布额外端口。任务数据与应用数据一起保存在 `data/yyb-worker-jobs/`。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/douyu_login_py
|
cd /opt/douyu_login_py
|
||||||
@@ -60,13 +60,12 @@ python3 -c 'import secrets; print(secrets.token_urlsafe(32))'
|
|||||||
将生成的值写入项目 `.env`:
|
将生成的值写入项目 `.env`:
|
||||||
|
|
||||||
```dotenv
|
```dotenv
|
||||||
YYB_WORKER_URL=http://yyb-worker:8810
|
|
||||||
YYB_WORKER_KEY=随机密钥
|
YYB_WORKER_KEY=随机密钥
|
||||||
```
|
```
|
||||||
|
|
||||||
随后执行 `./deploy.sh`,它会同时构建并启动后台、MySQL 和 Worker。Worker 不发布端口;扫码图片和支付二维码只经后台授权接口返回。Worker 的任务文件保存在独立 Docker volume `yyb-worker-data`。服务器只需要此仓库,不需要部署 `js_reverse`。
|
随后执行 `./deploy.sh`,它会构建并启动应用与 MySQL。Worker 不发布端口;扫码图片和支付二维码只经后台授权接口返回。服务器只需要此仓库,不需要部署 `js_reverse`。
|
||||||
|
|
||||||
本机热更新调试使用 `./dev.sh`(或 `./deploy.sh dev`)。它会构建并启动一个仅绑定 `127.0.0.1:8810` 的开发 Worker,后端自动使用该地址;Worker 动态数据写入 `data/yyb-worker-dev/`,按 `Ctrl+C` 会停止本次启动的容器。
|
本机热更新调试使用 `./dev.sh`(或 `./deploy.sh dev`)。它会直接启动一个仅绑定 `127.0.0.1:8810` 的开发 Worker,后端自动使用该地址;Worker 动态数据写入 `data/yyb-worker-dev/`,按 `Ctrl+C` 会一并停止。
|
||||||
|
|
||||||
**常用命令:**
|
**常用命令:**
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ cmd_reset() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo " 将删除以下内容:"
|
echo " 将删除以下内容:"
|
||||||
echo " • MySQL 数据卷"
|
echo " • MySQL 数据卷"
|
||||||
echo " • 应用宝 Worker 任务数据卷(扫码会话、二维码和订单记录)"
|
echo " • 应用宝 Worker 任务数据(扫码会话、二维码和订单记录)"
|
||||||
echo " • 历史Cookie (data/cookies/)"
|
echo " • 历史Cookie (data/cookies/)"
|
||||||
echo " • 应用日志 (logs/)"
|
echo " • 应用日志 (logs/)"
|
||||||
echo " • Docker容器和镜像"
|
echo " • Docker容器和镜像"
|
||||||
@@ -138,6 +138,9 @@ cmd_reset() {
|
|||||||
echo " 删除Cookie..."
|
echo " 删除Cookie..."
|
||||||
rm -rf data/cookies/
|
rm -rf data/cookies/
|
||||||
|
|
||||||
|
echo " 删除应用宝 Worker 任务..."
|
||||||
|
rm -rf data/yyb-worker-jobs/ data/yyb-worker-dev/
|
||||||
|
|
||||||
echo " 删除日志..."
|
echo " 删除日志..."
|
||||||
rm -rf logs/*.log
|
rm -rf logs/*.log
|
||||||
|
|
||||||
|
|||||||
@@ -36,10 +36,9 @@ DB_MAX_OVERFLOW="${DB_MAX_OVERFLOW:-20}"
|
|||||||
DB_POOL_TIMEOUT="${DB_POOL_TIMEOUT:-30}"
|
DB_POOL_TIMEOUT="${DB_POOL_TIMEOUT:-30}"
|
||||||
DB_POOL_RECYCLE="${DB_POOL_RECYCLE:-1800}"
|
DB_POOL_RECYCLE="${DB_POOL_RECYCLE:-1800}"
|
||||||
YYB_WORKER_PORT="${YYB_WORKER_PORT:-8810}"
|
YYB_WORKER_PORT="${YYB_WORKER_PORT:-8810}"
|
||||||
YYB_WORKER_IMAGE="${YYB_WORKER_IMAGE:-douyu-yyb-worker:local}"
|
|
||||||
DEV_YYB_WORKER_URL="${DEV_YYB_WORKER_URL:-http://127.0.0.1:${YYB_WORKER_PORT}}"
|
DEV_YYB_WORKER_URL="${DEV_YYB_WORKER_URL:-http://127.0.0.1:${YYB_WORKER_PORT}}"
|
||||||
YYB_WORKER_CONTAINER="${YYB_WORKER_CONTAINER:-douyu-yyb-worker-dev}"
|
|
||||||
YYB_WORKER_DATA_DIR="${YYB_WORKER_DATA_DIR:-$ROOT_DIR/data/yyb-worker-dev}"
|
YYB_WORKER_DATA_DIR="${YYB_WORKER_DATA_DIR:-$ROOT_DIR/data/yyb-worker-dev}"
|
||||||
|
YYB_WORKER_NODE_MODULES="$ROOT_DIR/services/yyb-worker/node_modules"
|
||||||
LOG_DIR="$ROOT_DIR/logs"
|
LOG_DIR="$ROOT_DIR/logs"
|
||||||
LOG_DATE="$(date +%Y-%m-%d)"
|
LOG_DATE="$(date +%Y-%m-%d)"
|
||||||
BACKEND_LOG="$LOG_DIR/backend-$LOG_DATE.log"
|
BACKEND_LOG="$LOG_DIR/backend-$LOG_DATE.log"
|
||||||
@@ -48,8 +47,7 @@ WORKER_LOG="$LOG_DIR/yyb-worker-$LOG_DATE.log"
|
|||||||
|
|
||||||
BACKEND_PID=""
|
BACKEND_PID=""
|
||||||
FRONTEND_PID=""
|
FRONTEND_PID=""
|
||||||
WORKER_LOG_PID=""
|
WORKER_PID=""
|
||||||
WORKER_STARTED_BY_DEV=false
|
|
||||||
|
|
||||||
stop_tree() {
|
stop_tree() {
|
||||||
local pid="$1"
|
local pid="$1"
|
||||||
@@ -68,10 +66,7 @@ cleanup() {
|
|||||||
trap - EXIT INT TERM
|
trap - EXIT INT TERM
|
||||||
stop_tree "$FRONTEND_PID"
|
stop_tree "$FRONTEND_PID"
|
||||||
stop_tree "$BACKEND_PID"
|
stop_tree "$BACKEND_PID"
|
||||||
stop_tree "$WORKER_LOG_PID"
|
stop_tree "$WORKER_PID"
|
||||||
if [ "$WORKER_STARTED_BY_DEV" = true ]; then
|
|
||||||
docker stop "$YYB_WORKER_CONTAINER" >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
require_cmd() {
|
require_cmd() {
|
||||||
@@ -143,26 +138,21 @@ mkdir -p "$YYB_WORKER_DATA_DIR"
|
|||||||
trap cleanup EXIT INT TERM
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
start_yyb_worker() {
|
start_yyb_worker() {
|
||||||
if docker ps --format '{{.Names}}' | grep -qx "$YYB_WORKER_CONTAINER"; then
|
if [ ! -d "$YYB_WORKER_NODE_MODULES/jsdom" ]; then
|
||||||
echo "本地 YYB Worker 已运行: ${DEV_YYB_WORKER_URL}"
|
echo "正在安装本地 YYB Worker Node 依赖..."
|
||||||
return
|
(cd "$ROOT_DIR/services/yyb-worker" && npm ci)
|
||||||
fi
|
|
||||||
if docker ps -a --format '{{.Names}}' | grep -qx "$YYB_WORKER_CONTAINER"; then
|
|
||||||
echo "发现已停止的本地 YYB Worker,正在清理旧容器..."
|
|
||||||
docker rm "$YYB_WORKER_CONTAINER" >/dev/null
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "正在构建本地 YYB Worker..."
|
|
||||||
YYB_WORKER_KEY="$YYB_WORKER_KEY" $COMPOSE build yyb-worker
|
|
||||||
echo "正在启动本地 YYB Worker..."
|
echo "正在启动本地 YYB Worker..."
|
||||||
docker run -d --rm \
|
(
|
||||||
--name "$YYB_WORKER_CONTAINER" \
|
cd "$ROOT_DIR/services/yyb-worker/runtime"
|
||||||
-p "127.0.0.1:${YYB_WORKER_PORT}:8810" \
|
YYB_WORKER_KEY="$YYB_WORKER_KEY" \
|
||||||
-e TZ="${TZ:-Asia/Shanghai}" \
|
uv run --project "$ROOT_DIR" python scripts/yyb-worker.py \
|
||||||
-e YYB_WORKER_KEY="$YYB_WORKER_KEY" \
|
--host 127.0.0.1 \
|
||||||
-v "$YYB_WORKER_DATA_DIR:/app/config/worker-jobs" \
|
--port "$YYB_WORKER_PORT" \
|
||||||
"$YYB_WORKER_IMAGE" >/dev/null
|
--data-dir "$YYB_WORKER_DATA_DIR" 2>&1 | tee -a "$WORKER_LOG"
|
||||||
WORKER_STARTED_BY_DEV=true
|
) &
|
||||||
|
WORKER_PID=$!
|
||||||
|
|
||||||
echo "等待本地 YYB Worker 就绪..."
|
echo "等待本地 YYB Worker 就绪..."
|
||||||
for i in $(seq 1 15); do
|
for i in $(seq 1 15); do
|
||||||
@@ -172,21 +162,14 @@ start_yyb_worker() {
|
|||||||
fi
|
fi
|
||||||
if [ "$i" = "15" ]; then
|
if [ "$i" = "15" ]; then
|
||||||
echo "YYB Worker 启动超时,查看日志:"
|
echo "YYB Worker 启动超时,查看日志:"
|
||||||
docker logs "$YYB_WORKER_CONTAINER" || true
|
tail -n 100 "$WORKER_LOG" 2>/dev/null || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
stream_yyb_worker_logs() {
|
|
||||||
# docker logs 默认只输出到终端;持续转存后保留 Worker 的异常和协议脚本输出。
|
|
||||||
(docker logs --tail 100 --follow "$YYB_WORKER_CONTAINER" 2>&1 | tee -a "$WORKER_LOG") &
|
|
||||||
WORKER_LOG_PID=$!
|
|
||||||
}
|
|
||||||
|
|
||||||
start_yyb_worker
|
start_yyb_worker
|
||||||
stream_yyb_worker_logs
|
|
||||||
|
|
||||||
echo "正在启动本地 MySQL..."
|
echo "正在启动本地 MySQL..."
|
||||||
MYSQL_IMAGE="$MYSQL_IMAGE" \
|
MYSQL_IMAGE="$MYSQL_IMAGE" \
|
||||||
|
|||||||
+4
-26
@@ -37,8 +37,8 @@ services:
|
|||||||
- COOKIE_SECURE=${COOKIE_SECURE:-false}
|
- COOKIE_SECURE=${COOKIE_SECURE:-false}
|
||||||
# CORS 允许的源(逗号分隔)
|
# CORS 允许的源(逗号分隔)
|
||||||
- CORS_ORIGINS=${CORS_ORIGINS:-}
|
- CORS_ORIGINS=${CORS_ORIGINS:-}
|
||||||
# 独立 yyb-worker 容器,只通过 Compose 默认内网访问。
|
# Worker 已合并到本容器,仅通过回环地址访问。
|
||||||
- YYB_WORKER_URL=${YYB_WORKER_URL:-http://yyb-worker:8810}
|
- YYB_WORKER_URL=http://127.0.0.1:8810
|
||||||
- YYB_WORKER_KEY=${YYB_WORKER_KEY:-}
|
- YYB_WORKER_KEY=${YYB_WORKER_KEY:-}
|
||||||
# Roundcube 邮件验证码读取服务地址(不填则使用代码默认值)
|
# Roundcube 邮件验证码读取服务地址(不填则使用代码默认值)
|
||||||
- MAIL_ROUNDCUBE_URL=${MAIL_ROUNDCUBE_URL:-}
|
- MAIL_ROUNDCUBE_URL=${MAIL_ROUNDCUBE_URL:-}
|
||||||
@@ -50,21 +50,19 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
mysql:
|
mysql:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
yyb-worker:
|
|
||||||
condition: service_healthy
|
|
||||||
# 增加文件描述符限制,避免 "Too many open files" 错误
|
# 增加文件描述符限制,避免 "Too many open files" 错误
|
||||||
ulimits:
|
ulimits:
|
||||||
nofile:
|
nofile:
|
||||||
soft: 65536
|
soft: 65536
|
||||||
hard: 65536
|
hard: 65536
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8800/api/health')"]
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8800/api/health'); urllib.request.urlopen('http://127.0.0.1:8810/health')"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
mysql:
|
mysql:
|
||||||
image: ${MYSQL_IMAGE:-docker.m.daocloud.io/library/mysql:8.4}
|
image: ${MYSQL_IMAGE:-mysql:8.4}
|
||||||
container_name: douyu-login-db
|
container_name: douyu-login-db
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@@ -88,28 +86,8 @@ services:
|
|||||||
retries: 12
|
retries: 12
|
||||||
start_period: 20s
|
start_period: 20s
|
||||||
|
|
||||||
yyb-worker:
|
|
||||||
build:
|
|
||||||
context: ./services/yyb-worker
|
|
||||||
image: ${YYB_WORKER_IMAGE:-douyu-yyb-worker:local}
|
|
||||||
container_name: douyu-yyb-worker
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- TZ=Asia/Shanghai
|
|
||||||
- YYB_WORKER_KEY=${YYB_WORKER_KEY:?请在 .env 中设置 YYB_WORKER_KEY}
|
|
||||||
volumes:
|
|
||||||
- yyb-worker-data:/app/config/worker-jobs
|
|
||||||
networks:
|
|
||||||
- default
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8810/health')"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql-data:
|
mysql-data:
|
||||||
yyb-worker-data:
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
order-site-net:
|
order-site-net:
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# docker compose run 传入命令时直接执行,例如 migrate-mysql 的迁移脚本。
|
||||||
|
if [ "$#" -gt 0 ]; then
|
||||||
|
exec "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Worker 仅监听容器回环地址,Web 后端通过 127.0.0.1 调用。
|
||||||
|
python /app/services/yyb-worker/runtime/scripts/yyb-worker.py \
|
||||||
|
--host 127.0.0.1 \
|
||||||
|
--port 8810 \
|
||||||
|
--data-dir /app/data/yyb-worker-jobs &
|
||||||
|
worker_pid=$!
|
||||||
|
web_pid=""
|
||||||
|
|
||||||
|
stop_processes() {
|
||||||
|
kill -TERM "$worker_pid" 2>/dev/null || true
|
||||||
|
if [ -n "$web_pid" ]; then
|
||||||
|
kill -TERM "$web_pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
wait "$worker_pid" 2>/dev/null || true
|
||||||
|
if [ -n "$web_pid" ]; then
|
||||||
|
wait "$web_pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
trap stop_processes INT TERM
|
||||||
|
|
||||||
|
python -m uvicorn web.backend.main:app --host 0.0.0.0 --port 8800 --workers 1 &
|
||||||
|
web_pid=$!
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
if ! kill -0 "$worker_pid" 2>/dev/null; then
|
||||||
|
if wait "$worker_pid"; then exit_code=0; else exit_code=$?; fi
|
||||||
|
kill -TERM "$web_pid" 2>/dev/null || true
|
||||||
|
wait "$web_pid" 2>/dev/null || true
|
||||||
|
exit "$exit_code"
|
||||||
|
fi
|
||||||
|
if ! kill -0 "$web_pid" 2>/dev/null; then
|
||||||
|
if wait "$web_pid"; then exit_code=0; else exit_code=$?; fi
|
||||||
|
kill -TERM "$worker_pid" 2>/dev/null || true
|
||||||
|
wait "$worker_pid" 2>/dev/null || true
|
||||||
|
exit "$exit_code"
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
@@ -24,6 +24,8 @@ dependencies = [
|
|||||||
"scipy>=1.13.0",
|
"scipy>=1.13.0",
|
||||||
"pyexecjs>=1.5.1",
|
"pyexecjs>=1.5.1",
|
||||||
"pymysql>=1.1,<2",
|
"pymysql>=1.1,<2",
|
||||||
|
"curl-cffi>=0.13",
|
||||||
|
"segno>=1.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
FROM node:20-bookworm-slim
|
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 NODE_ENV=production
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends python3 python3-pip ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
|
||||||
&& python3 -m pip install --no-cache-dir --break-system-packages --upgrade pip
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY package.json package-lock.json ./
|
|
||||||
RUN npm ci --omit=dev
|
|
||||||
COPY requirements-worker.txt ./
|
|
||||||
RUN python3 -m pip install --no-cache-dir --break-system-packages -r requirements-worker.txt
|
|
||||||
COPY runtime/ ./
|
|
||||||
RUN mkdir -p /app/config/worker-jobs
|
|
||||||
EXPOSE 8810
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD python3 -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8810/health')"
|
|
||||||
CMD ["python3", "scripts/yyb-worker.py", "--host", "0.0.0.0", "--port", "8810"]
|
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
# YYB Worker Runtime
|
# YYB Worker Runtime
|
||||||
|
|
||||||
此目录是部署到 `douyu_login_py` 的最小 YYB 运行时。构建上下文只包含 Worker 启动所需的 Python、Node/jsdom 依赖、协议脚本和静态重放数据。
|
此目录是随 `douyu-login` 主镜像部署的 YYB 运行时代码。构建会将 Worker 所需的 Python、Node/jsdom 依赖、协议脚本和静态重放数据纳入同一镜像。
|
||||||
|
|
||||||
- 源码分析和协议验证工作区仍在 `js_reverse/sites/yyb`;不要将该分析仓库上传到服务器。
|
- 源码分析和协议验证工作区仍在 `js_reverse/sites/yyb`;不要将该分析仓库上传到服务器。
|
||||||
- 不要向 `runtime/config/` 放入 Cookie、订单响应、二维码或付款链接。每个任务的动态数据仅写入 Docker volume 挂载的 `/app/config/worker-jobs/<job-id>/`。
|
- 不要向 `runtime/config/` 放入 Cookie、订单响应、二维码或付款链接。每个任务的动态数据仅写入挂载目录 `/app/data/yyb-worker-jobs/<job-id>/`。
|
||||||
- `mall-order-template.json` 仅保留请求结构与非用户常量。登录态、商品、区服、角色、支付平台、动态令牌和加密字段均由当前任务运行时填充。
|
- `mall-order-template.json` 仅保留请求结构与非用户常量。登录态、商品、区服、角色、支付平台、动态令牌和加密字段均由当前任务运行时填充。
|
||||||
- 本服务没有 Compose 端口映射,只允许 `douyu-login` 通过默认 Compose 内网的 `http://yyb-worker:8810` 访问。`YYB_WORKER_KEY` 必须与后台服务配置一致。
|
- Worker 在应用容器内仅监听 `127.0.0.1:8810`,Web 后端通过本机地址访问;`YYB_WORKER_KEY` 必须与后台服务配置一致。
|
||||||
|
|
||||||
本地独立验证:
|
本地独立验证:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t douyu-yyb-worker:local services/yyb-worker
|
YYB_WORKER_KEY=test-key uv run python runtime/scripts/yyb-worker.py --host 127.0.0.1
|
||||||
docker run --rm -e YYB_WORKER_KEY=test-key douyu-yyb-worker:local
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
curl_cffi>=0.13
|
|
||||||
pycryptodome>=3.20
|
|
||||||
segno>=1.6
|
|
||||||
@@ -14,6 +14,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
@@ -96,7 +97,7 @@ def _start_login(job_id: str, provider: str, timeout: int) -> None:
|
|||||||
directory = _job_dir(job_id)
|
directory = _job_dir(job_id)
|
||||||
session = directory / "mall-session.json"
|
session = directory / "mall-session.json"
|
||||||
qr = directory / ("qq-login.jpg" if provider == "qq" else "wechat-login.jpg")
|
qr = directory / ("qq-login.jpg" if provider == "qq" else "wechat-login.jpg")
|
||||||
command = ["python3", f"scripts/login-{provider}.py", "--session", str(session),
|
command = [sys.executable, f"scripts/login-{provider}.py", "--session", str(session),
|
||||||
"--qr", str(qr), "--timeout", str(timeout)]
|
"--qr", str(qr), "--timeout", str(timeout)]
|
||||||
with _lock:
|
with _lock:
|
||||||
job["provider"] = provider
|
job["provider"] = provider
|
||||||
@@ -135,7 +136,7 @@ def _start_payment(job_id: str, selection: dict) -> None:
|
|||||||
session = directory / "mall-session.json"
|
session = directory / "mall-session.json"
|
||||||
response = directory / "mall-order-response.json"
|
response = directory / "mall-order-response.json"
|
||||||
output = directory / "jsdom-order"
|
output = directory / "jsdom-order"
|
||||||
command = ["python3", "main.py", "mall", "auto", "--session", str(session),
|
command = [sys.executable, "main.py", "mall", "auto", "--session", str(session),
|
||||||
"--order-template", str(ROOT / "config" / "mall-order-template.json"),
|
"--order-template", str(ROOT / "config" / "mall-order-template.json"),
|
||||||
"--quantity", "1", "--product-id", str(selection["product_id"]),
|
"--quantity", "1", "--product-id", str(selection["product_id"]),
|
||||||
"--offer-id", str(selection["offer_id"]),
|
"--offer-id", str(selection["offer_id"]),
|
||||||
@@ -149,7 +150,7 @@ def _start_payment(job_id: str, selection: dict) -> None:
|
|||||||
job = _jobs[job_id]
|
job = _jobs[job_id]
|
||||||
if job.get("status") != "success" or not response.exists():
|
if job.get("status") != "success" or not response.exists():
|
||||||
return
|
return
|
||||||
pay_command = ["python3", "scripts/jsdom-pay.py", "--session", str(session),
|
pay_command = [sys.executable, "scripts/jsdom-pay.py", "--session", str(session),
|
||||||
"--mall-response", str(response), "--out-dir", str(output),
|
"--mall-response", str(response), "--out-dir", str(output),
|
||||||
"--zone-id", str(selection["zone_id"]),
|
"--zone-id", str(selection["zone_id"]),
|
||||||
"--pf", str(selection.get("order_pf", "")),
|
"--pf", str(selection.get("order_pf", "")),
|
||||||
|
|||||||
@@ -200,6 +200,30 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" },
|
{ url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "curl-cffi"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "cffi" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b4/23/d32e113b16dbfb458bea408871ed98dd12f306a366a04215e84537e0af7e/curl_cffi-0.16.0.tar.gz", hash = "sha256:b00b423da8028eb6221e3b63bcd63d681150c07cee8b16000d1f7ea292731895", size = 238344, upload-time = "2026-08-01T13:45:12.372Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/fe/0c330de78421af13e6384ab948e3adbcd4c638b06b53b7fff108bf1db121/curl_cffi-0.16.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6128021320f74999ec1216c1817b2c3adcb0f334d204add1ccf18e248bf7efcb", size = 3023503, upload-time = "2026-08-01T13:44:33.452Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/49/3b502d0d09e427b1bdec4f7339bb115c971c9b3fdaf355d02ca06e97ad61/curl_cffi-0.16.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:edd5f6e8f122157f4d2351b0b5e48e6a1c0677a2064da71451bb30ef57af19ba", size = 2780341, upload-time = "2026-08-01T13:44:35.131Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/b5/b341f96f9fa12b28d1150913a1f9007a09a36757c81b4100373c5bdbf78b/curl_cffi-0.16.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:93615d44f23e56c1256700c2e78de4b879310f39a5621828ea7e2a5ecc04bdda", size = 12824596, upload-time = "2026-08-01T13:44:36.556Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/3b/d600b20bff0c55b80b156dc9be0de7c3b3ee2d29977d0a839ea703fab978/curl_cffi-0.16.0-cp310-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3c31e71bf68a9c02a279a184ec9c0ea7c80ce1ef4f1d35073fae3124eb3a7868", size = 12647842, upload-time = "2026-08-01T13:44:38.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/45/9208864ec429558efac168088e50f8a91b947f742ee128cff55b88c7b635/curl_cffi-0.16.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:182416f07d71a342240554fa62c22e591999b78c225b21d3fe27d9f807420dd6", size = 13472637, upload-time = "2026-08-01T13:44:40.893Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/c8/1639a1d9c8b64d0323330b219b7207a54b14fc54982c30cb08c8cc95aa16/curl_cffi-0.16.0-cp310-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d95c0deccc2184eeee7c2aa18d07de261184b418210995aabd0d29f98717a05c", size = 12828918, upload-time = "2026-08-01T13:44:43.049Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/02/bcdf03ea583a445280568c9b163c10668037ee09ece62e78c15846a62df0/curl_cffi-0.16.0-cp310-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ce1f823bc5ce675291a7cf14781496775dfae9298638c25059f2565f6a58a704", size = 12604731, upload-time = "2026-08-01T13:44:45.239Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/8b/4ddae52044c537ace13ad7e46dded8e9340a64e0704e320455c5151108a8/curl_cffi-0.16.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e52586a9dc4ed5e75faa39be0f30353b10cdc7410bad276beb013085e974bb44", size = 12576433, upload-time = "2026-08-01T13:44:47.626Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/f5/38ee2f039db7832f07ce91f6a3d22d87ebfcfaa541130371ac1faa5caea9/curl_cffi-0.16.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce87f301b31147711c3aebc86fb7e16d8dc48f7e5df272c1bea760c687e28eef", size = 13240699, upload-time = "2026-08-01T13:44:49.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/03/b9df2973f1119f9d11d8fb3bf2682e5ffe5c52ef3ab89f720473c60fe97e/curl_cffi-0.16.0-cp310-abi3-win_amd64.whl", hash = "sha256:e22a8212d830108e977ff394237f637238e265f5f65037d6c1ee71ea8cc03bcb", size = 1976497, upload-time = "2026-08-01T13:44:51.839Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/8b/092beeb5fbe3b7370666708eb5618a9e593ffc80ecb2c97c3395158d270b/curl_cffi-0.16.0-cp310-abi3-win_arm64.whl", hash = "sha256:095fc36e4988736f31521d6fe0aa1f243dba22656b4818fc2fb3b7a547e7a9ba", size = 1711122, upload-time = "2026-08-01T13:44:53.242Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/ea/81cf3858b256494b31a554cf76bbd345def3ea7e7a1a592cc515633b4e28/curl_cffi-0.16.0-cp313-abi3-android_24_arm64_v8a.whl", hash = "sha256:06b1c7e07af8ff7c4c5ce4086ea89cc582ebff9adff4a37cfffa5f5de5d5b943", size = 8603463, upload-time = "2026-08-01T13:44:55.003Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "douyu-login-py"
|
name = "douyu-login-py"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -207,6 +231,7 @@ source = { editable = "." }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "alembic" },
|
{ name = "alembic" },
|
||||||
{ name = "bcrypt" },
|
{ name = "bcrypt" },
|
||||||
|
{ name = "curl-cffi" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
{ name = "loguru" },
|
{ name = "loguru" },
|
||||||
{ name = "numpy" },
|
{ name = "numpy" },
|
||||||
@@ -221,6 +246,7 @@ dependencies = [
|
|||||||
{ name = "python-multipart" },
|
{ name = "python-multipart" },
|
||||||
{ name = "requests", extra = ["socks"] },
|
{ name = "requests", extra = ["socks"] },
|
||||||
{ name = "scipy" },
|
{ name = "scipy" },
|
||||||
|
{ name = "segno" },
|
||||||
{ name = "sqlalchemy" },
|
{ name = "sqlalchemy" },
|
||||||
{ name = "uvicorn", extra = ["standard"] },
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
{ name = "websockets" },
|
{ name = "websockets" },
|
||||||
@@ -230,6 +256,7 @@ dependencies = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "alembic", specifier = ">=1.18.4" },
|
{ name = "alembic", specifier = ">=1.18.4" },
|
||||||
{ name = "bcrypt", specifier = ">=4.0.0" },
|
{ name = "bcrypt", specifier = ">=4.0.0" },
|
||||||
|
{ name = "curl-cffi", specifier = ">=0.13" },
|
||||||
{ name = "fastapi", specifier = ">=0.110.0" },
|
{ name = "fastapi", specifier = ">=0.110.0" },
|
||||||
{ name = "loguru", specifier = ">=0.7.0" },
|
{ name = "loguru", specifier = ">=0.7.0" },
|
||||||
{ name = "numpy", specifier = ">=1.24.0" },
|
{ name = "numpy", specifier = ">=1.24.0" },
|
||||||
@@ -244,6 +271,7 @@ requires-dist = [
|
|||||||
{ name = "python-multipart", specifier = ">=0.0.9" },
|
{ name = "python-multipart", specifier = ">=0.0.9" },
|
||||||
{ name = "requests", extras = ["socks"], specifier = ">=2.31.0" },
|
{ name = "requests", extras = ["socks"], specifier = ">=2.31.0" },
|
||||||
{ name = "scipy", specifier = ">=1.13.0" },
|
{ name = "scipy", specifier = ">=1.13.0" },
|
||||||
|
{ name = "segno", specifier = ">=1.6" },
|
||||||
{ name = "sqlalchemy", specifier = ">=2.0.0" },
|
{ name = "sqlalchemy", specifier = ">=2.0.0" },
|
||||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.27.0" },
|
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.27.0" },
|
||||||
{ name = "websockets", specifier = ">=16.0" },
|
{ name = "websockets", specifier = ">=16.0" },
|
||||||
@@ -693,6 +721,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" },
|
{ url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "segno"
|
||||||
|
version = "1.6.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1c/2e/b396f750c53f570055bf5a9fc1ace09bed2dff013c73b7afec5702a581ba/segno-1.6.6.tar.gz", hash = "sha256:e60933afc4b52137d323a4434c8340e0ce1e58cec71439e46680d4db188f11b3", size = 1628586, upload-time = "2025-03-12T22:12:53.324Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/02/12c73fd423eb9577b97fc1924966b929eff7074ae6b2e15dd3d30cb9e4ae/segno-1.6.6-py3-none-any.whl", hash = "sha256:28c7d081ed0cf935e0411293a465efd4d500704072cdb039778a2ab8736190c7", size = 76503, upload-time = "2025-03-12T22:12:48.106Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user