Files
qiyh99 4749e02088 fix: 修复浏览器进程泄漏强杀兜底失效、账号任务假死竞态、验证完成后截图误报 (#96)
* fix(滑块): 修复强杀兜底静默失效导致的浏览器进程泄漏

Python sync API 的 Browser/Context 对象并不暴露 process 属性,
_extract_browser_pid 两条提取路径实际永远返回 None,导致
_force_kill_browser_process_tree 在所有调用点都是静默空操作。
浏览器 close/stop 挂死或跨线程被跳过时,node driver + chrome
进程树无人回收,长期累积耗尽内存和 CPU(实测 2 天泄漏 3 组
会话、36 个 chrome 进程,swap 占满、负载冲到 11+)。

修复:
- _extract_browser_pid 增加回退路径:从 Playwright driver 的
  PipeTransport 子进程句柄提取 PID(chrome 全部挂在 driver 之下,
  按 driver 进程树清理等效覆盖浏览器进程树)
- close_browser 增加 20 秒看门狗:优雅清理挂死(如页面死循环时
  close() 同线程永久阻塞)时强杀进程树解锁流程
- docker-compose 增加 init: true,回收 chrome 退出后的僵尸进程

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(账号管理): 修复残留任务引用导致账号任务永久假死

登录/验证成功后新账号任务启动与状态置"启用"存在竞态:任务启动
瞬间若读到旧的"禁用"状态会立即退出,但 self.tasks 中的引用不会
被清理;之后置"启用"触发 _start_cookie_task 时因引用存在被
"任务已存在"跳过,账号从此永远无法连接(实测复现:人脸验证成功、
Cookie 已入库,但账号任务假死,界面持续显示未登录)。

修复:_start_cookie_task 检查已存在任务的 done() 状态,
已结束的残留引用先清理再正常启动。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(验证): 验证完成后回填风控日志状态,截图接口不再误报待验证

两个叠加问题导致人脸/扫码验证成功后,前端"查看验证截图"仍持续
展示历史截图、看起来像验证没有生效:

1. face_verify 等验证类风控日志在检测阶段写入 processing 状态,
   验证成功后只新增 cookie_refresh 成功日志,从不回填原日志,
   数据库中留下永远 processing 的验证记录
2. /face-verification/screenshot 接口只处理了 verification_required
   (返回截图)和 failed+超时(提示失效)两种状态,验证已成功时
   落到 glob 兜底,把留档的历史截图当成待处理验证返回

修复:
- db_manager 新增 resolve_pending_verification_risk_logs:按账号
  批量将悬挂在 processing/pending 的验证类日志置为 success
- _finalize_logged_in_cookies 成功收尾时调用回填
- 截图接口对最新验证日志为 success 的情况返回"当前没有待处理的
  验证",不再展示历史截图

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(滑块): 补齐 urlencode/quote_plus 缺失导入

_build_login_probe_requests 使用了 urlencode 和 quote_plus,但
urllib.parse 只导入了 parse_qs 和 urlparse。人脸验证成功后的
Cookie 探测链路执行到此处即抛 NameError:

    密码登录流程异常: name 'urlencode' is not defined

导致验证已成功、Cookie 已到手的恢复流程整体失败,账号被错误
标记为"待人脸验证"并暂停,用户反复完成人脸验证也无法登录。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(滑块): PID 提取兼容 asyncio.subprocess.Process;看门狗取消移入 finally

评审意见修复:

1. sync API 下 _connection._transport._proc 实际是
   asyncio.subprocess.Process(实测确认),只有 returncode 没有
   poll(),原实现会抛 AttributeError 被外层吞掉、回退路径永远
   返回 None。改为兼容 poll() 和 returncode 两种存活判断,并把
   属性链访问全部改为 getattr 防御式写法。

2. close_browser 的 close_watchdog.cancel() 移入覆盖整个清理
   流程的 finally,避免清理中途抛出未捕获异常时看门狗在 20 秒后
   按已过期的 _browser_pid 误杀后续新会话的进程树。

容器内实测:修复后回退路径成功提取 driver PID(alive 判断正确),
按进程树 kill 后 driver 进程确认退出。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 09:57:06 +08:00

129 lines
4.6 KiB
YAML

services:
xianyu-app:
build:
context: .
dockerfile: Dockerfile
args:
BASE_IMAGE: python:3.11-slim-bookworm
HTTP_PROXY: ${HTTP_PROXY}
HTTPS_PROXY: ${HTTPS_PROXY}
NO_PROXY: ${NO_PROXY}
http_proxy: ${HTTP_PROXY}
https_proxy: ${HTTPS_PROXY}
no_proxy: ${NO_PROXY}
image: ghcr.io/gudong2003/xianyu-auto-reply-fix:latest
container_name: xianyu-auto-reply-fix
restart: unless-stopped
# 内置 init 进程回收僵尸进程(chrome 子进程退出后无人 reap 会变 defunct,长期累积)
init: true
# 使用root用户避免权限问题
user: "0:0"
# 使用 bash 执行脚本,避免 Windows 行尾格式问题
# 即使挂载了 Windows 文件,bash 也能正确处理
entrypoint: ["/bin/bash", "-c", "dos2unix /app/entrypoint.sh 2>/dev/null || true; bash /app/entrypoint.sh"]
ports:
- "9000:8090"
- "5900:5900" # VNC 端口(可选,用于远程查看浏览器)
- "6080:6080" # noVNC Web 端口(浏览器手动接管)
volumes:
# 数据持久化
- ./data:/app/data:rw
- ./logs:/app/logs:rw
- ./global_config.yml:/app/global_config.yml:ro
# 可选:如果需要自定义配置
# - ./custom_config.yml:/app/global_config.yml:ro
# 可选:备份目录
- ./backups:/app/backups:rw
# 开发模式:挂载源代码(修改代码后只需重启容器)
- .:/app:rw
environment:
# 启用虚拟显示(有头模式)
- USE_XVFB=true # 启用 Xvfb 虚拟显示
- ENABLE_HEADFUL=true # 启用有头模式
- ENABLE_VNC=true # 启用 VNC 服务器(可选)
- DISPLAY=:99 # 虚拟显示编号
- PYTHONUNBUFFERED=${PYTHONUNBUFFERED:-1}
- PYTHONDONTWRITEBYTECODE=${PYTHONDONTWRITEBYTECODE:-1}
- TZ=${TZ:-Asia/Shanghai}
- DB_PATH=${DB_PATH:-/app/data/xianyu_data.db}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- DEBUG=${DEBUG:-false}
- RELOAD=${RELOAD:-false}
# SQL日志配置(默认启用,可通过环境变量覆盖)
- SQL_LOG_ENABLED=${SQL_LOG_ENABLED:-true}
- SQL_LOG_LEVEL=${SQL_LOG_LEVEL:-INFO}
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin123}
- JWT_SECRET_KEY=${JWT_SECRET_KEY:-default-secret-key}
- SESSION_TIMEOUT=${SESSION_TIMEOUT:-3600}
# 多用户系统配置
- MULTIUSER_ENABLED=${MULTIUSER_ENABLED:-true}
- USER_REGISTRATION_ENABLED=${USER_REGISTRATION_ENABLED:-true}
- EMAIL_VERIFICATION_ENABLED=${EMAIL_VERIFICATION_ENABLED:-true}
- CAPTCHA_ENABLED=${CAPTCHA_ENABLED:-true}
- TOKEN_EXPIRE_TIME=${TOKEN_EXPIRE_TIME:-86400}
- AUTO_REPLY_ENABLED=${AUTO_REPLY_ENABLED:-true}
- AUTO_DELIVERY_ENABLED=${AUTO_DELIVERY_ENABLED:-true}
- AUTO_DELIVERY_TIMEOUT=${AUTO_DELIVERY_TIMEOUT:-30}
- API_CARD_TIMEOUT=${API_CARD_TIMEOUT:-10}
- BATCH_DATA_LOCK_TIMEOUT=${BATCH_DATA_LOCK_TIMEOUT:-5}
# AI回复相关配置
- AI_REPLY_ENABLED=${AI_REPLY_ENABLED:-false}
- DEFAULT_AI_MODEL=${DEFAULT_AI_MODEL:-qwen-plus}
- DEFAULT_AI_BASE_URL=${DEFAULT_AI_BASE_URL:-https://dashscope.aliyuncs.com/compatible-mode/v1}
- AI_REQUEST_TIMEOUT=${AI_REQUEST_TIMEOUT:-30}
- AI_MAX_TOKENS=${AI_MAX_TOKENS:-100}
- WEBSOCKET_URL=${WEBSOCKET_URL:-wss://wss-goofish.dingtalk.com/}
- HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-15}
- HEARTBEAT_TIMEOUT=${HEARTBEAT_TIMEOUT:-5}
- TOKEN_REFRESH_INTERVAL=${TOKEN_REFRESH_INTERVAL:-3600}
- TOKEN_RETRY_INTERVAL=${TOKEN_RETRY_INTERVAL:-300}
- MESSAGE_EXPIRE_TIME=${MESSAGE_EXPIRE_TIME:-300000}
networks:
- xianyu-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8090/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# 资源限制
deploy:
resources:
limits:
memory: ${MEMORY_LIMIT:-2048}M
cpus: '${CPU_LIMIT:-2.0}'
reservations:
memory: ${MEMORY_RESERVATION:-512}M
cpus: '${CPU_RESERVATION:-0.5}'
# 可选:添加Nginx反向代理
nginx:
image: nginx:alpine
container_name: xianyu-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- xianyu-app
networks:
- xianyu-network
profiles:
- with-nginx
networks:
xianyu-network:
driver: bridge
volumes:
xianyu-data:
driver: local
xianyu-logs:
driver: local