新增滑块验证

新增滑块验证
This commit is contained in:
zhinianboke 2025-10-24 22:04:51 +08:00 committed by GitHub
commit 1515cfd8de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 6723 additions and 306 deletions

View File

@ -191,3 +191,173 @@ local/
.local/
debug.log
*.debug
# ==================== 2025年新增规则 ====================
# Nuitka编译临时文件
nuitka-crash-report*.xml
nuitka-onefile-warning.txt
utils/*.build/
utils/*.dist/
*.build/
*.dist/
*.onefile-build/
# 浏览器数据和缓存
DrissionPage/
.drission/
selenium/
webdriver/
.playwright/.browsers/
playwright-state/
# 统计和监控数据
user_stats.txt
user_stats.db
stats.log
stats_*.log
metrics/
analytics/
# 运行时生成的数据(这些在容器运行时挂载)
xianyu_data.db
xianyu_data.db.*
xianyu_data_backup_*.db
*.db-journal
*.db-wal
*.db-shm
realtime.log
# 用户上传的文件(运行时挂载)
static/uploads/*
!static/uploads/.gitkeep
# 轨迹历史数据
trajectory_history/*.json
trajectory_history/*.trajectory
# Python编译缓存补充
__pycache__/
*.pyc
*.pyo
*$py.class
# 临时脚本和测试文件
temp_*.py
tmp_*.py
quick_*.py
fix_*.py
test_*.py
check_*.py
debug_*.py
*_test.py
*_demo.py
*_example.py
# 数据导出文件
export_*.xlsx
export_*.csv
export_*.json
dump_*.sql
backup_*.db
# 开发环境文件
.env
.env.*
.env.local
.env.*.local
*.local.yml
*.dev.yml
*.test.yml
config.local.*
# Git相关补充
.git/
.github/
.gitignore
.gitattributes
.git-credentials
# 编辑器和IDE配置
.vscode/
.idea/
*.sublime-*
*.code-workspace
# 文档和说明Dockerfile中不需要
*.md
!README.md
docs/
documentation/
# 压缩包和大文件
*.zip
*.tar.gz
*.rar
*.7z
*.iso
*.dmg
# 媒体文件
*.mp4
*.avi
*.mov
*.mp3
*.wav
# AI模型文件
*.model
*.weights
*.h5
*.pb
*.onnx
# 性能分析和调试
*.perf
*.trace
*.profile
*.prof
memory_*.dump
cpu_*.profile
# 容器相关
docker-compose.override.yml
.docker/
docker-data/
container_logs/
# 密钥和证书(安全相关)
*.key
*.pem
*.crt
*.cert
*.p12
*.pfx
secrets/
credentials/
keys/
ssl/*.key
ssl/*.pem
# 备份文件
*.bak
*.backup
*.old
*~
# 操作系统特定文件
.DS_Store
Thumbs.db
desktop.ini
# Python虚拟环境
venv/
env/
ENV/
.venv/
# 项目特定排除文件(已清理的测试文件和调试工具)
# check_silent_mode.py - 已删除
# order_status_handler.py - 保留,是核心订单处理模块
php/
# simple_stats_server.py - 保留,是可选的统计服务器

251
.gitignore vendored
View File

@ -372,4 +372,253 @@ check_disk_usage.py
.env
.env.*
!.env.example
.env.docker
.env.docker
# === 允许跟踪二进制扩展模块(用于分发)===
!utils/xianyu_slider_stealth*.pyd
!utils/xianyu_slider_stealth*.so
# ==================== 新增项目特定规则 ====================
# 用户数据和隐私文件
user_data/
personal_configs/
*.personal.yml
*.private.yml
# 运行时生成的文件
*.runtime
*.session
session_*
runtime_*
# 第三方服务配置
*.service.yml
*.webhook.yml
external_configs/
# 性能分析和调试文件
*.perf
*.trace
memory_*.dump
cpu_*.profile
# 机器学习模型文件如果有AI功能扩展
models/
*.model
*.weights
*.checkpoint
# 容器相关文件
.docker/
docker-data/
container_logs/
# 监控和统计文件
metrics/
analytics/
*.metrics
*.analytics
# 自动生成的文档
auto_docs/
generated_docs/
# 临时API文件
api_temp/
temp_api/
# 插件和扩展
plugins/
extensions/
addons/
# 测试覆盖率报告
coverage_html/
.coverage.*
coverage.xml
# IDE和编辑器特定文件
.vscode/settings.json
.vscode/launch.json
.idea/workspace.xml
.idea/tasks.xml
# 系统特定文件
.DS_Store
Thumbs.db
desktop.ini
*.lnk
# 网络和代理配置
proxy_configs/
*.proxy
network_configs/
# ==================== 项目清理后新增规则 ====================
# 临时文档和说明文件
*功能说明.md
*修改说明.md
*分析报告.md
*使用说明.md
# 轨迹历史文件
trajectory_history/
*.trajectory
# 实时日志文件
realtime.log
*.realtime
# Nuitka编译报告
nuitka-crash-report.xml
*.crash-report.xml
# 项目压缩包
*.zip
*.tar.gz
*.rar
*.7z
# 临时数据库文件
*.db-journal
*.db-wal
*.db-shm
# ==================== 2025年新增规则 ====================
# 编译相关临时文件
*.build/
*.dist/
*.onefile-build/
nuitka-crash-report*.xml
nuitka-onefile-warning.txt
# Nuitka 编译产物保留在utils目录下的二进制模块
utils/*.build/
utils/*.dist/
!utils/xianyu_slider_stealth*.pyd
!utils/xianyu_slider_stealth*.so
# Python类型提示文件Nuitka生成的
*.pyi
!utils/xianyu_slider_stealth.pyi
# DrissionPage浏览器缓存
DrissionPage/
.drission/
# 其他浏览器数据
selenium/
webdriver/
*.crdownload
# 项目运行时生成的统计文件
user_stats.txt
user_stats.db
stats.log
stats_*.log
# 临时Python脚本不要提交临时脚本
temp_*.py
tmp_*.py
quick_*.py
fix_*.py
test_*.py
check_*.py
debug_*.py
# 数据导出文件(避免提交用户数据)
export_*.xlsx
export_*.csv
export_*.json
dump_*.sql
backup_*.db
# 日志文件(更全面)
*.log
*.log.*
logs/
*.realtime
realtime.log
# 配置文件备份
*.yml.bak
*.yaml.bak
*.json.bak
*.conf.bak
# 系统监控和性能分析
*.perf
*.trace
*.profile
memory_*.dump
cpu_*.profile
performance_*.txt
# 容器相关(补充)
.docker/
docker-data/
container_logs/
docker-compose.override.yml
# 数据目录(补充确保不提交用户数据)
data/
backups/
trajectory_history/
*.trajectory
# 临时下载和缓存
downloads/
temp_downloads/
.cache/
cache/
__cache__/
# 开发环境配置(保护开发者隐私)
.env
.env.*
!.env.example
*.local.yml
*.dev.yml
*.test.yml
*.personal.yml
*.private.yml
config.local.*
config.dev.*
# 加密密钥文件(确保安全)
*.key
*.pem
*.crt
*.cert
*.p12
*.pfx
secrets/
credentials/
keys/
# AI模型和大文件
*.model
*.weights
*.h5
*.pb
*.onnx
*.tflite
models/
checkpoints/
# 媒体文件(避免提交大文件)
*.mp4
*.avi
*.mov
*.wmv
*.flv
*.mp3
*.wav
*.ogg
# 项目特定排除(明确列出不需要跟踪的文件)
# check_silent_mode.py - 已删除
# order_status_handler.py - 是核心模块,需要跟踪

View File

@ -20,6 +20,10 @@ ENV PYTHONDONTWRITEBYTECODE=1
ENV TZ=Asia/Shanghai
ENV DOCKER_ENV=true
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
# Nuitka编译优化
ENV CC=gcc
ENV CXX=g++
ENV NUITKA_CACHE_DIR=/tmp/nuitka-cache
# 安装系统依赖包括Playwright浏览器依赖
RUN apt-get update && \
@ -30,6 +34,12 @@ RUN apt-get update && \
tzdata \
curl \
ca-certificates \
# 编译工具Nuitka需要
build-essential \
gcc \
g++ \
ccache \
patchelf \
# 图像处理依赖
libjpeg-dev \
libpng-dev \
@ -63,6 +73,10 @@ RUN apt-get update && \
libx11-xcb1 \
libxfixes3 \
xdg-utils \
chromium \
# OpenCV运行时依赖
libgl1 \
libglib2.0-0 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /tmp/* \
@ -83,6 +97,34 @@ RUN pip install --no-cache-dir --upgrade pip && \
# 复制项目文件
COPY . .
# 条件执行:如果 xianyu_slider_stealth.py 存在,则编译为二进制模块
RUN if [ -f "utils/xianyu_slider_stealth.py" ]; then \
echo "===================================="; \
echo "检测到 xianyu_slider_stealth.py"; \
echo "开始编译为二进制模块..."; \
echo "===================================="; \
pip install --no-cache-dir nuitka ordered-set zstandard && \
python build_binary_module.py; \
BUILD_RESULT=$?; \
if [ $BUILD_RESULT -eq 0 ]; then \
echo "===================================="; \
echo "✓ 二进制模块编译成功"; \
echo "===================================="; \
ls -lh utils/xianyu_slider_stealth.* 2>/dev/null || true; \
else \
echo "===================================="; \
echo "✗ 二进制模块编译失败 (错误码: $BUILD_RESULT)"; \
echo "将继续使用 Python 源代码版本"; \
echo "===================================="; \
fi; \
rm -rf /tmp/nuitka-cache utils/xianyu_slider_stealth.build utils/xianyu_slider_stealth.dist; \
else \
echo "===================================="; \
echo "未检测到 xianyu_slider_stealth.py"; \
echo "跳过二进制编译"; \
echo "===================================="; \
fi
# 安装Playwright浏览器必须在复制项目文件之后
RUN playwright install chromium && \
playwright install-deps chromium
@ -91,6 +133,9 @@ RUN playwright install chromium && \
RUN mkdir -p /app/logs /app/data /app/backups /app/static/uploads/images && \
chmod 777 /app/logs /app/data /app/backups /app/static/uploads /app/static/uploads/images
# 配置系统限制防止core文件生成
RUN echo "ulimit -c 0" >> /etc/profile
# 注意: 为了简化权限问题使用root用户运行
# 在生产环境中,建议配置适当的用户映射
@ -101,10 +146,9 @@ EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# 复制启动脚本并设置权限
# 复制启动脚本
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh && \
dos2unix /app/entrypoint.sh 2>/dev/null || true
RUN chmod +x /app/entrypoint.sh
# 启动命令使用ENTRYPOINT确保脚本被执行
ENTRYPOINT ["/bin/bash", "/app/entrypoint.sh"]
# 启动命令
CMD ["/app/entrypoint.sh"]

View File

@ -8,6 +8,8 @@ LABEL description="闲鱼自动回复系统 - 企业级多用户版本,支持
LABEL repository="https://github.com/zhinianboke/xianyu-auto-reply"
LABEL license="仅供学习使用,禁止商业用途"
LABEL author="zhinianboke"
LABEL build-date=""
LABEL vcs-ref=""
# 设置工作目录
WORKDIR /app
@ -64,6 +66,10 @@ RUN apt-get update && \
libx11-xcb1 \
libxfixes3 \
xdg-utils \
chromium \
# OpenCV运行时依赖
libgl1 \
libglib2.0-0 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /tmp/* \
@ -92,6 +98,9 @@ RUN playwright install chromium && \
RUN mkdir -p /app/logs /app/data /app/backups /app/static/uploads/images && \
chmod 777 /app/logs /app/data /app/backups /app/static/uploads /app/static/uploads/images
# 配置系统限制防止core文件生成
RUN echo "ulimit -c 0" >> /etc/profile
# 注意: 为了简化权限问题使用root用户运行
# 在生产环境中,建议配置适当的用户映射

138
README.md
View File

@ -41,6 +41,7 @@ https://pan.baidu.com/s/1I6muOGJQYd6y3oxQSmtvrQ?pwd=gcpd
- **数据完全隔离** - 每个用户的数据独立存储,互不干扰
- **权限管理** - 严格的用户权限控制和JWT认证
- **安全保护** - 防暴力破解、会话管理、安全日志
- **授权期限管理** - 核心滑块验证模块包含授权期限验证,确保合规使用
### 📱 多账号管理
- **无限账号支持** - 每个用户可管理多个闲鱼账号
@ -115,10 +116,16 @@ xianyu-auto-reply/
│ ├── db_manager.py # SQLite数据库管理支持多用户数据隔离
│ ├── cookie_manager.py # 多账号Cookie管理和任务调度
│ ├── ai_reply_engine.py # AI智能回复引擎支持多种AI模型
│ ├── order_status_handler.py # 订单状态处理和更新模块
│ ├── file_log_collector.py # 实时日志收集和管理系统
│ ├── config.py # 全局配置文件管理器
│ ├── usage_statistics.py # 用户统计和数据分析模块
│ ├── simple_stats_server.py # 简单统计服务器(可选)
│ ├── build_binary_module.py # 二进制模块编译脚本Nuitka编译工具
│ ├── secure_confirm_ultra.py # 自动确认发货模块(多层加密保护)
│ └── secure_freeshipping_ultra.py # 自动免拼发货模块(多层加密保护)
│ ├── secure_confirm_decrypted.py # 自动确认发货模块(解密版本)
│ ├── secure_freeshipping_ultra.py # 自动免拼发货模块(多层加密保护)
│ └── secure_freeshipping_decrypted.py # 自动免拼发货模块(解密版本)
├── 🛠️ 工具模块
│ └── utils/
│ ├── xianyu_utils.py # 闲鱼API工具函数加密、签名、解析
@ -184,6 +191,33 @@ xianyu-auto-reply/
</details>
## 🆕 最新更新
### 2025年1月更新
**🔥 性能与安全增强**
- ✅ 新增 Nuitka 二进制编译支持,核心模块可编译为 .pyd/.so 提升性能和安全性
- ✅ 滑块验证模块增加授权期限验证机制,确保合规使用
- ✅ Docker 构建优化,自动编译二进制模块,提升容器启动效率
- ✅ 完善的错误处理和重试机制,提升系统稳定性
- ✅ 修复滑块验证模块内存泄漏问题,浏览器资源正确释放
**🛠️ 配置文件优化**
- ✅ 完善 `.gitignore`,新增编译产物、浏览器缓存等规则
- ✅ 完善 `.dockerignore`优化Docker构建速度和镜像体积
- ✅ 增强 `entrypoint.sh`,添加环境验证和详细启动日志
- ✅ 清理测试文件和临时文件,保持代码库整洁
**📦 依赖管理**
- ✅ `requirements.txt` 优化移除Python内置模块按功能分类
- ✅ 添加 Nuitka 编译工具链(可选)
- ✅ 详细的依赖说明和安装指南
**🐛 Bug修复**
- ✅ 修复浏览器资源泄漏问题Docker容器RAM使用稳定
- ✅ 优化历史记录存储减少90%磁盘和内存占用
- ✅ 添加析构函数确保资源释放
## 🚀 快速开始
**⚡ 最快部署方式(推荐)**:使用预构建镜像,无需下载源码,一条命令即可启动!
@ -216,31 +250,39 @@ docker run -d -p 8080:8080 -v %cd%/xianyu-auto-reply/:/app/data/ --name xianyu-a
### 方式二:从源码构建部署
#### 🌍 国际版(推荐海外用户)
```bash
# 1. 克隆项目
git clone https://github.com/zhinianboke/xianyu-auto-reply.git
cd xianyu-auto-reply
# 2. 设置脚本执行权限Linux/macOS
chmod +x docker-deploy.sh
# 2. 使用完整版配置包含Redis缓存等增强功能
docker-compose up -d --build
# 3. 一键部署(自动构建镜像)
./docker-deploy.sh
# 3. 访问系统
# http://localhost:8080
```
# 4. 访问系统
#### 🇨🇳 中国版(推荐国内用户)
```bash
# 1. 克隆项目
git clone https://github.com/zhinianboke/xianyu-auto-reply.git
cd xianyu-auto-reply
# 2. 使用中国镜像源配置(下载速度更快)
docker-compose -f docker-compose-cn.yml up -d --build
# 3. 访问系统
# http://localhost:8080
```
**Windows用户**
```cmd
# 使用Windows批处理脚本推荐
docker-deploy.bat
# 或者使用Git Bash/WSL
bash docker-deploy.sh
# 或者直接使用Docker Compose
# 国际版
docker-compose up -d --build
# 中国版(推荐)
docker-compose -f docker-compose-cn.yml up -d --build
```
### 方式三:本地开发部署
@ -280,6 +322,39 @@ python Start.py
- **Docker**: 20.10+ (Docker部署)
- **Docker Compose**: 2.0+ (Docker部署)
### ⚙️ 环境变量配置(可选)
系统支持通过环境变量进行配置,主要配置项包括:
```bash
# 基础配置
WEB_PORT=8080 # Web服务端口
API_HOST=0.0.0.0 # API服务主机
TZ=Asia/Shanghai # 时区设置
# 管理员配置
ADMIN_USERNAME=admin # 管理员用户名
ADMIN_PASSWORD=admin123 # 管理员密码(请修改)
JWT_SECRET_KEY=your-secret-key # JWT密钥请修改
# 功能开关
AUTO_REPLY_ENABLED=true # 启用自动回复
AUTO_DELIVERY_ENABLED=true # 启用自动发货
AI_REPLY_ENABLED=false # 启用AI回复
# 日志配置
LOG_LEVEL=INFO # 日志级别
SQL_LOG_ENABLED=true # SQL日志
# 资源限制
MEMORY_LIMIT=2048 # 内存限制(MB)
CPU_LIMIT=2.0 # CPU限制(核心数)
# 更多配置请参考 docker-compose.yml 文件
```
> 💡 **提示**:所有配置项都有默认值,可根据需要选择性配置
### 🌐 访问系统
@ -434,6 +509,8 @@ python Start.py
- **`order_detail_fetcher.py`** - 订单详情获取工具解析订单信息、买家信息、SKU详情支持缓存优化、锁机制
- **`image_utils.py`** - 图片处理工具,支持压缩、格式转换、尺寸调整、水印添加、质量优化
- **`image_uploader.py`** - 图片上传工具支持多种CDN服务商、自动压缩、格式优化、批量上传
- **`xianyu_slider_stealth.py`** - 增强版滑块验证模块,采用高级反检测技术,支持密码登录、自动重试、并发控制,包含授权期限验证机制(可编译为二进制模块以提升性能和安全性)
- **`refresh_util.py`** - Cookie刷新工具自动检测和刷新过期的Cookie保持账号连接状态
### 🌐 前端界面 (`static/`)
- **`index.html`** - 主管理界面,集成所有功能模块:账号管理、关键词管理、商品管理、发货管理、系统监控、用户管理等
@ -446,17 +523,18 @@ python Start.py
- **`uploads/images/`** - 图片上传目录,支持发货图片和其他媒体文件存储
### 🐳 部署配置
- **`Dockerfile`** - Docker镜像构建文件基于Python 3.11-slim包含Playwright浏览器、系统依赖支持无头模式运行优化构建层级
- **`Dockerfile`** - Docker镜像构建文件基于Python 3.11-slim包含Playwright浏览器、C编译器支持Nuitka编译系统依赖,支持无头模式运行,优化构建层级,自动编译性能关键模块
- **`Dockerfile-cn`** - 国内优化版Docker镜像构建文件使用国内镜像源加速构建适合国内网络环境
- **`docker-compose.yml`** - Docker Compose配置支持一键部署、完整环境变量配置、资源限制、健康检查、可选Nginx代理
- **`docker-compose-cn.yml`** - 国内优化版Docker Compose配置文件使用国内镜像源
- **`docker-deploy.sh`** - Docker部署管理脚本提供构建、启动、停止、重启、监控、日志查看等功能Linux/macOS
- **`docker-deploy.bat`** - Windows版本部署脚本支持Windows环境一键部署和管理
- **`entrypoint.sh`** - Docker容器启动脚本处理环境初始化、目录创建、权限设置和服务启动
- **`entrypoint.sh`** - Docker容器启动脚本增强版包含环境验证、依赖检查、目录创建、权限设置和详细启动日志
- **`nginx/nginx.conf`** - Nginx反向代理配置支持负载均衡、SSL终端、WebSocket代理、静态文件服务
- **`requirements.txt`** - Python依赖包列表精简版本无内置模块按功能分类组织包含详细版本说明和安装指南
- **`.gitignore`** - Git忽略文件配置完整覆盖Python、Docker、前端、测试、临时文件等支持项目特定文件类型
- **`.dockerignore`** - Docker构建忽略文件优化构建上下文大小和构建速度排除不必要的文件和目录
- **`requirements.txt`** - Python依赖包列表精简版本无内置模块按功能分类组织包含详细版本说明和安装指南可选Nuitka编译工具
- **`.gitignore`** - Git忽略文件配置完整覆盖Python、Docker、前端、测试、临时文件等2025年更新包含编译产物、浏览器缓存、统计数据等新规则
- **`.dockerignore`** - Docker构建忽略文件优化构建上下文大小和构建速度排除不必要的文件和目录2025年更新包含Nuitka编译临时文件、浏览器数据等新规则
- **`build_binary_module.py`** - 二进制模块编译脚本使用Nuitka将性能关键的Python模块编译为二进制扩展(.pyd/.so),提升执行效率和代码安全性
## 🏗️ 详细技术架构
@ -575,6 +653,30 @@ python Start.py
## 🔧 高级功能
### 二进制模块编译(可选)
为了提升性能和代码安全性,可以将核心模块编译为二进制文件:
```bash
# 1. 安装 Nuitka已在 requirements.txt 中)
pip install nuitka ordered-set zstandard
# 2. 运行编译脚本
python build_binary_module.py
# 3. 编译完成后会生成 .pyd (Windows) 或 .so (Linux) 文件
# Python 会自动优先加载二进制版本
```
**Docker 部署自动编译**
- Docker 构建时会自动检测并编译相关模块
- 无需手动操作,构建完成即可使用
**编译优势**
- ⚡ 性能提升:编译后的代码执行效率更高
- 🔒 代码保护:二进制文件难以反编译
- 🛡️ 授权管理:集成授权期限验证
### AI回复配置
1. 在用户设置中配置OpenAI API密钥
2. 选择AI模型支持GPT-3.5、GPT-4、通义千问等

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@ class AIReplyEngine:
def __init__(self):
self.clients = {} # 存储不同账号的OpenAI客户端
self.agents = {} # 存储不同账号的Agent实例
self.client_last_used = {} # 记录客户端最后使用时间 {cookie_id: timestamp}
self._init_default_prompts()
def _init_default_prompts(self):
@ -71,6 +72,8 @@ class AIReplyEngine:
logger.error(f"创建OpenAI客户端失败 {cookie_id}: {e}")
return None
# 记录使用时间
self.client_last_used[cookie_id] = time.time()
return self.clients[cookie_id]
def _is_dashscope_api(self, settings: dict) -> bool:
@ -374,10 +377,42 @@ class AIReplyEngine:
"""清理客户端缓存"""
if cookie_id:
self.clients.pop(cookie_id, None)
self.client_last_used.pop(cookie_id, None)
logger.info(f"清理账号 {cookie_id} 的客户端缓存")
else:
self.clients.clear()
self.client_last_used.clear()
logger.info("清理所有客户端缓存")
def cleanup_unused_clients(self, max_idle_hours: int = 24):
"""清理长时间未使用的客户端(防止内存泄漏)
Args:
max_idle_hours: 最大空闲时间小时默认24小时
"""
try:
current_time = time.time()
max_idle_seconds = max_idle_hours * 3600
# 找出超过最大空闲时间的客户端
expired_clients = [
cookie_id for cookie_id, last_used in self.client_last_used.items()
if current_time - last_used > max_idle_seconds
]
# 清理过期客户端
for cookie_id in expired_clients:
self.clients.pop(cookie_id, None)
self.client_last_used.pop(cookie_id, None)
self.agents.pop(cookie_id, None)
if expired_clients:
logger.info(f"AI回复引擎清理了 {len(expired_clients)} 个长时间未使用的客户端")
logger.debug(f"清理的账号: {expired_clients}")
logger.debug(f"当前活跃客户端数量: {len(self.clients)}")
except Exception as e:
logger.error(f"AI回复引擎清理未使用客户端时出错: {e}")
# 全局AI回复引擎实例

155
build_binary_module.py Normal file
View File

@ -0,0 +1,155 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
utils/xianyu_slider_stealth.py 编译为可直接 import 的二进制扩展模块.pyd/.so
- 使用 Nuitka --module 模式
- 输出文件放到 utils/ 目录名称为 xianyu_slider_stealth.<abi>.pyd/.so
- 这样 Python 将优先加载二进制扩展而不是同名 .py
"""
import sys
import subprocess
from pathlib import Path
SRC = Path("utils/xianyu_slider_stealth.py")
OUT_DIR = Path("utils")
def ensure_nuitka():
try:
import nuitka # noqa: F401
print("✓ Nuitka 已安装")
return True
except Exception:
print("✗ 未检测到 Nuitka。请先允许我安装: pip install nuitka ordered-set zstandard")
return False
def clean_old_files():
"""清理旧的编译产物"""
import os
import glob
patterns = [
"utils/xianyu_slider_stealth.*.pyd",
"utils/xianyu_slider_stealth.*.so",
"utils/xianyu_slider_stealth.build",
"utils/xianyu_slider_stealth.dist"
]
for pattern in patterns:
for file_path in glob.glob(pattern):
try:
if os.path.isfile(file_path):
os.remove(file_path)
print(f"✓ 已删除旧文件: {file_path}")
elif os.path.isdir(file_path):
import shutil
shutil.rmtree(file_path)
print(f"✓ 已删除旧目录: {file_path}")
except Exception as e:
print(f"⚠️ 无法删除 {file_path}: {e}")
def check_permissions():
"""检查目录权限"""
try:
test_file = OUT_DIR / "test_write.tmp"
test_file.write_text("test")
test_file.unlink()
return True
except Exception as e:
print(f"✗ 目录权限检查失败: {e}")
print("💡 请尝试以管理员身份运行此脚本")
return False
def build():
OUT_DIR.mkdir(parents=True, exist_ok=True)
# 检查权限
if not check_permissions():
return 1
# 清理旧文件
print("🧹 清理旧的编译产物...")
clean_old_files()
cmd = [
sys.executable, "-m", "nuitka",
"--module",
"--output-dir=%s" % str(OUT_DIR),
"--remove-output",
"--assume-yes-for-downloads",
"--show-progress",
"--python-flag=no_docstrings",
"--python-flag=no_warnings",
"--enable-plugin=anti-bloat",
# 降低内存占用,避免容器内 OOM
"--lto=no",
"--jobs=1",
str(SRC)
]
print("执行编译命令:\n ", " ".join(cmd))
try:
result = subprocess.run(cmd, text=True, timeout=300) # 5分钟超时
if result.returncode != 0:
print("✗ 编译失败 (Nuitka 返回非零)")
print("💡 可能的解决方案:")
print(" 1. 以管理员身份运行此脚本")
print(" 2. 关闭杀毒软件的实时保护")
print(" 3. 检查是否有其他Python进程在运行")
return 1
except subprocess.TimeoutExpired:
print("✗ 编译超时 (5分钟)")
return 1
except Exception as e:
print(f"✗ 编译过程中发生错误: {e}")
return 1
# 列出 utils 目录下的产物
built = sorted(p for p in OUT_DIR.glob("xianyu_slider_stealth.*.pyd"))
if not built:
built = sorted(p for p in OUT_DIR.glob("xianyu_slider_stealth.*.so"))
if not built:
print("✗ 未找到编译产物。请检查输出日志。")
return 2
print("\n✓ 编译产物:")
for p in built:
print(" -", p)
return 0
def main():
print("🔨 开始编译 xianyu_slider_stealth 模块...")
print("📁 项目目录:", Path.cwd())
if not SRC.exists():
print(f"✗ 源文件不存在: {SRC}")
return 1
print(f"📄 源文件: {SRC}")
print(f"📂 输出目录: {OUT_DIR}")
if not ensure_nuitka():
return 2
# 检查是否以管理员身份运行Windows
import os
if os.name == 'nt': # Windows
try:
import ctypes
is_admin = ctypes.windll.shell32.IsUserAnAdmin()
if not is_admin:
print("⚠️ 建议以管理员身份运行此脚本以避免权限问题")
except:
pass
return build()
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -93,10 +93,14 @@ COOKIES_STR = config.get('COOKIES.value', '')
COOKIES_LAST_UPDATE = config.get('COOKIES.last_update_time', '')
WEBSOCKET_URL = config.get('WEBSOCKET_URL', 'wss://wss-goofish.dingtalk.com/')
HEARTBEAT_INTERVAL = config.get('HEARTBEAT_INTERVAL', 15)
HEARTBEAT_TIMEOUT = config.get('HEARTBEAT_TIMEOUT', 5)
TOKEN_REFRESH_INTERVAL = config.get('TOKEN_REFRESH_INTERVAL', 3600)
TOKEN_RETRY_INTERVAL = config.get('TOKEN_RETRY_INTERVAL', 300)
HEARTBEAT_TIMEOUT = config.get('HEARTBEAT_TIMEOUT', 30)
TOKEN_REFRESH_INTERVAL = config.get('TOKEN_REFRESH_INTERVAL', 72000)
TOKEN_RETRY_INTERVAL = config.get('TOKEN_RETRY_INTERVAL', 7200)
MESSAGE_EXPIRE_TIME = config.get('MESSAGE_EXPIRE_TIME', 300000)
SLIDER_VERIFICATION = config.get('SLIDER_VERIFICATION', {
'max_concurrent': 3,
'wait_timeout': 60
})
API_ENDPOINTS = config.get('API_ENDPOINTS', {})
DEFAULT_HEADERS = config.get('DEFAULT_HEADERS', {})
WEBSOCKET_HEADERS = config.get('WEBSOCKET_HEADERS', {})
@ -118,4 +122,4 @@ if isinstance(_cookies_raw, list):
else:
# 兼容旧格式,仅有 value 字段
val = _cookies_raw.get('value') if isinstance(_cookies_raw, dict) else None
COOKIES_LIST = [{'id': 'default', 'value': val}] if val else []
COOKIES_LIST = [{'id': 'default', 'value': val}] if val else []

View File

@ -100,6 +100,15 @@ class CookieManager:
task = self.tasks.pop(cookie_id, None)
if task:
task.cancel()
try:
# 等待任务完全清理,确保资源释放
await task
except asyncio.CancelledError:
# 任务被取消是预期行为
pass
except Exception as e:
logger.error(f"等待任务清理时出错: {cookie_id}, {e}")
self.cookies.pop(cookie_id, None)
self.keywords.pop(cookie_id, None)
# 从数据库删除
@ -138,8 +147,14 @@ class CookieManager:
return fut.result()
# 更新 Cookie 值
def update_cookie(self, cookie_id: str, new_value: str):
"""替换指定账号的 Cookie 并重启任务"""
def update_cookie(self, cookie_id: str, new_value: str, save_to_db: bool = True):
"""替换指定账号的 Cookie 并重启任务
Args:
cookie_id: Cookie ID
new_value: 新的Cookie值
save_to_db: 是否保存到数据库默认True当API层已经更新数据库时应设为False避免覆盖其他字段
"""
async def _update():
# 获取原有的user_id和关键词
original_user_id = None
@ -160,10 +175,21 @@ class CookieManager:
task = self.tasks.pop(cookie_id, None)
if task:
task.cancel()
try:
# 等待任务完全清理,确保资源释放
await task
except asyncio.CancelledError:
# 任务被取消是预期行为
pass
except Exception as e:
logger.error(f"等待任务清理时出错: {cookie_id}, {e}")
# 更新Cookie值保持原有user_id不删除关键词
# 更新Cookie值
self.cookies[cookie_id] = new_value
db_manager.save_cookie(cookie_id, new_value, original_user_id)
# 只有在需要时才保存到数据库避免覆盖其他字段如pause_duration、remark等
if save_to_db:
db_manager.save_cookie(cookie_id, new_value, original_user_id)
# 恢复关键词和状态
self.keywords[cookie_id] = original_keywords
@ -268,13 +294,38 @@ class CookieManager:
logger.warning(f"Cookie任务不存在跳过停止: {cookie_id}")
return
async def _stop_task_async():
"""异步停止任务并等待清理"""
try:
task = self.tasks[cookie_id]
if not task.done():
task.cancel()
try:
# 等待任务完全清理,确保资源释放
await task
except asyncio.CancelledError:
# 任务被取消是预期行为
pass
except Exception as e:
logger.error(f"等待任务清理时出错: {cookie_id}, {e}")
logger.info(f"已取消Cookie任务: {cookie_id}")
del self.tasks[cookie_id]
logger.info(f"成功停止Cookie任务: {cookie_id}")
except Exception as e:
logger.error(f"停止Cookie任务失败: {cookie_id}, {e}")
try:
task = self.tasks[cookie_id]
if not task.done():
task.cancel()
logger.info(f"已取消Cookie任务: {cookie_id}")
del self.tasks[cookie_id]
logger.info(f"成功停止Cookie任务: {cookie_id}")
# 在事件循环中执行异步停止
if hasattr(self.loop, 'is_running') and self.loop.is_running():
fut = asyncio.run_coroutine_threadsafe(_stop_task_async(), self.loop)
fut.result(timeout=10) # 等待最多10秒
else:
logger.warning(f"事件循环未运行,无法正常等待任务清理: {cookie_id}")
# 直接取消任务(非最佳方案)
task = self.tasks[cookie_id]
if not task.done():
task.cancel()
del self.tasks[cookie_id]
except Exception as e:
logger.error(f"停止Cookie任务失败: {cookie_id}, {e}")

View File

@ -115,6 +115,9 @@ class DBManager:
auto_confirm INTEGER DEFAULT 1,
remark TEXT DEFAULT '',
pause_duration INTEGER DEFAULT 10,
username TEXT DEFAULT '',
password TEXT DEFAULT '',
show_browser INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
@ -402,6 +405,22 @@ class DBManager:
)
''')
# 创建风控日志表
cursor.execute('''
CREATE TABLE IF NOT EXISTS risk_control_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cookie_id TEXT NOT NULL,
event_type TEXT NOT NULL DEFAULT 'slider_captcha',
event_description TEXT,
processing_result TEXT,
processing_status TEXT DEFAULT 'processing',
error_message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE
)
''')
# 插入默认系统设置不包括管理员密码由reply_server.py初始化
cursor.execute('''
INSERT OR IGNORE INTO system_settings (key, value, description) VALUES
@ -577,6 +596,13 @@ class DBManager:
self.set_system_setting("db_version", "1.4", "数据库版本号")
logger.info("数据库升级到版本1.4完成")
# 升级到版本1.5 - 为cookies表添加账号登录字段
if current_version < "1.5":
logger.info("开始升级数据库到版本1.5...")
self.upgrade_cookies_table_for_account_login(cursor)
self.set_system_setting("db_version", "1.5", "数据库版本号")
logger.info("数据库升级到版本1.5完成")
# 迁移遗留数据(在所有版本升级完成后执行)
self.migrate_legacy_data(cursor)
@ -889,6 +915,47 @@ class DBManager:
logger.error(f"升级notification_channels表类型失败: {e}")
raise
def upgrade_cookies_table_for_account_login(self, cursor):
"""升级cookies表支持账号密码登录功能"""
try:
logger.info("开始为cookies表添加账号登录相关字段...")
# 为cookies表添加username字段如果不存在
try:
self._execute_sql(cursor, "SELECT username FROM cookies LIMIT 1")
logger.info("cookies表username字段已存在")
except sqlite3.OperationalError:
# username字段不存在需要添加
self._execute_sql(cursor, "ALTER TABLE cookies ADD COLUMN username TEXT DEFAULT ''")
logger.info("为cookies表添加username字段")
# 为cookies表添加password字段如果不存在
try:
self._execute_sql(cursor, "SELECT password FROM cookies LIMIT 1")
logger.info("cookies表password字段已存在")
except sqlite3.OperationalError:
# password字段不存在需要添加
self._execute_sql(cursor, "ALTER TABLE cookies ADD COLUMN password TEXT DEFAULT ''")
logger.info("为cookies表添加password字段")
# 为cookies表添加show_browser字段如果不存在
try:
self._execute_sql(cursor, "SELECT show_browser FROM cookies LIMIT 1")
logger.info("cookies表show_browser字段已存在")
except sqlite3.OperationalError:
# show_browser字段不存在需要添加
self._execute_sql(cursor, "ALTER TABLE cookies ADD COLUMN show_browser INTEGER DEFAULT 0")
logger.info("为cookies表添加show_browser字段")
logger.info("✅ cookies表账号登录字段升级完成")
logger.info(" - username: 用于密码登录的用户名")
logger.info(" - password: 用于密码登录的密码")
logger.info(" - show_browser: 登录时是否显示浏览器0=隐藏1=显示)")
return True
except Exception as e:
logger.error(f"升级cookies表账号登录字段失败: {e}")
raise
def migrate_legacy_data(self, cursor):
"""迁移遗留数据到新表结构"""
try:
@ -1198,11 +1265,11 @@ class DBManager:
return None
def get_cookie_details(self, cookie_id: str) -> Optional[Dict[str, any]]:
"""获取Cookie的详细信息包括user_id、auto_confirm、remark和pause_duration"""
"""获取Cookie的详细信息包括user_id、auto_confirm、remark、pause_duration、username、password和show_browser"""
with self.lock:
try:
cursor = self.conn.cursor()
self._execute_sql(cursor, "SELECT id, value, user_id, auto_confirm, remark, pause_duration, created_at FROM cookies WHERE id = ?", (cookie_id,))
self._execute_sql(cursor, "SELECT id, value, user_id, auto_confirm, remark, pause_duration, username, password, show_browser, created_at FROM cookies WHERE id = ?", (cookie_id,))
result = cursor.fetchone()
if result:
return {
@ -1212,7 +1279,10 @@ class DBManager:
'auto_confirm': bool(result[3]),
'remark': result[4] or '',
'pause_duration': result[5] if result[5] is not None else 10, # 0是有效值表示不暂停
'created_at': result[6]
'username': result[6] or '',
'password': result[7] or '',
'show_browser': bool(result[8]) if result[8] is not None else False,
'created_at': result[9]
}
return None
except Exception as e:
@ -1280,6 +1350,47 @@ class DBManager:
logger.error(f"获取账号自动回复暂停时间失败: {e}")
return 10
def update_cookie_account_info(self, cookie_id: str, cookie_value: str = None, username: str = None, password: str = None, show_browser: bool = None) -> bool:
"""更新Cookie的账号信息包括cookie值、用户名、密码和显示浏览器设置"""
with self.lock:
try:
cursor = self.conn.cursor()
# 构建动态SQL更新语句
update_fields = []
params = []
if cookie_value is not None:
update_fields.append("value = ?")
params.append(cookie_value)
if username is not None:
update_fields.append("username = ?")
params.append(username)
if password is not None:
update_fields.append("password = ?")
params.append(password)
if show_browser is not None:
update_fields.append("show_browser = ?")
params.append(1 if show_browser else 0)
if not update_fields:
logger.warning(f"更新账号 {cookie_id} 信息时没有提供任何更新字段")
return False
params.append(cookie_id)
sql = f"UPDATE cookies SET {', '.join(update_fields)} WHERE id = ?"
self._execute_sql(cursor, sql, tuple(params))
self.conn.commit()
logger.info(f"更新账号 {cookie_id} 信息成功: {update_fields}")
return True
except Exception as e:
logger.error(f"更新账号信息失败: {e}")
return False
def get_auto_confirm(self, cookie_id: str) -> bool:
"""获取Cookie的自动确认发货设置"""
with self.lock:
@ -4664,6 +4775,274 @@ class DBManager:
return {"success_count": success_count, "failed_count": failed_count}
# ==================== 风控日志管理 ====================
def add_risk_control_log(self, cookie_id: str, event_type: str = 'slider_captcha',
event_description: str = None, processing_result: str = None,
processing_status: str = 'processing', error_message: str = None) -> bool:
"""
添加风控日志记录
Args:
cookie_id: Cookie ID
event_type: 事件类型默认为'slider_captcha'
event_description: 事件描述
processing_result: 处理结果
processing_status: 处理状态 ('processing', 'success', 'failed')
error_message: 错误信息
Returns:
bool: 添加成功返回True失败返回False
"""
try:
with self.lock:
cursor = self.conn.cursor()
cursor.execute('''
INSERT INTO risk_control_logs
(cookie_id, event_type, event_description, processing_result, processing_status, error_message)
VALUES (?, ?, ?, ?, ?, ?)
''', (cookie_id, event_type, event_description, processing_result, processing_status, error_message))
self.conn.commit()
return True
except Exception as e:
logger.error(f"添加风控日志失败: {e}")
return False
def update_risk_control_log(self, log_id: int, processing_result: str = None,
processing_status: str = None, error_message: str = None) -> bool:
"""
更新风控日志记录
Args:
log_id: 日志ID
processing_result: 处理结果
processing_status: 处理状态
error_message: 错误信息
Returns:
bool: 更新成功返回True失败返回False
"""
try:
with self.lock:
cursor = self.conn.cursor()
# 构建更新语句
update_fields = []
params = []
if processing_result is not None:
update_fields.append("processing_result = ?")
params.append(processing_result)
if processing_status is not None:
update_fields.append("processing_status = ?")
params.append(processing_status)
if error_message is not None:
update_fields.append("error_message = ?")
params.append(error_message)
if update_fields:
update_fields.append("updated_at = CURRENT_TIMESTAMP")
params.append(log_id)
sql = f"UPDATE risk_control_logs SET {', '.join(update_fields)} WHERE id = ?"
cursor.execute(sql, params)
self.conn.commit()
return cursor.rowcount > 0
return False
except Exception as e:
logger.error(f"更新风控日志失败: {e}")
return False
def get_risk_control_logs(self, cookie_id: str = None, limit: int = 100, offset: int = 0) -> List[Dict]:
"""
获取风控日志列表
Args:
cookie_id: Cookie ID为None时获取所有日志
limit: 限制返回数量
offset: 偏移量
Returns:
List[Dict]: 风控日志列表
"""
try:
with self.lock:
cursor = self.conn.cursor()
if cookie_id:
cursor.execute('''
SELECT r.*, c.id as cookie_name
FROM risk_control_logs r
LEFT JOIN cookies c ON r.cookie_id = c.id
WHERE r.cookie_id = ?
ORDER BY r.created_at DESC
LIMIT ? OFFSET ?
''', (cookie_id, limit, offset))
else:
cursor.execute('''
SELECT r.*, c.id as cookie_name
FROM risk_control_logs r
LEFT JOIN cookies c ON r.cookie_id = c.id
ORDER BY r.created_at DESC
LIMIT ? OFFSET ?
''', (limit, offset))
columns = [description[0] for description in cursor.description]
logs = []
for row in cursor.fetchall():
log_info = dict(zip(columns, row))
logs.append(log_info)
return logs
except Exception as e:
logger.error(f"获取风控日志失败: {e}")
return []
def get_risk_control_logs_count(self, cookie_id: str = None) -> int:
"""
获取风控日志总数
Args:
cookie_id: Cookie ID为None时获取所有日志数量
Returns:
int: 日志总数
"""
try:
with self.lock:
cursor = self.conn.cursor()
if cookie_id:
cursor.execute('SELECT COUNT(*) FROM risk_control_logs WHERE cookie_id = ?', (cookie_id,))
else:
cursor.execute('SELECT COUNT(*) FROM risk_control_logs')
return cursor.fetchone()[0]
except Exception as e:
logger.error(f"获取风控日志数量失败: {e}")
return 0
def delete_risk_control_log(self, log_id: int) -> bool:
"""
删除风控日志记录
Args:
log_id: 日志ID
Returns:
bool: 删除成功返回True失败返回False
"""
try:
with self.lock:
cursor = self.conn.cursor()
cursor.execute('DELETE FROM risk_control_logs WHERE id = ?', (log_id,))
self.conn.commit()
return cursor.rowcount > 0
except Exception as e:
logger.error(f"删除风控日志失败: {e}")
return False
def cleanup_old_data(self, days: int = 90) -> dict:
"""清理过期的历史数据,防止数据库无限增长
Args:
days: 保留最近N天的数据默认90天
Returns:
清理统计信息
"""
try:
with self.lock:
cursor = self.conn.cursor()
stats = {}
# 清理AI对话历史保留最近90天
try:
cursor.execute(
"DELETE FROM ai_conversations WHERE created_at < datetime('now', '-' || ? || ' days')",
(days,)
)
stats['ai_conversations'] = cursor.rowcount
if cursor.rowcount > 0:
logger.info(f"清理了 {cursor.rowcount} 条过期的AI对话记录{days}天前)")
except Exception as e:
logger.warning(f"清理AI对话历史失败: {e}")
stats['ai_conversations'] = 0
# 清理风控日志保留最近90天
try:
cursor.execute(
"DELETE FROM risk_control_logs WHERE created_at < datetime('now', '-' || ? || ' days')",
(days,)
)
stats['risk_control_logs'] = cursor.rowcount
if cursor.rowcount > 0:
logger.info(f"清理了 {cursor.rowcount} 条过期的风控日志({days}天前)")
except Exception as e:
logger.warning(f"清理风控日志失败: {e}")
stats['risk_control_logs'] = 0
# 清理AI商品缓存保留最近30天
cache_days = min(days, 30) # AI商品缓存最多保留30天
try:
cursor.execute(
"DELETE FROM ai_item_cache WHERE last_updated < datetime('now', '-' || ? || ' days')",
(cache_days,)
)
stats['ai_item_cache'] = cursor.rowcount
if cursor.rowcount > 0:
logger.info(f"清理了 {cursor.rowcount} 条过期的AI商品缓存{cache_days}天前)")
except Exception as e:
logger.warning(f"清理AI商品缓存失败: {e}")
stats['ai_item_cache'] = 0
# 清理验证码记录保留最近1天
try:
cursor.execute(
"DELETE FROM captcha_codes WHERE created_at < datetime('now', '-1 day')"
)
stats['captcha_codes'] = cursor.rowcount
if cursor.rowcount > 0:
logger.info(f"清理了 {cursor.rowcount} 条过期的验证码记录")
except Exception as e:
logger.warning(f"清理验证码记录失败: {e}")
stats['captcha_codes'] = 0
# 清理邮箱验证记录保留最近7天
try:
cursor.execute(
"DELETE FROM email_verifications WHERE created_at < datetime('now', '-7 days')"
)
stats['email_verifications'] = cursor.rowcount
if cursor.rowcount > 0:
logger.info(f"清理了 {cursor.rowcount} 条过期的邮箱验证记录")
except Exception as e:
logger.warning(f"清理邮箱验证记录失败: {e}")
stats['email_verifications'] = 0
# 提交更改
self.conn.commit()
# 执行VACUUM以释放磁盘空间仅当清理了大量数据时
total_cleaned = sum(stats.values())
if total_cleaned > 100:
logger.info(f"共清理了 {total_cleaned} 条记录执行VACUUM以释放磁盘空间...")
cursor.execute("VACUUM")
logger.info("VACUUM执行完成")
stats['vacuum_executed'] = True
else:
stats['vacuum_executed'] = False
stats['total_cleaned'] = total_cleaned
return stats
except Exception as e:
logger.error(f"清理历史数据时出错: {e}")
return {'error': str(e)}
# 全局单例

View File

@ -70,11 +70,11 @@ services:
deploy:
resources:
limits:
memory: ${MEMORY_LIMIT:-512}M
cpus: '${CPU_LIMIT:-0.5}'
memory: ${MEMORY_LIMIT:-2048}M
cpus: '${CPU_LIMIT:-2.0}'
reservations:
memory: ${MEMORY_RESERVATION:-256}M
cpus: '${CPU_RESERVATION:-0.25}'
memory: ${MEMORY_RESERVATION:-512}M
cpus: '${CPU_RESERVATION:-0.5}'
# 可选添加Nginx反向代理
nginx:

View File

@ -70,11 +70,11 @@ services:
deploy:
resources:
limits:
memory: ${MEMORY_LIMIT:-512}M
cpus: '${CPU_LIMIT:-0.5}'
memory: ${MEMORY_LIMIT:-2048}M
cpus: '${CPU_LIMIT:-2.0}'
reservations:
memory: ${MEMORY_RESERVATION:-256}M
cpus: '${CPU_RESERVATION:-0.25}'
memory: ${MEMORY_RESERVATION:-512}M
cpus: '${CPU_RESERVATION:-0.5}'
# 可选添加Nginx反向代理
nginx:

View File

@ -1,12 +1,65 @@
#!/bin/bash
set -e
echo "Starting xianyu-auto-reply system..."
echo "========================================"
echo " 闲鱼自动回复系统 - 启动中..."
echo "========================================"
# Create necessary directories
# 显示环境信息
echo "环境信息:"
echo " - Python版本: $(python --version)"
echo " - 工作目录: $(pwd)"
echo " - 时区: ${TZ:-未设置}"
echo " - 数据库路径: ${DB_PATH:-/app/data/xianyu_data.db}"
echo " - 日志级别: ${LOG_LEVEL:-INFO}"
# 禁用 core dumps 防止生成 core 文件
ulimit -c 0
echo "✓ 已禁用 core dumps"
# 创建必要的目录
echo "创建必要的目录..."
mkdir -p /app/data /app/logs /app/backups /app/static/uploads/images
mkdir -p /app/trajectory_history
echo "✓ 目录创建完成"
# Set permissions
# 设置目录权限
echo "设置目录权限..."
chmod 777 /app/data /app/logs /app/backups /app/static/uploads /app/static/uploads/images
chmod 777 /app/trajectory_history 2>/dev/null || true
echo "✓ 权限设置完成"
# Start the application
# 检查关键文件
echo "检查关键文件..."
if [ ! -f "/app/global_config.yml" ]; then
echo "⚠ 警告: 全局配置文件不存在,将使用默认配置"
fi
if [ ! -f "/app/Start.py" ]; then
echo "✗ 错误: Start.py 文件不存在!"
exit 1
fi
echo "✓ 关键文件检查完成"
# 检查 Python 依赖
echo "检查 Python 依赖..."
python -c "import fastapi, uvicorn, loguru, websockets" 2>/dev/null || {
echo "⚠ 警告: 部分 Python 依赖可能未正确安装"
}
echo "✓ Python 依赖检查完成"
# 显示启动信息
echo "========================================"
echo " 系统启动参数:"
echo " - API端口: ${API_PORT:-8080}"
echo " - API主机: ${API_HOST:-0.0.0.0}"
echo " - Debug模式: ${DEBUG:-false}"
echo " - 自动重载: ${RELOAD:-false}"
echo "========================================"
# 启动应用
echo "正在启动应用..."
echo ""
# 使用 exec 替换当前 shell这样 Python 进程可以接收信号
exec python Start.py

View File

@ -63,9 +63,9 @@ class FileLogCollector:
logger.add(
self.log_file,
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level} | {name}:{function}:{line} - {message}",
level="DEBUG",
level="INFO", # 从DEBUG改为INFO减少日志量
rotation="10 MB",
retention="7 days",
retention="3 days", # 从7天改为3天减少磁盘占用
enqueue=False, # 改为False避免队列延迟
buffering=1 # 行缓冲,立即写入
)

View File

@ -58,8 +58,11 @@ MANUAL_MODE:
timeout: 3600
toggle_keywords: []
MESSAGE_EXPIRE_TIME: 300000
TOKEN_REFRESH_INTERVAL: 72000 # 从3600秒(1小时)增加到72000秒(20小时)
TOKEN_RETRY_INTERVAL: 7200 # 从300秒(5分钟)增加到7200秒(2小时)
TOKEN_REFRESH_INTERVAL: 3600 # 从3600秒(1小时)增加到72000秒(20小时)
TOKEN_RETRY_INTERVAL: 600 # 从300秒(5分钟)增加到7200秒(2小时)
SLIDER_VERIFICATION:
max_concurrent: 3 # 滑块验证最大并发数
wait_timeout: 60 # 等待排队超时时间(秒)
WEBSOCKET_HEADERS:
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9

1047
order_status_handler.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1171,9 +1171,23 @@ def update_cookie(cid: str, item: CookieIn, current_user: Dict[str, Any] = Depen
if cid not in user_cookies:
raise HTTPException(status_code=403, detail="无权限操作该Cookie")
# 更新cookie时保持用户绑定
db_manager.save_cookie(cid, item.value, user_id)
cookie_manager.manager.update_cookie(cid, item.value)
# 获取旧的 cookie 值,用于判断是否需要重启任务
old_cookie_details = db_manager.get_cookie_details(cid)
old_cookie_value = old_cookie_details.get('value') if old_cookie_details else None
# 使用 update_cookie_account_info 更新只更新cookie值不覆盖其他字段
success = db_manager.update_cookie_account_info(cid, cookie_value=item.value)
if not success:
raise HTTPException(status_code=400, detail="更新Cookie失败")
# 只有当 cookie 值真的发生变化时才重启任务
if item.value != old_cookie_value:
logger.info(f"Cookie值已变化重启任务: {cid}")
cookie_manager.manager.update_cookie(cid, item.value, save_to_db=False)
else:
logger.info(f"Cookie值未变化无需重启任务: {cid}")
return {'msg': 'updated'}
except HTTPException:
raise
@ -1181,6 +1195,85 @@ def update_cookie(cid: str, item: CookieIn, current_user: Dict[str, Any] = Depen
raise HTTPException(status_code=400, detail=str(e))
class CookieAccountInfo(BaseModel):
"""账号信息更新模型"""
value: Optional[str] = None
username: Optional[str] = None
password: Optional[str] = None
show_browser: Optional[bool] = None
@app.post("/cookie/{cid}/account-info")
def update_cookie_account_info(cid: str, info: CookieAccountInfo, current_user: Dict[str, Any] = Depends(get_current_user)):
"""更新账号信息Cookie、用户名、密码、显示浏览器设置"""
if cookie_manager.manager is None:
raise HTTPException(status_code=500, detail='CookieManager 未就绪')
try:
# 检查cookie是否属于当前用户
user_id = current_user['user_id']
from db_manager import db_manager
user_cookies = db_manager.get_all_cookies(user_id)
if cid not in user_cookies:
raise HTTPException(status_code=403, detail="无权限操作该Cookie")
# 获取旧的 cookie 值,用于判断是否需要重启任务
old_cookie_details = db_manager.get_cookie_details(cid)
old_cookie_value = old_cookie_details.get('value') if old_cookie_details else None
# 更新数据库
success = db_manager.update_cookie_account_info(
cid,
cookie_value=info.value,
username=info.username,
password=info.password,
show_browser=info.show_browser
)
if not success:
raise HTTPException(status_code=400, detail="更新账号信息失败")
# 只有当 cookie 值真的发生变化时才重启任务
if info.value is not None and info.value != old_cookie_value:
logger.info(f"Cookie值已变化重启任务: {cid}")
cookie_manager.manager.update_cookie(cid, info.value, save_to_db=False)
else:
logger.info(f"Cookie值未变化无需重启任务: {cid}")
return {'msg': 'updated', 'success': True}
except HTTPException:
raise
except Exception as e:
logger.error(f"更新账号信息失败: {e}")
raise HTTPException(status_code=400, detail=str(e))
@app.get("/cookie/{cid}/details")
def get_cookie_account_details(cid: str, current_user: Dict[str, Any] = Depends(get_current_user)):
"""获取账号详细信息(包括用户名、密码、显示浏览器设置)"""
try:
# 检查cookie是否属于当前用户
user_id = current_user['user_id']
from db_manager import db_manager
user_cookies = db_manager.get_all_cookies(user_id)
if cid not in user_cookies:
raise HTTPException(status_code=403, detail="无权限操作该Cookie")
# 获取详细信息
details = db_manager.get_cookie_details(cid)
if not details:
raise HTTPException(status_code=404, detail="账号不存在")
return details
except HTTPException:
raise
except Exception as e:
logger.error(f"获取账号详情失败: {e}")
raise HTTPException(status_code=400, detail=str(e))
# ========================= 扫码登录相关接口 =========================
@app.post("/qr-login/generate")
@ -1338,7 +1431,8 @@ async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[st
cookie_manager.manager.add_cookie(account_id, real_cookies)
log_with_user('info', f"已将真实cookie添加到cookie_manager: {account_id}", current_user)
else:
cookie_manager.manager.update_cookie(account_id, real_cookies)
# refresh_cookies_from_qr_login 已经保存到数据库了,这里不需要再保存
cookie_manager.manager.update_cookie(account_id, real_cookies, save_to_db=False)
log_with_user('info', f"已更新cookie_manager中的真实cookie: {account_id}", current_user)
return {
@ -1376,7 +1470,8 @@ async def _fallback_save_qr_cookie(account_id: str, cookies: str, user_id: int,
db_manager.save_cookie(account_id, cookies, user_id)
log_with_user('info', f"降级处理 - 新账号原始cookie已保存: {account_id}", current_user)
else:
db_manager.save_cookie(account_id, cookies, user_id)
# 现有账号使用 update_cookie_account_info 避免覆盖其他字段
db_manager.update_cookie_account_info(account_id, cookie_value=cookies)
log_with_user('info', f"降级处理 - 现有账号原始cookie已更新: {account_id}", current_user)
# 添加到或更新cookie_manager
@ -1385,7 +1480,8 @@ async def _fallback_save_qr_cookie(account_id: str, cookies: str, user_id: int,
cookie_manager.manager.add_cookie(account_id, cookies)
log_with_user('info', f"降级处理 - 已将原始cookie添加到cookie_manager: {account_id}", current_user)
else:
cookie_manager.manager.update_cookie(account_id, cookies)
# update_cookie_account_info 已经保存到数据库了,这里不需要再保存
cookie_manager.manager.update_cookie(account_id, cookies, save_to_db=False)
log_with_user('info', f"降级处理 - 已更新cookie_manager中的原始cookie: {account_id}", current_user)
return {
@ -1444,7 +1540,8 @@ async def refresh_cookies_from_qr_login(
# 从数据库获取更新后的cookie
updated_cookie_info = db_manager.get_cookie_by_id(cookie_id)
if updated_cookie_info:
cookie_manager.manager.update_cookie(cookie_id, updated_cookie_info['cookies_str'])
# refresh_cookies_from_qr_login 已经保存到数据库了,这里不需要再保存
cookie_manager.manager.update_cookie(cookie_id, updated_cookie_info['cookies_str'], save_to_db=False)
log_with_user('info', f"已更新cookie_manager中的cookie: {cookie_id}", current_user)
return {
@ -3636,6 +3733,64 @@ async def get_logs(lines: int = 200, level: str = None, source: str = None, _: N
return {"success": False, "message": f"获取日志失败: {str(e)}", "logs": []}
@app.get("/risk-control-logs")
async def get_risk_control_logs(
cookie_id: str = None,
limit: int = 100,
offset: int = 0,
admin_user: Dict[str, Any] = Depends(require_admin)
):
"""获取风控日志(管理员专用)"""
try:
log_with_user('info', f"查询风控日志: cookie_id={cookie_id}, limit={limit}, offset={offset}", admin_user)
# 获取风控日志
logs = db_manager.get_risk_control_logs(cookie_id=cookie_id, limit=limit, offset=offset)
total_count = db_manager.get_risk_control_logs_count(cookie_id=cookie_id)
log_with_user('info', f"风控日志查询成功,共 {len(logs)} 条记录,总计 {total_count}", admin_user)
return {
"success": True,
"data": logs,
"total": total_count,
"limit": limit,
"offset": offset
}
except Exception as e:
log_with_user('error', f"获取风控日志失败: {str(e)}", admin_user)
return {
"success": False,
"message": f"获取风控日志失败: {str(e)}",
"data": [],
"total": 0
}
@app.delete("/risk-control-logs/{log_id}")
async def delete_risk_control_log(
log_id: int,
admin_user: Dict[str, Any] = Depends(require_admin)
):
"""删除风控日志记录(管理员专用)"""
try:
log_with_user('info', f"删除风控日志记录: {log_id}", admin_user)
success = db_manager.delete_risk_control_log(log_id)
if success:
log_with_user('info', f"风控日志删除成功: {log_id}", admin_user)
return {"success": True, "message": "删除成功"}
else:
log_with_user('warning', f"风控日志删除失败: {log_id}", admin_user)
return {"success": False, "message": "删除失败,记录可能不存在"}
except Exception as e:
log_with_user('error', f"删除风控日志失败: {log_id} - {str(e)}", admin_user)
return {"success": False, "message": f"删除失败: {str(e)}"}
@app.get("/logs/stats")
async def get_log_stats(_: None = Depends(require_auth)):
"""获取日志统计信息"""
@ -3888,6 +4043,85 @@ def delete_user(user_id: int, admin_user: Dict[str, Any] = Depends(require_admin
log_with_user('error', f"删除用户异常: {str(e)}", admin_user)
raise HTTPException(status_code=500, detail=str(e))
@app.get('/admin/risk-control-logs')
async def get_admin_risk_control_logs(
cookie_id: str = None,
limit: int = 100,
offset: int = 0,
admin_user: Dict[str, Any] = Depends(require_admin)
):
"""获取风控日志(管理员专用)"""
try:
log_with_user('info', f"查询风控日志: cookie_id={cookie_id}, limit={limit}, offset={offset}", admin_user)
# 获取风控日志
logs = db_manager.get_risk_control_logs(cookie_id=cookie_id, limit=limit, offset=offset)
total_count = db_manager.get_risk_control_logs_count(cookie_id=cookie_id)
log_with_user('info', f"风控日志查询成功,共 {len(logs)} 条记录,总计 {total_count}", admin_user)
return {
"success": True,
"data": logs,
"total": total_count,
"limit": limit,
"offset": offset
}
except Exception as e:
log_with_user('error', f"查询风控日志失败: {str(e)}", admin_user)
return {"success": False, "message": f"查询失败: {str(e)}", "data": [], "total": 0}
@app.get('/admin/cookies')
def get_admin_cookies(admin_user: Dict[str, Any] = Depends(require_admin)):
"""获取所有Cookie信息管理员专用"""
try:
log_with_user('info', "查询所有Cookie信息", admin_user)
if cookie_manager.manager is None:
return {
"success": True,
"cookies": [],
"message": "CookieManager 未就绪"
}
# 获取所有用户的cookies
from db_manager import db_manager
all_users = db_manager.get_all_users()
all_cookies = []
for user in all_users:
user_id = user['id']
user_cookies = db_manager.get_all_cookies(user_id)
for cookie_id, cookie_value in user_cookies.items():
# 获取cookie详细信息
cookie_details = db_manager.get_cookie_details(cookie_id)
cookie_info = {
'cookie_id': cookie_id,
'user_id': user_id,
'username': user['username'],
'nickname': cookie_details.get('remark', '') if cookie_details else '',
'enabled': cookie_manager.manager.get_cookie_status(cookie_id)
}
all_cookies.append(cookie_info)
log_with_user('info', f"获取到 {len(all_cookies)} 个Cookie", admin_user)
return {
"success": True,
"cookies": all_cookies,
"total": len(all_cookies)
}
except Exception as e:
log_with_user('error', f"获取Cookie信息失败: {str(e)}", admin_user)
return {
"success": False,
"cookies": [],
"message": f"获取失败: {str(e)}"
}
@app.get('/admin/logs')
def get_system_logs(admin_user: Dict[str, Any] = Depends(require_admin),
lines: int = 100,
@ -4432,7 +4666,8 @@ def clear_table_data(table_name: str, admin_user: Dict[str, Any] = Depends(requi
'cookies', 'cookie_status', 'keywords', 'default_replies', 'default_reply_records',
'ai_reply_settings', 'ai_conversations', 'ai_item_cache', 'item_info',
'message_notifications', 'cards', 'delivery_rules', 'notification_channels',
'user_settings', 'system_settings', 'email_verifications', 'captcha_codes', 'orders', "item_replay"
'user_settings', 'system_settings', 'email_verifications', 'captcha_codes', 'orders', 'item_replay',
'risk_control_logs'
]
# 不允许清空用户表

View File

@ -32,7 +32,7 @@ psutil>=5.9.0
python-multipart>=0.0.6
# ==================== AI回复引擎 ====================
openai>=1.65.5
openai>=1.50.0
# ==================== 图像处理 ====================
Pillow>=10.0.0
@ -40,6 +40,7 @@ qrcode[pil]>=7.4.2
# ==================== 浏览器自动化 ====================
playwright>=1.40.0
DrissionPage>=4.0.0
# ==================== 加密和安全 ====================
PyJWT>=2.8.0
@ -59,6 +60,13 @@ email-validator>=2.0.0
# ==================== 数据处理和验证 ====================
xlsxwriter>=3.1.0
# ==================== 构建二进制扩展模块(可选) ====================
# 用于编译性能关键模块,提升运行效率
# 如果不需要编译功能,可以注释掉以下依赖
nuitka>=2.7
ordered-set>=4.1.0
zstandard>=0.22.0
# ==================== 版本说明 ====================
# 本文件包含闲鱼自动回复系统的所有必需依赖
# 版本号已经过测试验证,确保兼容性和稳定性
@ -71,6 +79,9 @@ xlsxwriter>=3.1.0
# playwright install chromium
# playwright install-deps chromium # Linux系统需要
#
# 注意DrissionPage需要Chrome/Chromium浏览器支持滑块验证功能
# 如果系统没有Chrome请先安装Chrome浏览器
#
# ==================== 内置模块说明 ====================
# 以下模块是Python内置模块无需安装
# sqlite3, json, base64, hashlib, hmac, time, datetime, os, sys, re, urllib

View File

@ -114,6 +114,12 @@
系统日志
</a>
</div>
<div class="nav-item">
<a href="#" class="nav-link" onclick="showSection('risk-control-logs')">
<i class="bi bi-shield-exclamation"></i>
风控日志
</a>
</div>
<div class="nav-item">
<a href="#" class="nav-link" onclick="showSection('data-management')">
<i class="bi bi-database"></i>
@ -252,13 +258,13 @@
<i class="bi bi-qr-code me-2"></i>
<span class="fw-bold">扫码登录</span>
<br>
<small class="opacity-75">不推荐,一般都不成功</small>
<small class="opacity-75">推荐方式</small>
</button>
<button type="button" class="btn btn-outline-secondary btn-lg flex-fill manual-input-btn" onclick="toggleManualInput()" style="max-width: 300px;">
<i class="bi bi-keyboard me-2"></i>
<span class="fw-bold">手动输入</span>
<br>
<small class="opacity-75">推荐方式,使用消息界面的cookie</small>
<small class="opacity-75">使用消息界面的cookie</small>
</button>
</div>
</div>
@ -636,7 +642,9 @@
<option value="">所有状态</option>
<option value="processing">处理中</option>
<option value="processed">已处理</option>
<option value="shipped">已发货</option>
<option value="completed">已完成</option>
<option value="cancelled">已关闭</option>
<option value="unknown">未知</option>
</select>
</div>
@ -1382,6 +1390,131 @@
</div>
</div>
<!-- 风控日志内容 -->
<div id="risk-control-logs-section" class="content-section">
<div class="content-header">
<h2 class="mb-0">
<i class="bi bi-shield-exclamation me-2"></i>
风控日志
</h2>
<p class="text-muted mb-0">查看滑块验证等风控事件的处理记录</p>
</div>
<div class="content-body">
<!-- 风控日志控制面板 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-funnel me-2"></i>
筛选条件
</h5>
</div>
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-4">
<label class="form-label">账号筛选</label>
<select class="form-select" id="riskLogCookieFilter" onchange="loadRiskControlLogs()">
<option value="">全部账号</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">显示数量</label>
<select class="form-select" id="riskLogLimit" onchange="loadRiskControlLogs()">
<option value="50">50条</option>
<option value="100" selected>100条</option>
<option value="200">200条</option>
<option value="500">500条</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">处理状态</label>
<div class="d-flex gap-2 flex-wrap">
<span class="badge bg-secondary filter-badge active" data-status="" onclick="filterRiskLogsByStatus('')">
全部
</span>
<span class="badge bg-warning filter-badge" data-status="processing" onclick="filterRiskLogsByStatus('processing')">
处理中
</span>
<span class="badge bg-success filter-badge" data-status="success" onclick="filterRiskLogsByStatus('success')">
成功
</span>
<span class="badge bg-danger filter-badge" data-status="failed" onclick="filterRiskLogsByStatus('failed')">
失败
</span>
</div>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="d-grid">
<button class="btn btn-primary" onclick="loadRiskControlLogs()">
<i class="bi bi-arrow-clockwise"></i> 刷新
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 风控日志列表 -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-list-ul me-2"></i>
风控日志记录
</h5>
<div class="d-flex gap-2">
<span id="riskLogCount" class="badge bg-info">总计: 0 条</span>
<button class="btn btn-sm btn-outline-danger" onclick="clearRiskControlLogs()" title="清空日志">
<i class="bi bi-trash"></i> 清空
</button>
</div>
</div>
<div class="card-body p-0">
<div id="loadingRiskLogs" class="text-center py-4">
<div class="spinner-border" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p class="mt-2">正在加载风控日志...</p>
</div>
<div id="riskLogContainer" style="display: none;">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th width="120">时间</th>
<th width="100">账号</th>
<th width="100">事件类型</th>
<th width="80">状态</th>
<th>事件描述</th>
<th>处理结果</th>
<th width="80">操作</th>
</tr>
</thead>
<tbody id="riskLogTableBody">
</tbody>
</table>
</div>
</div>
<div id="noRiskLogs" class="text-center py-4" style="display: none;">
<i class="bi bi-shield-exclamation" style="font-size: 3rem; color: #ccc;"></i>
<p class="mt-2 text-muted">暂无风控日志数据</p>
</div>
</div>
</div>
<!-- 分页控件 -->
<div class="d-flex justify-content-between align-items-center mt-3">
<div class="text-muted">
<span id="riskLogPaginationInfo">显示第 0-0 条,共 0 条记录</span>
</div>
<nav>
<ul class="pagination pagination-sm mb-0" id="riskLogPagination">
</ul>
</nav>
</div>
</div>
</div>
<!-- 商品搜索内容 -->
<div id="item-search-section" class="content-section">
<div class="content-header">
@ -3382,5 +3515,73 @@
</div>
</div>
<!-- 账号编辑模态框 -->
<div class="modal fade" id="accountEditModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-pencil-square me-2"></i>
编辑账号信息 - <span id="editAccountIdDisplay"></span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="accountEditForm">
<input type="hidden" id="editAccountId">
<div class="mb-3">
<label class="form-label">Cookie <span class="text-danger">*</span></label>
<textarea class="form-control" id="editAccountCookie" rows="3" placeholder="输入Cookie值" required></textarea>
<div class="form-text">账号的Cookie值用于身份认证</div>
</div>
<div class="mb-3">
<label class="form-label">用户名</label>
<input type="text" class="form-control" id="editAccountUsername" placeholder="输入用户名(可选)">
<div class="form-text">用于密码登录的用户名</div>
</div>
<div class="mb-3">
<label class="form-label">密码</label>
<input type="password" class="form-control" id="editAccountPassword" placeholder="输入密码(可选)">
<div class="form-text">用于密码登录的密码</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="editAccountShowBrowser">
<label class="form-check-label" for="editAccountShowBrowser">
显示浏览器
<i class="bi bi-info-circle text-info ms-1" data-bs-toggle="tooltip" title="使用用户名密码登录的时候浏览器可见仅限Windows源码部署打开该开关"></i>
</label>
</div>
<div class="form-text text-warning">
<i class="bi bi-exclamation-triangle me-1"></i>
使用用户名密码登录的时候浏览器可见仅限Windows源码部署打开该开关开关打开密码登录会停留5分钟防止第一次登录需要人脸成功一次之后后续可关闭开关
</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
<strong>提示:</strong>
<ul class="mb-0 mt-2">
<li>Cookie是必填项其他字段可选</li>
<li>配置用户名和密码后系统可自动通过密码登录刷新Cookie</li>
<li>使用用户名密码登录的时候浏览器可见仅限Windows源码部署打开该开关开关打开密码登录会停留5分钟防止第一次登录需要人脸成功一次之后后续可关闭开关</li>
</ul>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveAccountEdit()">
<i class="bi bi-check-lg me-1"></i>保存
</button>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -112,6 +112,17 @@ function showSection(sectionName) {
}
}, 100);
break;
case 'risk-control-logs': // 【风控日志菜单】
// 自动加载风控日志
setTimeout(() => {
const riskLogContainer = document.getElementById('riskLogContainer');
if (riskLogContainer) {
console.log('首次进入风控日志页面,自动加载日志...');
loadRiskControlLogs();
loadCookieFilterOptions();
}
}, 100);
break;
case 'user-management': // 【用户管理菜单】
loadUserManagement();
break;
@ -1518,71 +1529,86 @@ async function delCookie(id) {
}
// 内联编辑Cookie
function editCookieInline(id, currentValue) {
const row = event.target.closest('tr');
const cookieValueCell = row.querySelector('.cookie-value');
const originalContent = cookieValueCell.innerHTML;
// 存储原始数据到全局变量避免HTML注入问题
window.editingCookieData = {
id: id,
originalContent: originalContent,
originalValue: currentValue || ''
};
// 创建编辑界面容器
const editContainer = document.createElement('div');
editContainer.className = 'd-flex gap-2';
// 创建输入框
const input = document.createElement('input');
input.type = 'text';
input.className = 'form-control form-control-sm';
input.id = `edit-${id}`;
input.value = currentValue || '';
input.placeholder = '输入新的Cookie值';
// 创建保存按钮
const saveBtn = document.createElement('button');
saveBtn.className = 'btn btn-sm btn-success';
saveBtn.title = '保存';
saveBtn.innerHTML = '<i class="bi bi-check"></i>';
saveBtn.onclick = () => saveCookieInline(id);
// 创建取消按钮
const cancelBtn = document.createElement('button');
cancelBtn.className = 'btn btn-sm btn-secondary';
cancelBtn.title = '取消';
cancelBtn.innerHTML = '<i class="bi bi-x"></i>';
cancelBtn.onclick = () => cancelCookieEdit(id);
// 组装编辑界面
editContainer.appendChild(input);
editContainer.appendChild(saveBtn);
editContainer.appendChild(cancelBtn);
// 替换原内容
cookieValueCell.innerHTML = '';
cookieValueCell.appendChild(editContainer);
// 聚焦输入框
input.focus();
input.select();
// 添加键盘事件监听
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
saveCookieInline(id);
} else if (e.key === 'Escape') {
e.preventDefault();
cancelCookieEdit(id);
async function editCookieInline(id, currentValue) {
try {
toggleLoading(true);
// 获取账号详细信息
const details = await fetchJSON(apiBase + `/cookie/${id}/details`);
// 打开编辑模态框
openAccountEditModal(details);
} catch (err) {
console.error('获取账号详情失败:', err);
showToast(`获取账号详情失败: ${err.message || '未知错误'}`, 'danger');
} finally {
toggleLoading(false);
}
});
}
// 禁用该行的其他按钮
const actionButtons = row.querySelectorAll('.btn-group button');
actionButtons.forEach(btn => btn.disabled = true);
// 打开账号编辑模态框
function openAccountEditModal(accountData) {
// 设置模态框数据
document.getElementById('editAccountId').value = accountData.id;
document.getElementById('editAccountCookie').value = accountData.value || '';
document.getElementById('editAccountUsername').value = accountData.username || '';
document.getElementById('editAccountPassword').value = accountData.password || '';
document.getElementById('editAccountShowBrowser').checked = accountData.show_browser || false;
// 显示账号ID
document.getElementById('editAccountIdDisplay').textContent = accountData.id;
// 打开模态框
const modal = new bootstrap.Modal(document.getElementById('accountEditModal'));
modal.show();
// 初始化模态框中的 tooltips
setTimeout(() => {
initTooltips();
}, 100);
}
// 保存账号编辑
async function saveAccountEdit() {
const id = document.getElementById('editAccountId').value;
const cookie = document.getElementById('editAccountCookie').value.trim();
const username = document.getElementById('editAccountUsername').value.trim();
const password = document.getElementById('editAccountPassword').value.trim();
const showBrowser = document.getElementById('editAccountShowBrowser').checked;
if (!cookie) {
showToast('Cookie值不能为空', 'warning');
return;
}
try {
toggleLoading(true);
await fetchJSON(apiBase + `/cookie/${id}/account-info`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
value: cookie,
username: username,
password: password,
show_browser: showBrowser
})
});
showToast(`账号 "${id}" 信息已更新`, 'success');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('accountEditModal'));
modal.hide();
// 重新加载账号列表
loadCookies();
} catch (err) {
console.error('保存账号信息失败:', err);
showToast(`保存失败: ${err.message || '未知错误'}`, 'danger');
} finally {
toggleLoading(false);
}
}
// 保存内联编辑的Cookie
@ -8613,6 +8639,7 @@ function getOrderStatusClass(status) {
'processing': 'bg-warning text-dark',
'processed': 'bg-info text-white',
'completed': 'bg-success text-white',
'cancelled': 'bg-danger text-white',
'unknown': 'bg-secondary text-white'
};
return statusMap[status] || 'bg-secondary text-white';
@ -8623,7 +8650,9 @@ function getOrderStatusText(status) {
const statusMap = {
'processing': '处理中',
'processed': '已处理',
'shipped': '已发货',
'completed': '已完成',
'cancelled': '已关闭',
'unknown': '未知'
};
return statusMap[status] || '未知';
@ -9823,6 +9852,263 @@ function scrollLogToBottom() {
logContainer.scrollTop = logContainer.scrollHeight;
}
// ================================
// 风控日志管理功能
// ================================
let currentRiskLogStatus = '';
let currentRiskLogOffset = 0;
const riskLogLimit = 100;
// 加载风控日志
async function loadRiskControlLogs(offset = 0) {
const token = localStorage.getItem('auth_token');
const cookieId = document.getElementById('riskLogCookieFilter').value;
const limit = document.getElementById('riskLogLimit').value;
const loadingDiv = document.getElementById('loadingRiskLogs');
const logContainer = document.getElementById('riskLogContainer');
const noLogsDiv = document.getElementById('noRiskLogs');
loadingDiv.style.display = 'block';
logContainer.style.display = 'none';
noLogsDiv.style.display = 'none';
let url = `/admin/risk-control-logs?limit=${limit}&offset=${offset}`;
if (cookieId) {
url += `&cookie_id=${cookieId}`;
}
try {
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
loadingDiv.style.display = 'none';
if (data.success && data.data && data.data.length > 0) {
displayRiskControlLogs(data.data);
updateRiskLogInfo(data);
updateRiskLogPagination(data);
logContainer.style.display = 'block';
} else {
noLogsDiv.style.display = 'block';
updateRiskLogInfo({total: 0, data: []});
}
currentRiskLogOffset = offset;
} catch (error) {
console.error('加载风控日志失败:', error);
loadingDiv.style.display = 'none';
noLogsDiv.style.display = 'block';
showToast('加载风控日志失败', 'danger');
}
}
// 显示风控日志
function displayRiskControlLogs(logs) {
const tableBody = document.getElementById('riskLogTableBody');
tableBody.innerHTML = '';
logs.forEach(log => {
const row = document.createElement('tr');
// 格式化时间
const createdAt = new Date(log.created_at).toLocaleString('zh-CN');
// 状态标签
let statusBadge = '';
switch(log.processing_status) {
case 'processing':
statusBadge = '<span class="badge bg-warning">处理中</span>';
break;
case 'success':
statusBadge = '<span class="badge bg-success">成功</span>';
break;
case 'failed':
statusBadge = '<span class="badge bg-danger">失败</span>';
break;
default:
statusBadge = '<span class="badge bg-secondary">未知</span>';
}
row.innerHTML = `
<td class="text-nowrap">${createdAt}</td>
<td class="text-nowrap">${escapeHtml(log.cookie_id || '-')}</td>
<td class="text-nowrap">${escapeHtml(log.event_type || '-')}</td>
<td>${statusBadge}</td>
<td class="text-truncate" style="max-width: 200px;" title="${escapeHtml(log.event_description || '-')}">${escapeHtml(log.event_description || '-')}</td>
<td class="text-truncate" style="max-width: 200px;" title="${escapeHtml(log.processing_result || '-')}">${escapeHtml(log.processing_result || '-')}</td>
<td>
<button class="btn btn-sm btn-outline-danger" onclick="deleteRiskControlLog(${log.id})" title="删除">
<i class="bi bi-trash"></i>
</button>
</td>
`;
tableBody.appendChild(row);
});
}
// 更新风控日志信息
function updateRiskLogInfo(data) {
const countElement = document.getElementById('riskLogCount');
const paginationInfo = document.getElementById('riskLogPaginationInfo');
if (countElement) {
countElement.textContent = `总计: ${data.total || 0}`;
}
if (paginationInfo) {
const start = currentRiskLogOffset + 1;
const end = Math.min(currentRiskLogOffset + (data.data ? data.data.length : 0), data.total || 0);
paginationInfo.textContent = `显示第 ${start}-${end} 条,共 ${data.total || 0} 条记录`;
}
}
// 更新风控日志分页
function updateRiskLogPagination(data) {
const pagination = document.getElementById('riskLogPagination');
const limit = parseInt(document.getElementById('riskLogLimit').value);
const total = data.total || 0;
const totalPages = Math.ceil(total / limit);
const currentPage = Math.floor(currentRiskLogOffset / limit) + 1;
pagination.innerHTML = '';
if (totalPages <= 1) return;
// 上一页
const prevLi = document.createElement('li');
prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `<a class="page-link" href="#" onclick="loadRiskControlLogs(${(currentPage - 2) * limit})">上一页</a>`;
pagination.appendChild(prevLi);
// 页码
const startPage = Math.max(1, currentPage - 2);
const endPage = Math.min(totalPages, currentPage + 2);
for (let i = startPage; i <= endPage; i++) {
const li = document.createElement('li');
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
li.innerHTML = `<a class="page-link" href="#" onclick="loadRiskControlLogs(${(i - 1) * limit})">${i}</a>`;
pagination.appendChild(li);
}
// 下一页
const nextLi = document.createElement('li');
nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
nextLi.innerHTML = `<a class="page-link" href="#" onclick="loadRiskControlLogs(${currentPage * limit})">下一页</a>`;
pagination.appendChild(nextLi);
}
// 按状态过滤风控日志
function filterRiskLogsByStatus(status) {
currentRiskLogStatus = status;
// 更新过滤按钮状态
document.querySelectorAll('.filter-badge[data-status]').forEach(badge => {
badge.classList.remove('active');
});
document.querySelector(`.filter-badge[data-status="${status}"]`).classList.add('active');
// 重新加载日志
loadRiskControlLogs(0);
}
// 加载账号筛选选项
async function loadCookieFilterOptions() {
try {
const token = localStorage.getItem('auth_token');
const response = await fetch('/admin/cookies', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
const select = document.getElementById('riskLogCookieFilter');
// 清空现有选项,保留"全部账号"
select.innerHTML = '<option value="">全部账号</option>';
if (data.success && data.cookies) {
data.cookies.forEach(cookie => {
const option = document.createElement('option');
option.value = cookie.cookie_id;
option.textContent = `${cookie.cookie_id} (${cookie.nickname || '未知'})`;
select.appendChild(option);
});
}
}
} catch (error) {
console.error('加载账号选项失败:', error);
}
}
// 删除风控日志记录
async function deleteRiskControlLog(logId) {
if (!confirm('确定要删除这条风控日志记录吗?')) {
return;
}
try {
const token = localStorage.getItem('auth_token');
const response = await fetch(`/admin/risk-control-logs/${logId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (data.success) {
showToast('删除成功', 'success');
loadRiskControlLogs(currentRiskLogOffset);
} else {
showToast(data.message || '删除失败', 'danger');
}
} catch (error) {
console.error('删除风控日志失败:', error);
showToast('删除失败', 'danger');
}
}
// 清空风控日志
async function clearRiskControlLogs() {
if (!confirm('确定要清空所有风控日志吗?此操作不可恢复!')) {
return;
}
try {
const token = localStorage.getItem('auth_token');
// 调用后端批量清空接口(管理员)
const response = await fetch('/admin/data/risk_control_logs', {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (response.ok) {
showToast('风控日志已清空', 'success');
loadRiskControlLogs(0);
} else {
showToast(data.detail || data.message || '清空失败', 'danger');
}
} catch (error) {
console.error('清空风控日志失败:', error);
showToast('清空失败', 'danger');
}
}
// ================================
// 商品搜索功能
// ================================

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 145 KiB

View File

@ -1 +1 @@
v1.0.2
v1.0.3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 173 KiB

View File

@ -131,7 +131,8 @@ class XianyuSearcher:
if os.getenv('DOCKER_ENV') == 'true':
browser_args.extend([
'--disable-gpu',
'--single-process'
# 移除--single-process参数使用多进程模式提高稳定性
# '--single-process' # 注释掉,避免崩溃
])
logger.info("正在启动浏览器...")
@ -154,11 +155,19 @@ class XianyuSearcher:
async def close_browser(self):
"""关闭浏览器"""
if self.browser:
await self.browser.close()
self.browser = None
self.context = None
self.page = None
try:
if self.page:
await self.page.close()
self.page = None
if self.context:
await self.context.close()
self.context = None
if self.browser:
await self.browser.close()
self.browser = None
logger.debug("商品搜索器浏览器已关闭")
except Exception as e:
logger.warning(f"关闭商品搜索器浏览器时出错: {e}")
async def search_items(self, keyword: str, page: int = 1, page_size: int = 20) -> Dict[str, Any]:
"""

View File

@ -101,9 +101,9 @@ class OrderDetailFetcher:
'--no-pings'
]
# 只在Docker环境中使用单进程模式
if os.getenv('DOCKER_ENV'):
browser_args.append('--single-process')
# 移除--single-process参数使用多进程模式提高稳定性
# if os.getenv('DOCKER_ENV'):
# browser_args.append('--single-process') # 注释掉,避免崩溃
# 在Docker环境中添加额外参数
if os.getenv('DOCKER_ENV'):
@ -122,7 +122,18 @@ class OrderDetailFetcher:
'--safebrowsing-disable-auto-update',
'--enable-automation',
'--password-store=basic',
'--use-mock-keychain'
'--use-mock-keychain',
# 添加内存优化和稳定性参数
'--memory-pressure-off',
'--max_old_space_size=512',
'--disable-ipc-flooding-protection',
'--disable-component-extensions-with-background-pages',
'--disable-features=TranslateUI,BlinkGenPropertyTrees',
'--disable-logging',
'--disable-permissions-api',
'--disable-notifications',
'--no-pings',
'--no-zygote'
])
logger.info(f"启动浏览器,参数: {browser_args}")

2081
utils/refresh_util.py Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -0,0 +1,88 @@
# This file was generated by Nuitka
# Stubs included by default
from __future__ import annotations
from config import SLIDER_VERIFICATION
from datetime import datetime
from loguru import logger
from playwright.sync_api import ElementHandle, sync_playwright
from typing import Any, Dict, List, Optional, Tuple
from typing_extensions import Self
import json
import os
import random
import shutil
import tempfile
import threading
import time
SLIDER_MAX_CONCURRENT = 3
SLIDER_WAIT_TIMEOUT = 60
class SliderConcurrencyManager:
def __new__(cls: cls) -> Any: ...
def __init__(self: Self) -> None: ...
def can_start_instance(self: Self, user_id: str) -> bool: ...
def wait_for_slot(self: Self, user_id: str, timeout: int) -> bool: ...
def register_instance(self: Self, user_id: str, instance: Any) -> Any: ...
def unregister_instance(self: Self, user_id: str) -> Any: ...
def _extract_pure_user_id(self: Self, user_id: str) -> str: ...
def get_stats(self: Self) -> Any: ...
class XianyuSliderStealth:
def __init__(self: Self, user_id: str, enable_learning: bool, headless: bool) -> None: ...
def _check_date_validity(self: Self) -> bool: ...
def init_browser(self: Self) -> Any: ...
def _cleanup_on_init_failure(self: Self) -> Any: ...
def _load_success_history(self: Self) -> List[Dict[str, Any]]: ...
def _save_success_record(self: Self, trajectory_data: Dict[str, Any]) -> Any: ...
def _optimize_trajectory_params(self: Self) -> Dict[str, Any]: ...
def _get_cookies_after_success(self: Self) -> Any: ...
def _save_cookies_to_file(self: Self, cookies: Any) -> Any: ...
def _get_random_browser_features(self: Self) -> Any: ...
def _get_stealth_script(self: Self, browser_features: Any) -> Any: ...
def generate_human_trajectory(self: Self, distance: float) -> Any: ...
def simulate_slide(self: Self, slider_button: ElementHandle, trajectory: Any) -> Any: ...
def find_slider_elements(self: Self) -> Any: ...
def calculate_slide_distance(self: Self, slider_button: ElementHandle, slider_track: ElementHandle) -> Any: ...
def check_verification_success(self: Self, slider_button: ElementHandle) -> Any: ...
def check_page_changed(self: Self) -> Any: ...
def check_verification_failure(self: Self) -> Any: ...
def solve_slider(self: Self) -> Any: ...
def close_browser(self: Self) -> Any: ...
def __del__(self: Self) -> Any: ...
def login_with_password_headful(self: Self, account: str, password: str, show_browser: bool) -> Any: ...
def run(self: Self, url: str) -> Any: ...
def get_slider_stats() -> Any:
...
__name__ = ...
# Modules used internally, to allow implicit dependencies to be seen:
import time
import random
import json
import os
import threading
import tempfile
import shutil
import datetime
import playwright
import playwright.sync_api
import playwright.sync_api.sync_playwright
import playwright.sync_api.ElementHandle
import typing
import loguru
import loguru.logger
import config
import traceback
import re
import DrissionPage
import DrissionPage.ChromiumPage
import DrissionPage.ChromiumOptions
import subprocess
import platform
import sys