新增滑块验证
新增滑块验证
This commit is contained in:
commit
1515cfd8de
170
.dockerignore
170
.dockerignore
@ -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
251
.gitignore
vendored
@ -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 - 是核心模块,需要跟踪
|
||||
54
Dockerfile
54
Dockerfile
@ -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"]
|
||||
@ -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
138
README.md
@ -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、通义千问等)
|
||||
|
||||
1522
XianyuAutoAsync.py
1522
XianyuAutoAsync.py
File diff suppressed because it is too large
Load Diff
@ -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
155
build_binary_module.py
Normal 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())
|
||||
|
||||
12
config.py
12
config.py
@ -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 []
|
||||
|
||||
@ -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}")
|
||||
|
||||
|
||||
385
db_manager.py
385
db_manager.py
@ -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)}
|
||||
|
||||
|
||||
# 全局单例
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 # 行缓冲,立即写入
|
||||
)
|
||||
|
||||
@ -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
1047
order_status_handler.py
Normal file
File diff suppressed because it is too large
Load Diff
251
reply_server.py
251
reply_server.py
@ -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'
|
||||
]
|
||||
|
||||
# 不允许清空用户表
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"> </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>
|
||||
|
||||
412
static/js/app.js
412
static/js/app.js
@ -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 |
@ -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 |
@ -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]:
|
||||
"""
|
||||
|
||||
@ -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
2081
utils/refresh_util.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
utils/xianyu_slider_stealth.cp312-win_amd64.pyd
Normal file
BIN
utils/xianyu_slider_stealth.cp312-win_amd64.pyd
Normal file
Binary file not shown.
BIN
utils/xianyu_slider_stealth.cpython-311-x86_64-linux-gnu.so
Normal file
BIN
utils/xianyu_slider_stealth.cpython-311-x86_64-linux-gnu.so
Normal file
Binary file not shown.
88
utils/xianyu_slider_stealth.pyi
Normal file
88
utils/xianyu_slider_stealth.pyi
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user