mirror of
https://github.com/zhinianboke/xianyu-auto-reply.git
synced 2025-08-01 12:07:36 +08:00
首次提交
This commit is contained in:
commit
73b274cce8
91
.dockerignore
Normal file
91
.dockerignore
Normal file
@ -0,0 +1,91 @@
|
||||
# Git相关
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# Python相关
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# 虚拟环境
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDE相关
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# 操作系统相关
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# 日志文件
|
||||
logs/*.log
|
||||
*.log
|
||||
|
||||
# 数据库文件(构建时不包含,运行时挂载)
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
temp/
|
||||
tmp/
|
||||
|
||||
# 测试文件
|
||||
test_*.py
|
||||
*_test.py
|
||||
tests/
|
||||
|
||||
# 文档
|
||||
*.md
|
||||
docs/
|
||||
|
||||
# Docker相关
|
||||
Dockerfile*
|
||||
docker-compose*.yml
|
||||
.dockerignore
|
||||
|
||||
# 配置文件(运行时挂载)
|
||||
# global_config.yml
|
||||
|
||||
# 其他
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
103
.env
Normal file
103
.env
Normal file
@ -0,0 +1,103 @@
|
||||
# 闲鱼自动回复系统 Docker 环境变量配置文件
|
||||
# 这是默认配置,您可以根据需要修改
|
||||
|
||||
# ================================
|
||||
# 基础配置
|
||||
# ================================
|
||||
|
||||
# 时区设置
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
# Python配置
|
||||
PYTHONUNBUFFERED=1
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# 日志级别 (DEBUG, INFO, WARNING, ERROR)
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# ================================
|
||||
# 数据库配置
|
||||
# ================================
|
||||
|
||||
# 数据库文件路径
|
||||
DB_PATH=/app/data/xianyu_data.db
|
||||
|
||||
# ================================
|
||||
# 服务配置
|
||||
# ================================
|
||||
|
||||
# API服务配置
|
||||
API_HOST=0.0.0.0 # 绑定所有网络接口,支持IP访问
|
||||
API_PORT=8080 # Web服务端口
|
||||
|
||||
# Web服务端口 (Docker端口映射)
|
||||
WEB_PORT=8080
|
||||
|
||||
# ================================
|
||||
# 安全配置
|
||||
# ================================
|
||||
|
||||
# 管理员账号密码 (建议修改)
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=admin123
|
||||
|
||||
# JWT密钥 (建议修改为随机字符串)
|
||||
JWT_SECRET_KEY=xianyu-auto-reply-secret-key-2024
|
||||
|
||||
# Session超时时间 (秒)
|
||||
SESSION_TIMEOUT=3600
|
||||
|
||||
# ================================
|
||||
# 闲鱼API配置
|
||||
# ================================
|
||||
|
||||
# WebSocket连接URL
|
||||
WEBSOCKET_URL=wss://wss-goofish.dingtalk.com/
|
||||
|
||||
# 心跳间隔 (秒)
|
||||
HEARTBEAT_INTERVAL=15
|
||||
|
||||
# 心跳超时 (秒)
|
||||
HEARTBEAT_TIMEOUT=5
|
||||
|
||||
# Token刷新间隔 (秒)
|
||||
TOKEN_REFRESH_INTERVAL=3600
|
||||
|
||||
# Token重试间隔 (秒)
|
||||
TOKEN_RETRY_INTERVAL=300
|
||||
|
||||
# 消息过期时间 (毫秒)
|
||||
MESSAGE_EXPIRE_TIME=300000
|
||||
|
||||
# ================================
|
||||
# 自动回复配置
|
||||
# ================================
|
||||
|
||||
# 是否启用自动回复
|
||||
AUTO_REPLY_ENABLED=true
|
||||
|
||||
# ================================
|
||||
# 资源限制
|
||||
# ================================
|
||||
|
||||
# 内存限制 (MB)
|
||||
MEMORY_LIMIT=512
|
||||
|
||||
# CPU限制 (核心数)
|
||||
CPU_LIMIT=0.5
|
||||
|
||||
# 内存预留 (MB)
|
||||
MEMORY_RESERVATION=256
|
||||
|
||||
# CPU预留 (核心数)
|
||||
CPU_RESERVATION=0.25
|
||||
|
||||
# ================================
|
||||
# 开发配置
|
||||
# ================================
|
||||
|
||||
# 开发模式 (true/false)
|
||||
DEBUG=false
|
||||
|
||||
# 热重载 (true/false)
|
||||
RELOAD=false
|
192
.env.example
Normal file
192
.env.example
Normal file
@ -0,0 +1,192 @@
|
||||
# 闲鱼自动回复系统 Docker 环境变量配置文件
|
||||
# 复制此文件为 .env 并根据需要修改配置
|
||||
# 或者直接重命名为 .env 使用默认配置
|
||||
|
||||
# ================================
|
||||
# 基础配置
|
||||
# ================================
|
||||
|
||||
# 时区设置
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
# Python配置
|
||||
PYTHONUNBUFFERED=1
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# 日志级别 (DEBUG, INFO, WARNING, ERROR)
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# ================================
|
||||
# 数据库配置
|
||||
# ================================
|
||||
|
||||
# 数据库文件路径
|
||||
DB_PATH=/app/data/xianyu_data.db
|
||||
|
||||
# ================================
|
||||
# 服务配置
|
||||
# ================================
|
||||
|
||||
# Web服务端口
|
||||
WEB_PORT=8080
|
||||
|
||||
# API服务配置
|
||||
API_HOST=0.0.0.0
|
||||
API_PORT=8080
|
||||
|
||||
# ================================
|
||||
# 安全配置
|
||||
# ================================
|
||||
|
||||
# 管理员账号密码 (建议修改)
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=admin123
|
||||
|
||||
# JWT密钥 (建议修改为随机字符串)
|
||||
JWT_SECRET_KEY=your-secret-key-here
|
||||
|
||||
# Session超时时间 (秒)
|
||||
SESSION_TIMEOUT=3600
|
||||
|
||||
# ================================
|
||||
# 闲鱼API配置
|
||||
# ================================
|
||||
|
||||
# WebSocket连接URL
|
||||
WEBSOCKET_URL=wss://wss-goofish.dingtalk.com/
|
||||
|
||||
# 心跳间隔 (秒)
|
||||
HEARTBEAT_INTERVAL=15
|
||||
|
||||
# 心跳超时 (秒)
|
||||
HEARTBEAT_TIMEOUT=5
|
||||
|
||||
# Token刷新间隔 (秒)
|
||||
TOKEN_REFRESH_INTERVAL=3600
|
||||
|
||||
# Token重试间隔 (秒)
|
||||
TOKEN_RETRY_INTERVAL=300
|
||||
|
||||
# 消息过期时间 (毫秒)
|
||||
MESSAGE_EXPIRE_TIME=300000
|
||||
|
||||
# ================================
|
||||
# 自动回复配置
|
||||
# ================================
|
||||
|
||||
# 是否启用自动回复
|
||||
AUTO_REPLY_ENABLED=true
|
||||
|
||||
# 默认回复消息
|
||||
AUTO_REPLY_DEFAULT_MESSAGE=亲爱的"{send_user_name}" 老板你好!所有宝贝都可以拍,秒发货的哈~不满意的话可以直接申请退款哈~
|
||||
|
||||
# 最大重试次数
|
||||
AUTO_REPLY_MAX_RETRY=3
|
||||
|
||||
# 重试间隔 (秒)
|
||||
AUTO_REPLY_RETRY_INTERVAL=5
|
||||
|
||||
# ================================
|
||||
# 自动发货配置
|
||||
# ================================
|
||||
|
||||
# 是否启用自动发货
|
||||
AUTO_DELIVERY_ENABLED=true
|
||||
|
||||
# 自动发货超时时间 (秒)
|
||||
AUTO_DELIVERY_TIMEOUT=30
|
||||
|
||||
# API卡券请求超时时间 (秒)
|
||||
API_CARD_TIMEOUT=10
|
||||
|
||||
# 批量数据并发保护
|
||||
BATCH_DATA_LOCK_TIMEOUT=5
|
||||
|
||||
# ================================
|
||||
# 代理配置 (可选)
|
||||
# ================================
|
||||
|
||||
# HTTP代理
|
||||
# HTTP_PROXY=http://proxy.example.com:8080
|
||||
|
||||
# HTTPS代理
|
||||
# HTTPS_PROXY=https://proxy.example.com:8080
|
||||
|
||||
# 不使用代理的地址
|
||||
# NO_PROXY=localhost,127.0.0.1
|
||||
|
||||
# ================================
|
||||
# 监控配置
|
||||
# ================================
|
||||
|
||||
# 健康检查间隔 (秒)
|
||||
HEALTH_CHECK_INTERVAL=30
|
||||
|
||||
# 健康检查超时 (秒)
|
||||
HEALTH_CHECK_TIMEOUT=10
|
||||
|
||||
# ================================
|
||||
# 资源限制
|
||||
# ================================
|
||||
|
||||
# 内存限制 (MB)
|
||||
MEMORY_LIMIT=512
|
||||
|
||||
# CPU限制 (核心数)
|
||||
CPU_LIMIT=0.5
|
||||
|
||||
# ================================
|
||||
# 备份配置
|
||||
# ================================
|
||||
|
||||
# 自动备份间隔 (小时)
|
||||
BACKUP_INTERVAL=24
|
||||
|
||||
# 备份保留天数
|
||||
BACKUP_RETENTION_DAYS=7
|
||||
|
||||
# 备份目录
|
||||
BACKUP_DIR=/app/backups
|
||||
|
||||
# ================================
|
||||
# 开发配置
|
||||
# ================================
|
||||
|
||||
# 开发模式 (true/false)
|
||||
DEBUG=false
|
||||
|
||||
# 热重载 (true/false)
|
||||
RELOAD=false
|
||||
|
||||
# API文档 (true/false)
|
||||
ENABLE_DOCS=true
|
||||
|
||||
# ================================
|
||||
# 第三方服务配置
|
||||
# ================================
|
||||
|
||||
# 邮件通知配置 (可选)
|
||||
# SMTP_HOST=smtp.example.com
|
||||
# SMTP_PORT=587
|
||||
# SMTP_USERNAME=your-email@example.com
|
||||
# SMTP_PASSWORD=your-password
|
||||
# SMTP_FROM=noreply@example.com
|
||||
|
||||
# 钉钉机器人通知 (可选)
|
||||
# DINGTALK_WEBHOOK=https://oapi.dingtalk.com/robot/send?access_token=your-token
|
||||
|
||||
# 企业微信机器人通知 (可选)
|
||||
# WECHAT_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-key
|
||||
|
||||
# ================================
|
||||
# 自定义配置
|
||||
# ================================
|
||||
|
||||
# 自定义配置文件路径
|
||||
# CUSTOM_CONFIG_PATH=/app/custom_config.yml
|
||||
|
||||
# 插件目录
|
||||
# PLUGINS_DIR=/app/plugins
|
||||
|
||||
# 模板目录
|
||||
# TEMPLATES_DIR=/app/templates
|
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
__pycache__
|
||||
*node_modules/*
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
MANIFEST
|
||||
*.manifest
|
||||
*.spec
|
||||
.cache
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
__pypackages__/
|
||||
.venv
|
||||
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
67
Dockerfile
Normal file
67
Dockerfile
Normal file
@ -0,0 +1,67 @@
|
||||
# 使用Python 3.11作为基础镜像
|
||||
FROM python:3.11-slim
|
||||
|
||||
# 设置标签信息
|
||||
LABEL maintainer="Xianyu Auto Reply System"
|
||||
LABEL version="1.0.0"
|
||||
LABEL description="闲鱼自动回复系统 - Docker版本"
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 设置环境变量
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
nodejs \
|
||||
npm \
|
||||
tzdata \
|
||||
curl \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 设置时区
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
# 复制requirements.txt并安装Python依赖
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制项目文件
|
||||
COPY . .
|
||||
|
||||
# 创建必要的目录并设置权限
|
||||
RUN mkdir -p /app/logs /app/data /app/backups && \
|
||||
chmod 777 /app/logs /app/data /app/backups
|
||||
|
||||
# 注意: 为了简化权限问题,使用root用户运行
|
||||
# 在生产环境中,建议配置适当的用户映射
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
|
||||
# 创建启动脚本
|
||||
RUN echo '#!/bin/bash' > /app/entrypoint.sh && \
|
||||
echo 'set -e' >> /app/entrypoint.sh && \
|
||||
echo '' >> /app/entrypoint.sh && \
|
||||
echo 'echo "🚀 启动闲鱼自动回复系统..."' >> /app/entrypoint.sh && \
|
||||
echo '' >> /app/entrypoint.sh && \
|
||||
echo '# 数据库将在应用启动时自动初始化' >> /app/entrypoint.sh && \
|
||||
echo 'echo "📊 数据库将在应用启动时自动初始化..."' >> /app/entrypoint.sh && \
|
||||
echo '' >> /app/entrypoint.sh && \
|
||||
echo '# 启动主应用' >> /app/entrypoint.sh && \
|
||||
echo 'echo "🎯 启动主应用..."' >> /app/entrypoint.sh && \
|
||||
echo 'exec python Start.py' >> /app/entrypoint.sh && \
|
||||
chmod +x /app/entrypoint.sh
|
||||
|
||||
# 启动命令
|
||||
CMD ["/app/entrypoint.sh"]
|
344
Docker部署说明.md
Normal file
344
Docker部署说明.md
Normal file
@ -0,0 +1,344 @@
|
||||
# 🐳 Docker 部署说明
|
||||
|
||||
## 📋 部署概述
|
||||
|
||||
本项目支持完整的Docker容器化部署,包含所有必要的依赖和配置。支持单容器部署和多容器编排部署。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 方式一:使用 Docker Compose(推荐)
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd xianyuapis
|
||||
```
|
||||
|
||||
2. **配置环境变量**
|
||||
```bash
|
||||
# 复制环境变量模板
|
||||
cp .env.example .env
|
||||
|
||||
# 编辑配置文件(可选)
|
||||
nano .env
|
||||
```
|
||||
|
||||
3. **启动服务**
|
||||
```bash
|
||||
# 启动基础服务
|
||||
docker-compose up -d
|
||||
|
||||
# 或者启动包含Nginx的完整服务
|
||||
docker-compose --profile with-nginx up -d
|
||||
```
|
||||
|
||||
4. **访问系统**
|
||||
- 基础部署:http://localhost:8080
|
||||
- 带Nginx:http://localhost
|
||||
|
||||
### 方式二:使用 Docker 命令
|
||||
|
||||
1. **构建镜像**
|
||||
```bash
|
||||
docker build -t xianyu-auto-reply:latest .
|
||||
```
|
||||
|
||||
2. **运行容器**
|
||||
```bash
|
||||
docker run -d \
|
||||
--name xianyu-auto-reply \
|
||||
-p 8080:8080 \
|
||||
-v $(pwd)/data:/app/data \
|
||||
-v $(pwd)/logs:/app/logs \
|
||||
-v $(pwd)/global_config.yml:/app/global_config.yml:ro \
|
||||
-e ADMIN_USERNAME=admin \
|
||||
-e ADMIN_PASSWORD=admin123 \
|
||||
xianyu-auto-reply:latest
|
||||
```
|
||||
|
||||
## 📦 依赖说明
|
||||
|
||||
### 新增依赖
|
||||
- `python-multipart>=0.0.6` - 文件上传支持(商品管理功能需要)
|
||||
|
||||
### 完整依赖列表
|
||||
```
|
||||
# Web框架和API相关
|
||||
fastapi>=0.111
|
||||
uvicorn[standard]>=0.29
|
||||
pydantic>=2.7
|
||||
python-multipart>=0.0.6
|
||||
|
||||
# 日志记录
|
||||
loguru>=0.7
|
||||
|
||||
# 网络通信
|
||||
websockets>=10.0,<13.0
|
||||
aiohttp>=3.9
|
||||
|
||||
# 配置文件处理
|
||||
PyYAML>=6.0
|
||||
|
||||
# JavaScript执行引擎
|
||||
PyExecJS>=1.5.1
|
||||
|
||||
# 协议缓冲区解析
|
||||
blackboxprotobuf>=1.0.1
|
||||
|
||||
# 系统监控
|
||||
psutil>=5.9.0
|
||||
|
||||
# HTTP客户端(用于测试)
|
||||
requests>=2.31.0
|
||||
```
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
#### 基础配置
|
||||
```bash
|
||||
# 时区设置
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
# 服务端口
|
||||
WEB_PORT=8080
|
||||
|
||||
# 管理员账号(建议修改)
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=admin123
|
||||
|
||||
# JWT密钥(建议修改)
|
||||
JWT_SECRET_KEY=your-secret-key-here
|
||||
```
|
||||
|
||||
#### 功能配置
|
||||
```bash
|
||||
# 自动回复
|
||||
AUTO_REPLY_ENABLED=true
|
||||
|
||||
# 自动发货
|
||||
AUTO_DELIVERY_ENABLED=true
|
||||
AUTO_DELIVERY_TIMEOUT=30
|
||||
|
||||
# 商品管理(新功能)
|
||||
ENABLE_ITEM_MANAGEMENT=true
|
||||
```
|
||||
|
||||
### 数据持久化
|
||||
|
||||
#### 重要目录
|
||||
- `/app/data` - 数据库文件
|
||||
- `/app/logs` - 日志文件
|
||||
- `/app/backups` - 备份文件
|
||||
|
||||
#### 挂载配置
|
||||
```yaml
|
||||
volumes:
|
||||
- ./data:/app/data:rw # 数据库持久化
|
||||
- ./logs:/app/logs:rw # 日志持久化
|
||||
- ./backups:/app/backups:rw # 备份持久化
|
||||
- ./global_config.yml:/app/global_config.yml:ro # 配置文件
|
||||
```
|
||||
|
||||
## 🏗️ 架构说明
|
||||
|
||||
### 容器架构
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Nginx (可选) │
|
||||
│ 反向代理 + SSL │
|
||||
└─────────────┬───────────────────────┘
|
||||
│
|
||||
┌─────────────▼───────────────────────┐
|
||||
│ Xianyu App Container │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ FastAPI Server │ │
|
||||
│ │ (Port 8080) │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ XianyuAutoAsync │ │
|
||||
│ │ (WebSocket Client) │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ SQLite Database │ │
|
||||
│ │ (商品信息 + 配置) │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 新功能支持
|
||||
- ✅ 商品信息管理
|
||||
- ✅ 商品详情编辑
|
||||
- ✅ 文件上传功能
|
||||
- ✅ 消息通知格式化
|
||||
|
||||
## 🔍 健康检查
|
||||
|
||||
### 内置健康检查
|
||||
```bash
|
||||
# 检查容器状态
|
||||
docker ps
|
||||
|
||||
# 查看健康状态
|
||||
docker inspect xianyu-auto-reply | grep Health -A 10
|
||||
|
||||
# 手动健康检查
|
||||
curl -f http://localhost:8080/health
|
||||
```
|
||||
|
||||
### 健康检查端点
|
||||
- `GET /health` - 基础健康检查
|
||||
- `GET /api/status` - 详细状态信息
|
||||
|
||||
## 📊 监控和日志
|
||||
|
||||
### 日志查看
|
||||
```bash
|
||||
# 查看容器日志
|
||||
docker logs xianyu-auto-reply
|
||||
|
||||
# 实时查看日志
|
||||
docker logs -f xianyu-auto-reply
|
||||
|
||||
# 查看应用日志文件
|
||||
docker exec xianyu-auto-reply tail -f /app/logs/xianyu_$(date +%Y%m%d).log
|
||||
```
|
||||
|
||||
### 性能监控
|
||||
```bash
|
||||
# 查看资源使用
|
||||
docker stats xianyu-auto-reply
|
||||
|
||||
# 进入容器
|
||||
docker exec -it xianyu-auto-reply bash
|
||||
|
||||
# 查看进程状态
|
||||
docker exec xianyu-auto-reply ps aux
|
||||
```
|
||||
|
||||
## 🔒 安全配置
|
||||
|
||||
### 生产环境建议
|
||||
1. **修改默认密码**
|
||||
```bash
|
||||
ADMIN_USERNAME=your-admin
|
||||
ADMIN_PASSWORD=your-strong-password
|
||||
```
|
||||
|
||||
2. **使用强JWT密钥**
|
||||
```bash
|
||||
JWT_SECRET_KEY=$(openssl rand -base64 32)
|
||||
```
|
||||
|
||||
3. **启用HTTPS**
|
||||
```yaml
|
||||
# 使用Nginx配置SSL
|
||||
docker-compose --profile with-nginx up -d
|
||||
```
|
||||
|
||||
4. **限制网络访问**
|
||||
```yaml
|
||||
# 仅允许本地访问
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
```
|
||||
|
||||
## 🚨 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **容器启动失败**
|
||||
```bash
|
||||
# 查看详细错误
|
||||
docker logs xianyu-auto-reply
|
||||
|
||||
# 检查端口占用
|
||||
netstat -tlnp | grep 8080
|
||||
```
|
||||
|
||||
2. **数据库初始化失败**
|
||||
```bash
|
||||
# 数据库会在应用启动时自动初始化
|
||||
# 如果需要重新初始化,可以删除数据库文件后重启容器
|
||||
docker exec xianyu-auto-reply rm -f /app/data/xianyu_data.db
|
||||
docker restart xianyu-auto-reply
|
||||
```
|
||||
|
||||
3. **权限问题**
|
||||
```bash
|
||||
# 修复目录权限
|
||||
sudo chown -R 1000:1000 ./data ./logs ./backups
|
||||
```
|
||||
|
||||
4. **依赖安装失败**
|
||||
```bash
|
||||
# 重新构建镜像
|
||||
docker-compose build --no-cache
|
||||
```
|
||||
|
||||
### 调试模式
|
||||
```bash
|
||||
# 启用调试模式
|
||||
docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d
|
||||
|
||||
# 或设置环境变量
|
||||
docker run -e DEBUG=true -e LOG_LEVEL=DEBUG ...
|
||||
```
|
||||
|
||||
## 🔄 更新部署
|
||||
|
||||
### 更新步骤
|
||||
1. **停止服务**
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
2. **拉取最新代码**
|
||||
```bash
|
||||
git pull origin main
|
||||
```
|
||||
|
||||
3. **重新构建**
|
||||
```bash
|
||||
docker-compose build --no-cache
|
||||
```
|
||||
|
||||
4. **启动服务**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 数据备份
|
||||
```bash
|
||||
# 备份数据库
|
||||
docker exec xianyu-auto-reply cp /app/data/xianyu_data.db /app/backups/
|
||||
|
||||
# 备份配置
|
||||
cp .env .env.backup
|
||||
cp global_config.yml global_config.yml.backup
|
||||
```
|
||||
|
||||
## 📈 性能优化
|
||||
|
||||
### 资源限制
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M # 内存限制
|
||||
cpus: '0.5' # CPU限制
|
||||
reservations:
|
||||
memory: 256M # 内存预留
|
||||
cpus: '0.25' # CPU预留
|
||||
```
|
||||
|
||||
### 优化建议
|
||||
1. **调整内存限制**:根据实际使用情况调整
|
||||
2. **使用SSD存储**:提高数据库性能
|
||||
3. **配置日志轮转**:避免日志文件过大
|
||||
4. **定期清理**:清理旧的备份文件
|
||||
|
||||
---
|
||||
|
||||
🎉 **Docker部署配置完善,支持所有新功能!**
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 肥极喵
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
173
README.md
Normal file
173
README.md
Normal file
@ -0,0 +1,173 @@
|
||||
# 🐟 XianYuAutoDeliveryX - 闲鱼虚拟商品商自动发货&聊天对接大模型
|
||||
|
||||
[](https://www.python.org/)
|
||||
[](LICENSE)
|
||||
|
||||
**✨ 基于闲鱼API的自动发货系统,支持虚拟商品商品聊天窗口自动发货、消息自动回复等功能。**
|
||||
**⚠️ 注意:本项目仅供学习交流使用,请勿用于商业用途。**
|
||||
|
||||
## 🌟 核心特性
|
||||
|
||||
- 🔐 **用户认证系统** - 安全的登录认证,保护管理界面
|
||||
- 👥 **多账号管理** - 支持同时管理多个闲鱼账号
|
||||
- 🎯 **智能关键词回复** - 每个账号独立的关键词回复设置
|
||||
- 💾 **数据持久化** - SQLite数据库存储账号和关键词数据
|
||||
- 🌐 **美观Web界面** - 响应式设计,操作简单直观
|
||||
- 📡 **API接口** - 完整的RESTful API支持
|
||||
- 🔄 **实时消息处理** - 基于WebSocket的实时消息监控
|
||||
- 📊 **订单状态监控** - 实时跟踪订单状态变化
|
||||
- 📝 **完善的日志系统** - 详细的操作日志记录
|
||||
|
||||
## 🛠️ 快速开始
|
||||
|
||||
### ⛳ 运行环境
|
||||
- Python 3.7+
|
||||
|
||||
### 🎯 安装依赖
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 🎨 配置说明
|
||||
1. 在 `global_config.yml` 中配置基本参数
|
||||
2. 系统支持多账号管理,可通过Web界面添加多个闲鱼账号Cookie
|
||||
|
||||
### 🚀 运行项目
|
||||
```bash
|
||||
python Start.py
|
||||
```
|
||||
|
||||
### 🔐 登录系统
|
||||
1. 启动后访问 `http://localhost:8080`
|
||||
2. 默认登录账号:
|
||||
- 用户名:`admin`
|
||||
- 密码:`admin123`
|
||||
3. 登录后可进入管理界面进行操作
|
||||
|
||||
## 📁 项目结构
|
||||
```
|
||||
├── Start.py # 项目启动入口
|
||||
├── XianyuAutoAsync.py # 核心业务逻辑
|
||||
├── config.py # 配置管理
|
||||
├── cookie_manager.py # Cookie管理器
|
||||
├── db_manager.py # 数据库管理
|
||||
├── reply_server.py # FastAPI服务器
|
||||
├── utils/ # 工具函数目录
|
||||
│ ├── xianyu_utils.py # 闲鱼相关工具
|
||||
│ ├── message_utils.py # 消息处理工具
|
||||
│ └── ws_utils.py # WebSocket工具
|
||||
├── static/ # 静态资源
|
||||
│ ├── index.html # 管理界面
|
||||
│ └── login.html # 登录页面
|
||||
├── logs/ # 日志文件
|
||||
├── global_config.yml # 全局配置文件
|
||||
├── xianyu_data.db # SQLite数据库
|
||||
└── requirements.txt # Python依赖
|
||||
```
|
||||
|
||||
## 🎯 主要功能
|
||||
|
||||
### 1. 用户认证系统
|
||||
- 安全的登录认证机制
|
||||
- Session token管理
|
||||
- 自动登录状态检查
|
||||
- 登出功能
|
||||
|
||||
### 2. 多账号管理
|
||||
- 支持添加多个闲鱼账号
|
||||
- 每个账号独立管理
|
||||
- Cookie安全存储
|
||||
- 账号状态监控
|
||||
|
||||
### 3. 智能关键词回复
|
||||
- 每个账号独立的关键词设置
|
||||
- 支持变量替换:`{send_user_name}`, `{send_user_id}`, `{send_message}`
|
||||
- 实时关键词匹配
|
||||
- 默认回复机制
|
||||
|
||||
### 4. Web管理界面
|
||||
- 响应式设计,支持移动端
|
||||
- 直观的操作界面
|
||||
- 实时数据更新
|
||||
- 操作反馈提示
|
||||
|
||||
## 🔌 API 接口说明
|
||||
|
||||
### 智能回复接口
|
||||
`POST http://localhost:8080/xianyu/reply`
|
||||
|
||||
#### 接口说明
|
||||
你需要实现这个接口,本项目会调用这个接口获取自动回复的内容并发送给客户
|
||||
不实现这个接口也没关系,系统会默认回复,你也可以配置默认回复的内容
|
||||
用于处理闲鱼消息的自动回复,支持对接大语言模型进行智能回复。
|
||||
|
||||
**通过这个接口可以检测到用户是否已付款,然后回复虚拟资料内容即可**
|
||||
#### 请求参数
|
||||
```json
|
||||
{
|
||||
"msg_time": "消息时间",
|
||||
"user_url": "用户主页URL",
|
||||
"send_user_id": "发送者ID",
|
||||
"send_user_name": "发送者昵称",
|
||||
"item_id": "商品ID",
|
||||
"send_message": "发送的消息内容",
|
||||
"chat_id": "会话ID"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应格式
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"send_msg": "回复的消息内容"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 配置示例
|
||||
```yaml
|
||||
AUTO_REPLY:
|
||||
api:
|
||||
enabled: true # 是否启用API回复
|
||||
timeout: 10 # 超时时间(秒)
|
||||
url: http://localhost:8080/xianyu/reply
|
||||
```
|
||||
|
||||
#### 使用场景
|
||||
- 当收到买家消息时,系统会自动调用此接口
|
||||
- 支持接入 ChatGPT、文心一言等大语言模型
|
||||
- 支持自定义回复规则和模板
|
||||
- 支持消息变量替换(如 `{send_user_name}`)
|
||||
|
||||
#### 注意事项
|
||||
- 接口需要返回正确的状态码(200)和消息内容
|
||||
- 建议实现错误重试机制
|
||||
- 注意处理超时情况(默认10秒)
|
||||
- 可以根据需要扩展更多的参数和功能
|
||||
|
||||
## 🗝️ 注意事项
|
||||
- 请确保闲鱼账号已登录并获取有效的 Cookie
|
||||
- 建议在正式环境使用前先在测试环境验证
|
||||
- 定期检查日志文件,及时处理异常情况
|
||||
- 使用大模型时注意 API 调用频率和成本控制
|
||||
|
||||
## 📝 效果
|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 🧸特别鸣谢
|
||||
|
||||
本项目参考了以下开源项目: https://github.com/cv-cat/XianYuApis
|
||||
|
||||
感谢[@CVcat](https://github.com/cv-cat)的技术支持
|
||||
|
||||
## 📞 联系方式
|
||||
如有问题或建议,欢迎提交 Issue 或 Pull Request。
|
||||
|
||||
## 技术交流
|
||||
|
||||

|
125
Start.py
Normal file
125
Start.py
Normal file
@ -0,0 +1,125 @@
|
||||
"""项目启动入口:
|
||||
|
||||
1. 创建 CookieManager,按配置文件 / 环境变量初始化账号任务
|
||||
2. 在后台线程启动 FastAPI (reply_server) 提供管理与自动回复接口
|
||||
3. 主协程保持运行
|
||||
"""
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
import threading
|
||||
import uvicorn
|
||||
from urllib.parse import urlparse
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
|
||||
from config import AUTO_REPLY, COOKIES_LIST
|
||||
import cookie_manager as cm
|
||||
from db_manager import db_manager
|
||||
from file_log_collector import setup_file_logging
|
||||
|
||||
|
||||
def _start_api_server():
|
||||
"""后台线程启动 FastAPI 服务"""
|
||||
api_conf = AUTO_REPLY.get('api', {})
|
||||
|
||||
# 优先使用环境变量配置
|
||||
host = os.getenv('API_HOST', '0.0.0.0') # 默认绑定所有接口
|
||||
port = int(os.getenv('API_PORT', '8080')) # 默认端口8080
|
||||
|
||||
# 如果配置文件中有特定配置,则使用配置文件
|
||||
if 'host' in api_conf:
|
||||
host = api_conf['host']
|
||||
if 'port' in api_conf:
|
||||
port = api_conf['port']
|
||||
|
||||
# 兼容旧的URL配置方式
|
||||
if 'url' in api_conf and 'host' not in api_conf and 'port' not in api_conf:
|
||||
url = api_conf.get('url', 'http://0.0.0.0:8080/xianyu/reply')
|
||||
parsed = urlparse(url)
|
||||
if parsed.hostname and parsed.hostname != 'localhost':
|
||||
host = parsed.hostname
|
||||
port = parsed.port or 8080
|
||||
|
||||
logger.info(f"启动Web服务器: http://{host}:{port}")
|
||||
uvicorn.run("reply_server:app", host=host, port=port, log_level="info")
|
||||
|
||||
|
||||
def load_keywords_file(path: str):
|
||||
"""从文件读取关键字 -> [(keyword, reply)]"""
|
||||
kw_list = []
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
return kw_list
|
||||
with p.open('r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '\t' in line:
|
||||
k, r = line.split('\t', 1)
|
||||
elif ' ' in line:
|
||||
k, r = line.split(' ', 1)
|
||||
elif ':' in line:
|
||||
k, r = line.split(':', 1)
|
||||
else:
|
||||
continue
|
||||
kw_list.append((k.strip(), r.strip()))
|
||||
return kw_list
|
||||
|
||||
|
||||
async def main():
|
||||
print("开始启动主程序...")
|
||||
|
||||
# 初始化文件日志收集器
|
||||
print("初始化文件日志收集器...")
|
||||
setup_file_logging()
|
||||
logger.info("文件日志收集器已启动,开始收集实时日志")
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# 创建 CookieManager 并在全局暴露
|
||||
print("创建 CookieManager...")
|
||||
cm.manager = cm.CookieManager(loop)
|
||||
manager = cm.manager
|
||||
print("CookieManager 创建完成")
|
||||
|
||||
# 1) 从数据库加载的 Cookie 已经在 CookieManager 初始化时完成
|
||||
# 为每个 Cookie 启动任务
|
||||
for cid, val in manager.cookies.items():
|
||||
try:
|
||||
await manager._add_cookie_async(cid, val)
|
||||
logger.info(f"启动数据库中的 Cookie 任务: {cid}")
|
||||
except Exception as e:
|
||||
logger.error(f"启动 Cookie 任务失败: {cid}, {e}")
|
||||
|
||||
# 2) 如果配置文件中有新的 Cookie,也加载它们
|
||||
for entry in COOKIES_LIST:
|
||||
cid = entry.get('id')
|
||||
val = entry.get('value')
|
||||
if not cid or not val or cid in manager.cookies:
|
||||
continue
|
||||
|
||||
kw_file = entry.get('keywords_file')
|
||||
kw_list = load_keywords_file(kw_file) if kw_file else None
|
||||
manager.add_cookie(cid, val, kw_list)
|
||||
logger.info(f"从配置文件加载 Cookie: {cid}")
|
||||
|
||||
# 3) 若老环境变量仍提供单账号 Cookie,则作为 default 账号
|
||||
env_cookie = os.getenv('COOKIES_STR')
|
||||
if env_cookie and 'default' not in manager.list_cookies():
|
||||
manager.add_cookie('default', env_cookie)
|
||||
logger.info("从环境变量加载 default Cookie")
|
||||
|
||||
# 启动 API 服务线程
|
||||
print("启动 API 服务线程...")
|
||||
threading.Thread(target=_start_api_server, daemon=True).start()
|
||||
print("API 服务线程已启动")
|
||||
|
||||
# 阻塞保持运行
|
||||
print("主程序启动完成,保持运行...")
|
||||
await asyncio.Event().wait()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
92
UI_IMPROVEMENTS.md
Normal file
92
UI_IMPROVEMENTS.md
Normal file
@ -0,0 +1,92 @@
|
||||
# 🎨 界面优化总结
|
||||
|
||||
## ✨ 主要改进
|
||||
|
||||
### 1. 🎯 Cookie显示优化
|
||||
- **❌ 修改前**: Cookie值被隐藏为星号 (`****`)
|
||||
- **✅ 修改后**: 显示完整的Cookie内容,便于查看和调试
|
||||
|
||||
### 2. 🎨 视觉设计升级
|
||||
- **现代化配色方案**: 使用更现代的紫色主题 (`#4f46e5`)
|
||||
- **渐变背景**: 美丽的渐变背景和卡片效果
|
||||
- **毛玻璃效果**: 卡片使用 `backdrop-filter: blur()` 实现毛玻璃效果
|
||||
- **阴影和动画**: 悬停时的阴影和位移动画效果
|
||||
|
||||
### 3. 🔧 功能增强
|
||||
- **一键复制Cookie**: 点击Cookie值或复制按钮即可复制到剪贴板
|
||||
- **改进的按钮组**: 更紧凑的按钮布局,包含复制功能
|
||||
- **更好的空状态**: 当没有账号时显示更友好的提示
|
||||
|
||||
### 4. 📱 响应式设计
|
||||
- **移动端优化**: 在小屏幕上按钮垂直排列
|
||||
- **自适应布局**: 表格和卡片在不同屏幕尺寸下的自适应
|
||||
|
||||
## 🛠️ 技术改进
|
||||
|
||||
### 新增API接口
|
||||
```javascript
|
||||
GET /cookies/details
|
||||
```
|
||||
返回包含Cookie ID和完整值的详细信息,而不仅仅是ID列表。
|
||||
|
||||
### CSS样式优化
|
||||
- **CSS变量**: 统一的颜色管理
|
||||
- **现代字体**: 使用 Inter 字体提升可读性
|
||||
- **代码字体**: Cookie值使用等宽字体 (JetBrains Mono)
|
||||
- **流畅动画**: 所有交互都有平滑的过渡效果
|
||||
|
||||
### JavaScript功能增强
|
||||
- **复制功能**: 支持现代浏览器的 Clipboard API
|
||||
- **降级方案**: 对于不支持的浏览器提供传统复制方法
|
||||
- **用户反馈**: 复制成功/失败的Toast提示
|
||||
|
||||
## 🎯 用户体验提升
|
||||
|
||||
### 1. Cookie管理
|
||||
- **完整显示**: 不再隐藏Cookie内容,便于调试
|
||||
- **一键复制**: 快速复制Cookie值到剪贴板
|
||||
- **格式化显示**: 使用等宽字体和适当的行高
|
||||
|
||||
### 2. 视觉反馈
|
||||
- **悬停效果**: 所有可交互元素都有悬停反馈
|
||||
- **状态指示**: 清晰的按钮状态和颜色区分
|
||||
- **加载动画**: 优雅的加载状态显示
|
||||
|
||||
### 3. 操作便利性
|
||||
- **按钮分组**: 相关操作按钮紧凑排列
|
||||
- **图标提示**: 每个按钮都有清晰的图标和提示
|
||||
- **确认对话框**: 危险操作有确认提示
|
||||
|
||||
## 📊 界面对比
|
||||
|
||||
| 功能 | 修改前 | 修改后 |
|
||||
|------|--------|--------|
|
||||
| Cookie显示 | 隐藏为星号 | 完整显示 |
|
||||
| 复制功能 | 无 | 一键复制 |
|
||||
| 视觉效果 | 基础Bootstrap | 现代化渐变设计 |
|
||||
| 响应式 | 基本支持 | 完全优化 |
|
||||
| 用户反馈 | 基础提示 | 丰富的Toast反馈 |
|
||||
|
||||
## 🚀 使用说明
|
||||
|
||||
### 访问界面
|
||||
1. 启动系统: `python Start.py`
|
||||
2. 打开浏览器: `http://localhost:8080`
|
||||
3. 登录: `admin` / `admin123`
|
||||
|
||||
### 主要功能
|
||||
- **添加账号**: 在顶部表单中输入账号ID和Cookie值
|
||||
- **查看Cookie**: 完整的Cookie值显示在表格中
|
||||
- **复制Cookie**: 点击Cookie值或复制按钮
|
||||
- **管理关键词**: 点击关键词按钮设置自动回复
|
||||
- **删除账号**: 点击删除按钮(有确认提示)
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
这次界面优化大幅提升了用户体验:
|
||||
- ✅ **Cookie不再隐藏**,便于查看和调试
|
||||
- ✅ **现代化设计**,视觉效果更佳
|
||||
- ✅ **功能增强**,操作更便利
|
||||
- ✅ **响应式优化**,支持各种设备
|
||||
|
||||
界面现在更加美观、实用和用户友好!🎨✨
|
1955
XianyuAutoAsync.py
Normal file
1955
XianyuAutoAsync.py
Normal file
File diff suppressed because it is too large
Load Diff
121
config.py
Normal file
121
config.py
Normal file
@ -0,0 +1,121 @@
|
||||
import os
|
||||
import yaml
|
||||
from typing import Dict, Any
|
||||
|
||||
class Config:
|
||||
"""配置管理类
|
||||
|
||||
用于加载和管理全局配置文件(global_config.yml)。
|
||||
支持配置的读取、修改和保存。
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
_config = {}
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(Config, cls).__new__(cls)
|
||||
cls._instance._load_config()
|
||||
return cls._instance
|
||||
|
||||
def _load_config(self):
|
||||
"""加载配置文件
|
||||
|
||||
从global_config.yml文件中加载配置信息。
|
||||
如果文件不存在则抛出FileNotFoundError异常。
|
||||
"""
|
||||
config_path = os.path.join(os.path.dirname(__file__), 'global_config.yml')
|
||||
if not os.path.exists(config_path):
|
||||
raise FileNotFoundError(f"配置文件不存在: {config_path}")
|
||||
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
self._config = yaml.safe_load(f)
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""获取配置项
|
||||
|
||||
Args:
|
||||
key: 配置项的键,支持点号分隔的多级键
|
||||
default: 当配置项不存在时返回的默认值
|
||||
|
||||
Returns:
|
||||
配置项的值或默认值
|
||||
"""
|
||||
keys = key.split('.')
|
||||
value = self._config
|
||||
for k in keys:
|
||||
if isinstance(value, dict):
|
||||
value = value.get(k)
|
||||
else:
|
||||
return default
|
||||
if value is None:
|
||||
return default
|
||||
return value
|
||||
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
"""设置配置项
|
||||
|
||||
Args:
|
||||
key: 配置项的键,支持点号分隔的多级键
|
||||
value: 要设置的值
|
||||
"""
|
||||
keys = key.split('.')
|
||||
config = self._config
|
||||
for k in keys[:-1]:
|
||||
if k not in config:
|
||||
config[k] = {}
|
||||
config = config[k]
|
||||
config[keys[-1]] = value
|
||||
|
||||
def save(self) -> None:
|
||||
"""保存配置到文件
|
||||
|
||||
将当前配置保存回global_config.yml文件
|
||||
"""
|
||||
config_path = os.path.join(os.path.dirname(__file__), 'global_config.yml')
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.safe_dump(self._config, f, allow_unicode=True, default_flow_style=False)
|
||||
|
||||
@property
|
||||
def config(self) -> Dict[str, Any]:
|
||||
"""获取完整配置
|
||||
|
||||
Returns:
|
||||
包含所有配置项的字典
|
||||
"""
|
||||
return self._config
|
||||
|
||||
# 创建全局配置实例
|
||||
config = Config()
|
||||
|
||||
# 导出常用配置项
|
||||
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)
|
||||
MESSAGE_EXPIRE_TIME = config.get('MESSAGE_EXPIRE_TIME', 300000)
|
||||
API_ENDPOINTS = config.get('API_ENDPOINTS', {})
|
||||
DEFAULT_HEADERS = config.get('DEFAULT_HEADERS', {})
|
||||
WEBSOCKET_HEADERS = config.get('WEBSOCKET_HEADERS', {})
|
||||
APP_CONFIG = config.get('APP_CONFIG', {})
|
||||
AUTO_REPLY = config.get('AUTO_REPLY', {
|
||||
'enabled': True,
|
||||
'default_message': '亲爱的"{send_user_name}" 老板你好!所有宝贝都可以拍,秒发货的哈~不满意的话可以直接申请退款哈~',
|
||||
'api': {
|
||||
'enabled': False,
|
||||
'url': 'http://localhost:8080/xianyu/reply',
|
||||
'timeout': 10
|
||||
}
|
||||
})
|
||||
MANUAL_MODE = config.get('MANUAL_MODE', {})
|
||||
LOG_CONFIG = config.get('LOG_CONFIG', {})
|
||||
_cookies_raw = config.get('COOKIES', [])
|
||||
if isinstance(_cookies_raw, list):
|
||||
COOKIES_LIST = _cookies_raw
|
||||
else:
|
||||
# 兼容旧格式,仅有 value 字段
|
||||
val = _cookies_raw.get('value') if isinstance(_cookies_raw, dict) else None
|
||||
COOKIES_LIST = [{'id': 'default', 'value': val}] if val else []
|
157
cookie_manager.py
Normal file
157
cookie_manager.py
Normal file
@ -0,0 +1,157 @@
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
from loguru import logger
|
||||
from db_manager import db_manager
|
||||
|
||||
__all__ = ["CookieManager", "manager"]
|
||||
|
||||
|
||||
class CookieManager:
|
||||
"""管理多账号 Cookie 及其对应的 XianyuLive 任务和关键字"""
|
||||
|
||||
def __init__(self, loop: asyncio.AbstractEventLoop):
|
||||
self.loop = loop
|
||||
self.cookies: Dict[str, str] = {}
|
||||
self.tasks: Dict[str, asyncio.Task] = {}
|
||||
self.keywords: Dict[str, List[Tuple[str, str]]] = {}
|
||||
self.cookie_status: Dict[str, bool] = {} # 账号启用状态
|
||||
self._load_from_db()
|
||||
|
||||
def _load_from_db(self):
|
||||
"""从数据库加载所有Cookie、关键字和状态"""
|
||||
try:
|
||||
# 加载所有Cookie
|
||||
self.cookies = db_manager.get_all_cookies()
|
||||
# 加载所有关键字
|
||||
self.keywords = db_manager.get_all_keywords()
|
||||
# 加载所有Cookie状态(默认启用)
|
||||
self.cookie_status = db_manager.get_all_cookie_status()
|
||||
# 为没有状态记录的Cookie设置默认启用状态
|
||||
for cookie_id in self.cookies.keys():
|
||||
if cookie_id not in self.cookie_status:
|
||||
self.cookie_status[cookie_id] = True
|
||||
logger.info(f"从数据库加载了 {len(self.cookies)} 个Cookie、{len(self.keywords)} 组关键字和 {len(self.cookie_status)} 个状态记录")
|
||||
except Exception as e:
|
||||
logger.error(f"从数据库加载数据失败: {e}")
|
||||
|
||||
# ------------------------ 内部协程 ------------------------
|
||||
async def _run_xianyu(self, cookie_id: str, cookie_value: str):
|
||||
"""在事件循环中启动 XianyuLive.main"""
|
||||
from XianyuAutoAsync import XianyuLive # 延迟导入,避免循环
|
||||
try:
|
||||
live = XianyuLive(cookie_value, cookie_id=cookie_id)
|
||||
await live.main()
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"XianyuLive 任务已取消: {cookie_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"XianyuLive 任务异常({cookie_id}): {e}")
|
||||
|
||||
async def _add_cookie_async(self, cookie_id: str, cookie_value: str):
|
||||
if cookie_id in self.tasks:
|
||||
raise ValueError("Cookie ID already exists")
|
||||
self.cookies[cookie_id] = cookie_value
|
||||
# 保存到数据库
|
||||
db_manager.save_cookie(cookie_id, cookie_value)
|
||||
task = self.loop.create_task(self._run_xianyu(cookie_id, cookie_value))
|
||||
self.tasks[cookie_id] = task
|
||||
logger.info(f"已启动账号任务: {cookie_id}")
|
||||
|
||||
async def _remove_cookie_async(self, cookie_id: str):
|
||||
task = self.tasks.pop(cookie_id, None)
|
||||
if task:
|
||||
task.cancel()
|
||||
self.cookies.pop(cookie_id, None)
|
||||
self.keywords.pop(cookie_id, None)
|
||||
# 从数据库删除
|
||||
db_manager.delete_cookie(cookie_id)
|
||||
logger.info(f"已移除账号: {cookie_id}")
|
||||
|
||||
# ------------------------ 对外线程安全接口 ------------------------
|
||||
def add_cookie(self, cookie_id: str, cookie_value: str, kw_list: Optional[List[Tuple[str, str]]] = None):
|
||||
"""线程安全新增 Cookie 并启动任务"""
|
||||
if kw_list is not None:
|
||||
self.keywords[cookie_id] = kw_list
|
||||
else:
|
||||
self.keywords.setdefault(cookie_id, [])
|
||||
try:
|
||||
current_loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
current_loop = None
|
||||
|
||||
if current_loop and current_loop == self.loop:
|
||||
# 同一事件循环中,直接调度
|
||||
return self.loop.create_task(self._add_cookie_async(cookie_id, cookie_value))
|
||||
else:
|
||||
fut = asyncio.run_coroutine_threadsafe(self._add_cookie_async(cookie_id, cookie_value), self.loop)
|
||||
return fut.result()
|
||||
|
||||
def remove_cookie(self, cookie_id: str):
|
||||
try:
|
||||
current_loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
current_loop = None
|
||||
|
||||
if current_loop and current_loop == self.loop:
|
||||
return self.loop.create_task(self._remove_cookie_async(cookie_id))
|
||||
else:
|
||||
fut = asyncio.run_coroutine_threadsafe(self._remove_cookie_async(cookie_id), self.loop)
|
||||
return fut.result()
|
||||
|
||||
# 更新 Cookie 值
|
||||
def update_cookie(self, cookie_id: str, new_value: str):
|
||||
"""替换指定账号的 Cookie 并重启任务"""
|
||||
async def _update():
|
||||
# 先移除
|
||||
if cookie_id in self.tasks:
|
||||
await self._remove_cookie_async(cookie_id)
|
||||
# 再添加
|
||||
await self._add_cookie_async(cookie_id, new_value)
|
||||
|
||||
try:
|
||||
current_loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
current_loop = None
|
||||
|
||||
if current_loop and current_loop == self.loop:
|
||||
return self.loop.create_task(_update())
|
||||
else:
|
||||
fut = asyncio.run_coroutine_threadsafe(_update(), self.loop)
|
||||
return fut.result()
|
||||
|
||||
def update_keywords(self, cookie_id: str, kw_list: List[Tuple[str, str]]):
|
||||
"""线程安全更新关键字"""
|
||||
self.keywords[cookie_id] = kw_list
|
||||
# 保存到数据库
|
||||
db_manager.save_keywords(cookie_id, kw_list)
|
||||
logger.info(f"更新关键字: {cookie_id} -> {len(kw_list)} 条")
|
||||
|
||||
# 查询接口
|
||||
def list_cookies(self):
|
||||
return list(self.cookies.keys())
|
||||
|
||||
def get_keywords(self, cookie_id: str) -> List[Tuple[str, str]]:
|
||||
return self.keywords.get(cookie_id, [])
|
||||
|
||||
def update_cookie_status(self, cookie_id: str, enabled: bool):
|
||||
"""更新Cookie的启用/禁用状态"""
|
||||
if cookie_id not in self.cookies:
|
||||
raise ValueError(f"Cookie ID {cookie_id} 不存在")
|
||||
|
||||
self.cookie_status[cookie_id] = enabled
|
||||
# 保存到数据库
|
||||
db_manager.save_cookie_status(cookie_id, enabled)
|
||||
logger.info(f"更新Cookie状态: {cookie_id} -> {'启用' if enabled else '禁用'}")
|
||||
|
||||
def get_cookie_status(self, cookie_id: str) -> bool:
|
||||
"""获取Cookie的启用状态"""
|
||||
return self.cookie_status.get(cookie_id, True) # 默认启用
|
||||
|
||||
def get_enabled_cookies(self) -> Dict[str, str]:
|
||||
"""获取所有启用的Cookie"""
|
||||
return {cid: value for cid, value in self.cookies.items()
|
||||
if self.cookie_status.get(cid, True)}
|
||||
|
||||
|
||||
# 在 Start.py 中会把此变量赋值为具体实例
|
||||
manager: Optional[CookieManager] = None
|
BIN
data/xianyu_data.db
Normal file
BIN
data/xianyu_data.db
Normal file
Binary file not shown.
1823
db_manager.py
Normal file
1823
db_manager.py
Normal file
File diff suppressed because it is too large
Load Diff
298
deploy.bat
Normal file
298
deploy.bat
Normal file
@ -0,0 +1,298 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: 闲鱼自动回复系统 Docker 部署脚本 (Windows版本)
|
||||
:: 作者: Xianyu Auto Reply System
|
||||
:: 版本: 1.0.0
|
||||
|
||||
title 闲鱼自动回复系统 Docker 部署
|
||||
|
||||
:: 颜色定义
|
||||
set "RED=[91m"
|
||||
set "GREEN=[92m"
|
||||
set "YELLOW=[93m"
|
||||
set "BLUE=[94m"
|
||||
set "NC=[0m"
|
||||
|
||||
:: 打印带颜色的消息
|
||||
:print_info
|
||||
echo %BLUE%[INFO]%NC% %~1
|
||||
goto :eof
|
||||
|
||||
:print_success
|
||||
echo %GREEN%[SUCCESS]%NC% %~1
|
||||
goto :eof
|
||||
|
||||
:print_warning
|
||||
echo %YELLOW%[WARNING]%NC% %~1
|
||||
goto :eof
|
||||
|
||||
:print_error
|
||||
echo %RED%[ERROR]%NC% %~1
|
||||
goto :eof
|
||||
|
||||
:: 检查Docker是否安装
|
||||
:check_docker
|
||||
call :print_info "检查 Docker 环境..."
|
||||
|
||||
docker --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
call :print_error "Docker 未安装,请先安装 Docker Desktop"
|
||||
echo.
|
||||
echo 下载地址: https://www.docker.com/products/docker-desktop
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
docker-compose --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
call :print_error "Docker Compose 未安装,请先安装 Docker Compose"
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
call :print_success "Docker 环境检查通过"
|
||||
goto :eof
|
||||
|
||||
:: 创建必要的目录
|
||||
:create_directories
|
||||
call :print_info "创建必要的目录..."
|
||||
|
||||
if not exist "data" mkdir data
|
||||
if not exist "logs" mkdir logs
|
||||
if not exist "backups" mkdir backups
|
||||
if not exist "nginx" mkdir nginx
|
||||
if not exist "nginx\ssl" mkdir nginx\ssl
|
||||
|
||||
REM 检查目录是否创建成功
|
||||
if not exist "data" (
|
||||
call :print_error "data目录创建失败"
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if not exist "logs" (
|
||||
call :print_error "logs目录创建失败"
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
call :print_success "目录创建完成"
|
||||
goto :eof
|
||||
|
||||
:: 生成默认配置文件
|
||||
:generate_config
|
||||
REM 生成.env文件
|
||||
if not exist ".env" (
|
||||
if exist ".env.example" (
|
||||
call :print_info "从模板生成 .env 文件..."
|
||||
copy ".env.example" ".env" >nul
|
||||
call :print_success ".env 文件已生成"
|
||||
) else (
|
||||
call :print_warning ".env.example 文件不存在,跳过 .env 文件生成"
|
||||
)
|
||||
) else (
|
||||
call :print_info ".env 文件已存在,跳过生成"
|
||||
)
|
||||
|
||||
REM 生成global_config.yml文件
|
||||
if exist "global_config.yml" (
|
||||
call :print_info "配置文件已存在,跳过生成"
|
||||
goto :eof
|
||||
)
|
||||
|
||||
call :print_info "生成默认配置文件..."
|
||||
|
||||
(
|
||||
echo # 闲鱼自动回复系统配置文件
|
||||
echo API_ENDPOINTS:
|
||||
echo login_check: https://passport.goofish.com/newlogin/hasLogin.do
|
||||
echo message_headinfo: https://h5api.m.goofish.com/h5/mtop.idle.trade.pc.message.headinfo/1.0/
|
||||
echo token: https://h5api.m.goofish.com/h5/mtop.taobao.idlemessage.pc.login.token/1.0/
|
||||
echo.
|
||||
echo APP_CONFIG:
|
||||
echo api_version: '1.0'
|
||||
echo app_key: 444e9908a51d1cb236a27862abc769c9
|
||||
echo app_version: '1.0'
|
||||
echo platform: web
|
||||
echo.
|
||||
echo AUTO_REPLY:
|
||||
echo enabled: true
|
||||
echo default_message: '亲爱的"{send_user_name}" 老板你好!所有宝贝都可以拍,秒发货的哈~不满意的话可以直接申请退款哈~'
|
||||
echo max_retry: 3
|
||||
echo retry_interval: 5
|
||||
echo api:
|
||||
echo enabled: false
|
||||
echo host: 0.0.0.0 # 绑定所有网络接口,支持IP访问
|
||||
echo port: 8080 # Web服务端口
|
||||
echo url: http://0.0.0.0:8080/xianyu/reply
|
||||
echo timeout: 10
|
||||
echo.
|
||||
echo COOKIES:
|
||||
echo last_update_time: ''
|
||||
echo value: ''
|
||||
echo.
|
||||
echo DEFAULT_HEADERS:
|
||||
echo accept: application/json
|
||||
echo accept-language: zh-CN,zh;q=0.9
|
||||
echo cache-control: no-cache
|
||||
echo origin: https://www.goofish.com
|
||||
echo pragma: no-cache
|
||||
echo referer: https://www.goofish.com/
|
||||
echo user-agent: Mozilla/5.0 ^(Windows NT 10.0; Win64; x64^) AppleWebKit/537.36 ^(KHTML, like Gecko^) Chrome/119.0.0.0 Safari/537.36
|
||||
echo.
|
||||
echo WEBSOCKET_URL: wss://wss-goofish.dingtalk.com/
|
||||
echo HEARTBEAT_INTERVAL: 15
|
||||
echo HEARTBEAT_TIMEOUT: 5
|
||||
echo TOKEN_REFRESH_INTERVAL: 3600
|
||||
echo TOKEN_RETRY_INTERVAL: 300
|
||||
echo MESSAGE_EXPIRE_TIME: 300000
|
||||
echo.
|
||||
echo LOG_CONFIG:
|
||||
echo level: INFO
|
||||
echo format: '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level} | {name}:{function}:{line} - {message}'
|
||||
echo rotation: '1 day'
|
||||
echo retention: '7 days'
|
||||
) > global_config.yml
|
||||
|
||||
call :print_success "默认配置文件已生成"
|
||||
goto :eof
|
||||
|
||||
:: 构建Docker镜像
|
||||
:build_image
|
||||
call :print_info "构建 Docker 镜像..."
|
||||
|
||||
docker build -t xianyu-auto-reply:latest .
|
||||
if errorlevel 1 (
|
||||
call :print_error "Docker 镜像构建失败"
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
call :print_success "Docker 镜像构建完成"
|
||||
goto :eof
|
||||
|
||||
:: 启动服务
|
||||
:start_services
|
||||
call :print_info "启动服务..."
|
||||
|
||||
if "%~1"=="--with-nginx" (
|
||||
call :print_info "启动服务(包含 Nginx)..."
|
||||
docker-compose --profile with-nginx up -d
|
||||
) else (
|
||||
call :print_info "启动服务(不包含 Nginx)..."
|
||||
docker-compose up -d
|
||||
)
|
||||
|
||||
if errorlevel 1 (
|
||||
call :print_error "服务启动失败"
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
call :print_success "服务启动完成"
|
||||
goto :eof
|
||||
|
||||
:: 显示服务状态
|
||||
:show_status
|
||||
call :print_info "服务状态:"
|
||||
docker-compose ps
|
||||
|
||||
echo.
|
||||
call :print_info "服务日志(最近10行):"
|
||||
docker-compose logs --tail=10
|
||||
goto :eof
|
||||
|
||||
:: 显示访问信息
|
||||
:show_access_info
|
||||
call :print_success "部署完成!"
|
||||
echo.
|
||||
call :print_info "访问信息:"
|
||||
echo Web界面: http://localhost:8080
|
||||
echo 默认账号: admin
|
||||
echo 默认密码: admin123
|
||||
echo.
|
||||
call :print_info "常用命令:"
|
||||
echo 查看日志: docker-compose logs -f
|
||||
echo 重启服务: docker-compose restart
|
||||
echo 停止服务: docker-compose down
|
||||
echo 更新服务: deploy.bat update
|
||||
echo.
|
||||
call :print_info "数据目录:"
|
||||
echo 数据库: .\data\xianyu_data.db
|
||||
echo 日志: .\logs\
|
||||
echo 配置: .\global_config.yml
|
||||
echo.
|
||||
goto :eof
|
||||
|
||||
:: 更新服务
|
||||
:update_services
|
||||
call :print_info "更新服务..."
|
||||
|
||||
docker-compose down
|
||||
call :build_image
|
||||
call :start_services %~1
|
||||
|
||||
call :print_success "服务更新完成"
|
||||
goto :eof
|
||||
|
||||
:: 清理资源
|
||||
:cleanup
|
||||
call :print_warning "清理 Docker 资源..."
|
||||
|
||||
docker-compose down --volumes --remove-orphans
|
||||
docker rmi xianyu-auto-reply:latest 2>nul
|
||||
|
||||
call :print_success "清理完成"
|
||||
goto :eof
|
||||
|
||||
:: 显示帮助
|
||||
:show_help
|
||||
echo 使用方法:
|
||||
echo %~nx0 # 首次部署
|
||||
echo %~nx0 with-nginx # 部署并启动 Nginx
|
||||
echo %~nx0 update # 更新服务
|
||||
echo %~nx0 update with-nginx # 更新服务并启动 Nginx
|
||||
echo %~nx0 status # 查看服务状态
|
||||
echo %~nx0 cleanup # 清理所有资源
|
||||
echo %~nx0 help # 显示帮助
|
||||
goto :eof
|
||||
|
||||
:: 主函数
|
||||
:main
|
||||
echo ========================================
|
||||
echo 闲鱼自动回复系统 Docker 部署脚本
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
if "%~1"=="update" (
|
||||
call :print_info "更新模式"
|
||||
call :check_docker
|
||||
call :update_services %~2
|
||||
call :show_status
|
||||
call :show_access_info
|
||||
) else if "%~1"=="cleanup" (
|
||||
call :print_warning "清理模式"
|
||||
call :cleanup
|
||||
) else if "%~1"=="status" (
|
||||
call :show_status
|
||||
) else if "%~1"=="help" (
|
||||
call :show_help
|
||||
) else (
|
||||
call :print_info "首次部署模式"
|
||||
call :check_docker
|
||||
call :create_directories
|
||||
call :generate_config
|
||||
call :build_image
|
||||
call :start_services %~1
|
||||
call :show_status
|
||||
call :show_access_info
|
||||
)
|
||||
|
||||
echo.
|
||||
pause
|
||||
goto :eof
|
||||
|
||||
:: 执行主函数
|
||||
call :main %*
|
288
deploy.sh
Normal file
288
deploy.sh
Normal file
@ -0,0 +1,288 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 闲鱼自动回复系统 Docker 部署脚本
|
||||
# 作者: Xianyu Auto Reply System
|
||||
# 版本: 1.0.0
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 打印带颜色的消息
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 检查Docker是否安装
|
||||
check_docker() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
print_error "Docker 未安装,请先安装 Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
print_error "Docker Compose 未安装,请先安装 Docker Compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "Docker 环境检查通过"
|
||||
}
|
||||
|
||||
# 创建必要的目录
|
||||
create_directories() {
|
||||
print_info "创建必要的目录..."
|
||||
|
||||
# 创建目录
|
||||
mkdir -p data
|
||||
mkdir -p logs
|
||||
mkdir -p backups
|
||||
mkdir -p nginx/ssl
|
||||
|
||||
# 设置权限 (确保Docker容器可以写入)
|
||||
chmod 755 data logs backups
|
||||
|
||||
# 检查权限
|
||||
if [ ! -w "data" ]; then
|
||||
print_error "data目录没有写权限"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -w "logs" ]; then
|
||||
print_error "logs目录没有写权限"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "目录创建完成"
|
||||
}
|
||||
|
||||
# 生成默认配置文件
|
||||
generate_config() {
|
||||
# 生成.env文件
|
||||
if [ ! -f ".env" ]; then
|
||||
if [ -f ".env.example" ]; then
|
||||
print_info "从模板生成 .env 文件..."
|
||||
cp .env.example .env
|
||||
print_success ".env 文件已生成"
|
||||
else
|
||||
print_warning ".env.example 文件不存在,跳过 .env 文件生成"
|
||||
fi
|
||||
else
|
||||
print_info ".env 文件已存在,跳过生成"
|
||||
fi
|
||||
|
||||
# 生成global_config.yml文件
|
||||
if [ ! -f "global_config.yml" ]; then
|
||||
print_info "生成默认配置文件..."
|
||||
|
||||
cat > global_config.yml << EOF
|
||||
# 闲鱼自动回复系统配置文件
|
||||
API_ENDPOINTS:
|
||||
login_check: https://passport.goofish.com/newlogin/hasLogin.do
|
||||
message_headinfo: https://h5api.m.goofish.com/h5/mtop.idle.trade.pc.message.headinfo/1.0/
|
||||
token: https://h5api.m.goofish.com/h5/mtop.taobao.idlemessage.pc.login.token/1.0/
|
||||
|
||||
APP_CONFIG:
|
||||
api_version: '1.0'
|
||||
app_key: 444e9908a51d1cb236a27862abc769c9
|
||||
app_version: '1.0'
|
||||
platform: web
|
||||
|
||||
AUTO_REPLY:
|
||||
enabled: true
|
||||
default_message: '亲爱的"{send_user_name}" 老板你好!所有宝贝都可以拍,秒发货的哈~不满意的话可以直接申请退款哈~'
|
||||
max_retry: 3
|
||||
retry_interval: 5
|
||||
api:
|
||||
enabled: false
|
||||
host: 0.0.0.0 # 绑定所有网络接口,支持IP访问
|
||||
port: 8080 # Web服务端口
|
||||
url: http://0.0.0.0:8080/xianyu/reply
|
||||
timeout: 10
|
||||
|
||||
COOKIES:
|
||||
last_update_time: ''
|
||||
value: ''
|
||||
|
||||
DEFAULT_HEADERS:
|
||||
accept: application/json
|
||||
accept-language: zh-CN,zh;q=0.9
|
||||
cache-control: no-cache
|
||||
origin: https://www.goofish.com
|
||||
pragma: no-cache
|
||||
referer: https://www.goofish.com/
|
||||
sec-ch-ua: '"Google Chrome";v="119", "Chromium";v="119", "Not?A_Brand";v="24"'
|
||||
sec-ch-ua-mobile: '?0'
|
||||
sec-ch-ua-platform: '"Windows"'
|
||||
sec-fetch-dest: empty
|
||||
sec-fetch-mode: cors
|
||||
sec-fetch-site: same-site
|
||||
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36
|
||||
|
||||
WEBSOCKET_URL: wss://wss-goofish.dingtalk.com/
|
||||
HEARTBEAT_INTERVAL: 15
|
||||
HEARTBEAT_TIMEOUT: 5
|
||||
TOKEN_REFRESH_INTERVAL: 3600
|
||||
TOKEN_RETRY_INTERVAL: 300
|
||||
MESSAGE_EXPIRE_TIME: 300000
|
||||
|
||||
LOG_CONFIG:
|
||||
level: INFO
|
||||
format: '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level} | {name}:{function}:{line} - {message}'
|
||||
rotation: '1 day'
|
||||
retention: '7 days'
|
||||
EOF
|
||||
|
||||
print_success "默认配置文件已生成"
|
||||
else
|
||||
print_info "配置文件已存在,跳过生成"
|
||||
fi
|
||||
}
|
||||
|
||||
# 构建Docker镜像
|
||||
build_image() {
|
||||
print_info "构建 Docker 镜像..."
|
||||
|
||||
docker build -t xianyu-auto-reply:latest .
|
||||
|
||||
print_success "Docker 镜像构建完成"
|
||||
}
|
||||
|
||||
# 启动服务
|
||||
start_services() {
|
||||
print_info "启动服务..."
|
||||
|
||||
# 检查是否需要启动 Nginx
|
||||
if [ "$1" = "--with-nginx" ]; then
|
||||
print_info "启动服务(包含 Nginx)..."
|
||||
docker-compose --profile with-nginx up -d
|
||||
else
|
||||
print_info "启动服务(不包含 Nginx)..."
|
||||
docker-compose up -d
|
||||
fi
|
||||
|
||||
print_success "服务启动完成"
|
||||
}
|
||||
|
||||
# 显示服务状态
|
||||
show_status() {
|
||||
print_info "服务状态:"
|
||||
docker-compose ps
|
||||
|
||||
print_info "服务日志(最近10行):"
|
||||
docker-compose logs --tail=10
|
||||
}
|
||||
|
||||
# 显示访问信息
|
||||
show_access_info() {
|
||||
print_success "部署完成!"
|
||||
echo ""
|
||||
print_info "访问信息:"
|
||||
echo " Web界面: http://localhost:8080"
|
||||
echo " 默认账号: admin"
|
||||
echo " 默认密码: admin123"
|
||||
echo ""
|
||||
print_info "常用命令:"
|
||||
echo " 查看日志: docker-compose logs -f"
|
||||
echo " 重启服务: docker-compose restart"
|
||||
echo " 停止服务: docker-compose down"
|
||||
echo " 更新服务: ./deploy.sh --update"
|
||||
echo ""
|
||||
print_info "数据目录:"
|
||||
echo " 数据库: ./data/xianyu_data.db"
|
||||
echo " 日志: ./logs/"
|
||||
echo " 配置: ./global_config.yml"
|
||||
}
|
||||
|
||||
# 更新服务
|
||||
update_services() {
|
||||
print_info "更新服务..."
|
||||
|
||||
# 停止服务
|
||||
docker-compose down
|
||||
|
||||
# 重新构建镜像
|
||||
build_image
|
||||
|
||||
# 启动服务
|
||||
start_services $1
|
||||
|
||||
print_success "服务更新完成"
|
||||
}
|
||||
|
||||
# 清理资源
|
||||
cleanup() {
|
||||
print_warning "清理 Docker 资源..."
|
||||
|
||||
# 停止并删除容器
|
||||
docker-compose down --volumes --remove-orphans
|
||||
|
||||
# 删除镜像
|
||||
docker rmi xianyu-auto-reply:latest 2>/dev/null || true
|
||||
|
||||
print_success "清理完成"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
echo "========================================"
|
||||
echo " 闲鱼自动回复系统 Docker 部署脚本"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
case "$1" in
|
||||
--update)
|
||||
print_info "更新模式"
|
||||
check_docker
|
||||
update_services $2
|
||||
show_status
|
||||
show_access_info
|
||||
;;
|
||||
--cleanup)
|
||||
print_warning "清理模式"
|
||||
cleanup
|
||||
;;
|
||||
--status)
|
||||
show_status
|
||||
;;
|
||||
--help)
|
||||
echo "使用方法:"
|
||||
echo " $0 # 首次部署"
|
||||
echo " $0 --with-nginx # 部署并启动 Nginx"
|
||||
echo " $0 --update # 更新服务"
|
||||
echo " $0 --update --with-nginx # 更新服务并启动 Nginx"
|
||||
echo " $0 --status # 查看服务状态"
|
||||
echo " $0 --cleanup # 清理所有资源"
|
||||
echo " $0 --help # 显示帮助"
|
||||
;;
|
||||
*)
|
||||
print_info "首次部署模式"
|
||||
check_docker
|
||||
create_directories
|
||||
generate_config
|
||||
build_image
|
||||
start_services $1
|
||||
show_status
|
||||
show_access_info
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 执行主函数
|
||||
main "$@"
|
92
docker-compose.yml
Normal file
92
docker-compose.yml
Normal file
@ -0,0 +1,92 @@
|
||||
services:
|
||||
xianyu-app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: xianyu-auto-reply:latest
|
||||
container_name: xianyu-auto-reply
|
||||
restart: unless-stopped
|
||||
# 使用root用户避免权限问题
|
||||
user: "0:0"
|
||||
ports:
|
||||
- "${WEB_PORT:-8080}:8080"
|
||||
volumes:
|
||||
# 数据持久化
|
||||
- ./data:/app/data:rw
|
||||
- ./logs:/app/logs:rw
|
||||
- ./global_config.yml:/app/global_config.yml:ro
|
||||
# 可选:如果需要自定义配置
|
||||
# - ./custom_config.yml:/app/global_config.yml:ro
|
||||
# 可选:备份目录
|
||||
- ./backups:/app/backups:rw
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=${PYTHONUNBUFFERED:-1}
|
||||
- PYTHONDONTWRITEBYTECODE=${PYTHONDONTWRITEBYTECODE:-1}
|
||||
- TZ=${TZ:-Asia/Shanghai}
|
||||
- DB_PATH=${DB_PATH:-/app/data/xianyu_data.db}
|
||||
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||
- DEBUG=${DEBUG:-false}
|
||||
- RELOAD=${RELOAD:-false}
|
||||
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
|
||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin123}
|
||||
- JWT_SECRET_KEY=${JWT_SECRET_KEY:-default-secret-key}
|
||||
- SESSION_TIMEOUT=${SESSION_TIMEOUT:-3600}
|
||||
- AUTO_REPLY_ENABLED=${AUTO_REPLY_ENABLED:-true}
|
||||
- AUTO_DELIVERY_ENABLED=${AUTO_DELIVERY_ENABLED:-true}
|
||||
- AUTO_DELIVERY_TIMEOUT=${AUTO_DELIVERY_TIMEOUT:-30}
|
||||
- API_CARD_TIMEOUT=${API_CARD_TIMEOUT:-10}
|
||||
- BATCH_DATA_LOCK_TIMEOUT=${BATCH_DATA_LOCK_TIMEOUT:-5}
|
||||
- WEBSOCKET_URL=${WEBSOCKET_URL:-wss://wss-goofish.dingtalk.com/}
|
||||
- HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-15}
|
||||
- HEARTBEAT_TIMEOUT=${HEARTBEAT_TIMEOUT:-5}
|
||||
- TOKEN_REFRESH_INTERVAL=${TOKEN_REFRESH_INTERVAL:-3600}
|
||||
- TOKEN_RETRY_INTERVAL=${TOKEN_RETRY_INTERVAL:-300}
|
||||
- MESSAGE_EXPIRE_TIME=${MESSAGE_EXPIRE_TIME:-300000}
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
networks:
|
||||
- xianyu-network
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
# 资源限制
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: ${MEMORY_LIMIT:-512}M
|
||||
cpus: '${CPU_LIMIT:-0.5}'
|
||||
reservations:
|
||||
memory: ${MEMORY_RESERVATION:-256}M
|
||||
cpus: '${CPU_RESERVATION:-0.25}'
|
||||
|
||||
# 可选:添加Nginx反向代理
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: xianyu-nginx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
depends_on:
|
||||
- xianyu-app
|
||||
networks:
|
||||
- xianyu-network
|
||||
profiles:
|
||||
- with-nginx
|
||||
|
||||
networks:
|
||||
xianyu-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
xianyu-data:
|
||||
driver: local
|
||||
xianyu-logs:
|
||||
driver: local
|
301
docker-deploy.bat
Normal file
301
docker-deploy.bat
Normal file
@ -0,0 +1,301 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: 闲鱼自动回复系统 Docker 部署脚本 (Windows版本)
|
||||
:: 支持快速部署和管理
|
||||
|
||||
set PROJECT_NAME=xianyu-auto-reply
|
||||
set COMPOSE_FILE=docker-compose.yml
|
||||
set ENV_FILE=.env
|
||||
|
||||
:: 颜色定义 (Windows 10+ 支持ANSI颜色)
|
||||
set "RED=[31m"
|
||||
set "GREEN=[32m"
|
||||
set "YELLOW=[33m"
|
||||
set "BLUE=[34m"
|
||||
set "NC=[0m"
|
||||
|
||||
:: 打印带颜色的消息
|
||||
:print_info
|
||||
echo %BLUE%ℹ️ %~1%NC%
|
||||
goto :eof
|
||||
|
||||
:print_success
|
||||
echo %GREEN%✅ %~1%NC%
|
||||
goto :eof
|
||||
|
||||
:print_warning
|
||||
echo %YELLOW%⚠️ %~1%NC%
|
||||
goto :eof
|
||||
|
||||
:print_error
|
||||
echo %RED%❌ %~1%NC%
|
||||
goto :eof
|
||||
|
||||
:: 检查依赖
|
||||
:check_dependencies
|
||||
call :print_info "检查系统依赖..."
|
||||
|
||||
docker --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
call :print_error "Docker 未安装,请先安装 Docker Desktop"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
docker-compose --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
call :print_error "Docker Compose 未安装,请先安装 Docker Compose"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
call :print_success "系统依赖检查通过"
|
||||
goto :eof
|
||||
|
||||
:: 初始化配置
|
||||
:init_config
|
||||
call :print_info "初始化配置文件..."
|
||||
|
||||
if not exist "%ENV_FILE%" (
|
||||
if exist ".env.example" (
|
||||
copy ".env.example" "%ENV_FILE%" >nul
|
||||
call :print_success "已创建 %ENV_FILE% 配置文件"
|
||||
) else (
|
||||
call :print_error ".env.example 文件不存在"
|
||||
exit /b 1
|
||||
)
|
||||
) else (
|
||||
call :print_warning "%ENV_FILE% 已存在,跳过创建"
|
||||
)
|
||||
|
||||
:: 创建必要的目录
|
||||
if not exist "data" mkdir data
|
||||
if not exist "logs" mkdir logs
|
||||
if not exist "backups" mkdir backups
|
||||
call :print_success "已创建必要的目录"
|
||||
goto :eof
|
||||
|
||||
:: 构建镜像
|
||||
:build_image
|
||||
call :print_info "构建 Docker 镜像..."
|
||||
docker-compose build --no-cache
|
||||
if errorlevel 1 (
|
||||
call :print_error "镜像构建失败"
|
||||
exit /b 1
|
||||
)
|
||||
call :print_success "镜像构建完成"
|
||||
goto :eof
|
||||
|
||||
:: 启动服务
|
||||
:start_services
|
||||
set "profile="
|
||||
if "%~1"=="with-nginx" (
|
||||
set "profile=--profile with-nginx"
|
||||
call :print_info "启动服务(包含 Nginx)..."
|
||||
) else (
|
||||
call :print_info "启动基础服务..."
|
||||
)
|
||||
|
||||
docker-compose %profile% up -d
|
||||
if errorlevel 1 (
|
||||
call :print_error "服务启动失败"
|
||||
exit /b 1
|
||||
)
|
||||
call :print_success "服务启动完成"
|
||||
|
||||
:: 等待服务就绪
|
||||
call :print_info "等待服务就绪..."
|
||||
timeout /t 10 /nobreak >nul
|
||||
|
||||
:: 检查服务状态
|
||||
docker-compose ps | findstr "Up" >nul
|
||||
if errorlevel 1 (
|
||||
call :print_error "服务启动失败"
|
||||
docker-compose logs
|
||||
exit /b 1
|
||||
) else (
|
||||
call :print_success "服务运行正常"
|
||||
call :show_access_info "%~1"
|
||||
)
|
||||
goto :eof
|
||||
|
||||
:: 停止服务
|
||||
:stop_services
|
||||
call :print_info "停止服务..."
|
||||
docker-compose down
|
||||
call :print_success "服务已停止"
|
||||
goto :eof
|
||||
|
||||
:: 重启服务
|
||||
:restart_services
|
||||
call :print_info "重启服务..."
|
||||
docker-compose restart
|
||||
call :print_success "服务已重启"
|
||||
goto :eof
|
||||
|
||||
:: 查看日志
|
||||
:show_logs
|
||||
if "%~1"=="" (
|
||||
docker-compose logs -f
|
||||
) else (
|
||||
docker-compose logs -f "%~1"
|
||||
)
|
||||
goto :eof
|
||||
|
||||
:: 查看状态
|
||||
:show_status
|
||||
call :print_info "服务状态:"
|
||||
docker-compose ps
|
||||
|
||||
call :print_info "资源使用:"
|
||||
for /f "tokens=*" %%i in ('docker-compose ps -q') do (
|
||||
docker stats --no-stream %%i
|
||||
)
|
||||
goto :eof
|
||||
|
||||
:: 显示访问信息
|
||||
:show_access_info
|
||||
echo.
|
||||
call :print_success "🎉 部署完成!"
|
||||
echo.
|
||||
|
||||
if "%~1"=="with-nginx" (
|
||||
echo 📱 访问地址:
|
||||
echo HTTP: http://localhost
|
||||
echo HTTPS: https://localhost ^(如果配置了SSL^)
|
||||
) else (
|
||||
echo 📱 访问地址:
|
||||
echo HTTP: http://localhost:8080
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 🔐 默认登录信息:
|
||||
echo 用户名: admin
|
||||
echo 密码: admin123
|
||||
echo.
|
||||
echo 📊 管理命令:
|
||||
echo 查看状态: %~nx0 status
|
||||
echo 查看日志: %~nx0 logs
|
||||
echo 重启服务: %~nx0 restart
|
||||
echo 停止服务: %~nx0 stop
|
||||
echo.
|
||||
goto :eof
|
||||
|
||||
:: 健康检查
|
||||
:health_check
|
||||
call :print_info "执行健康检查..."
|
||||
|
||||
set "url=http://localhost:8080/health"
|
||||
set "max_attempts=30"
|
||||
set "attempt=1"
|
||||
|
||||
:health_loop
|
||||
curl -f -s "%url%" >nul 2>&1
|
||||
if not errorlevel 1 (
|
||||
call :print_success "健康检查通过"
|
||||
goto :eof
|
||||
)
|
||||
|
||||
call :print_info "等待服务就绪... (!attempt!/%max_attempts%)"
|
||||
timeout /t 2 /nobreak >nul
|
||||
set /a attempt+=1
|
||||
|
||||
if !attempt! leq %max_attempts% goto health_loop
|
||||
|
||||
call :print_error "健康检查失败"
|
||||
exit /b 1
|
||||
|
||||
:: 备份数据
|
||||
:backup_data
|
||||
call :print_info "备份数据..."
|
||||
|
||||
for /f "tokens=2 delims==" %%i in ('wmic OS Get localdatetime /value') do set datetime=%%i
|
||||
set backup_dir=backups\%datetime:~0,8%_%datetime:~8,6%
|
||||
mkdir "%backup_dir%" 2>nul
|
||||
|
||||
:: 备份数据库
|
||||
if exist "data\xianyu_data.db" (
|
||||
copy "data\xianyu_data.db" "%backup_dir%\" >nul
|
||||
call :print_success "数据库备份完成"
|
||||
)
|
||||
|
||||
:: 备份配置
|
||||
copy "%ENV_FILE%" "%backup_dir%\" >nul
|
||||
copy "global_config.yml" "%backup_dir%\" >nul 2>&1
|
||||
|
||||
call :print_success "数据备份完成: %backup_dir%"
|
||||
goto :eof
|
||||
|
||||
:: 显示帮助信息
|
||||
:show_help
|
||||
echo 闲鱼自动回复系统 Docker 部署脚本 ^(Windows版本^)
|
||||
echo.
|
||||
echo 用法: %~nx0 [命令] [选项]
|
||||
echo.
|
||||
echo 命令:
|
||||
echo init 初始化配置文件
|
||||
echo build 构建 Docker 镜像
|
||||
echo start [with-nginx] 启动服务^(可选包含 Nginx^)
|
||||
echo stop 停止服务
|
||||
echo restart 重启服务
|
||||
echo status 查看服务状态
|
||||
echo logs [service] 查看日志
|
||||
echo health 健康检查
|
||||
echo backup 备份数据
|
||||
echo help 显示帮助信息
|
||||
echo.
|
||||
echo 示例:
|
||||
echo %~nx0 init # 初始化配置
|
||||
echo %~nx0 start # 启动基础服务
|
||||
echo %~nx0 start with-nginx # 启动包含 Nginx 的服务
|
||||
echo %~nx0 logs xianyu-app # 查看应用日志
|
||||
echo.
|
||||
goto :eof
|
||||
|
||||
:: 主函数
|
||||
:main
|
||||
if "%~1"=="init" (
|
||||
call :check_dependencies
|
||||
call :init_config
|
||||
) else if "%~1"=="build" (
|
||||
call :check_dependencies
|
||||
call :build_image
|
||||
) else if "%~1"=="start" (
|
||||
call :check_dependencies
|
||||
call :init_config
|
||||
call :build_image
|
||||
call :start_services "%~2"
|
||||
) else if "%~1"=="stop" (
|
||||
call :stop_services
|
||||
) else if "%~1"=="restart" (
|
||||
call :restart_services
|
||||
) else if "%~1"=="status" (
|
||||
call :show_status
|
||||
) else if "%~1"=="logs" (
|
||||
call :show_logs "%~2"
|
||||
) else if "%~1"=="health" (
|
||||
call :health_check
|
||||
) else if "%~1"=="backup" (
|
||||
call :backup_data
|
||||
) else if "%~1"=="help" (
|
||||
call :show_help
|
||||
) else if "%~1"=="-h" (
|
||||
call :show_help
|
||||
) else if "%~1"=="--help" (
|
||||
call :show_help
|
||||
) else if "%~1"=="" (
|
||||
call :print_info "快速部署模式"
|
||||
call :check_dependencies
|
||||
call :init_config
|
||||
call :build_image
|
||||
call :start_services
|
||||
) else (
|
||||
call :print_error "未知命令: %~1"
|
||||
call :show_help
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
goto :eof
|
||||
|
||||
:: 执行主函数
|
||||
call :main %*
|
350
docker-deploy.sh
Normal file
350
docker-deploy.sh
Normal file
@ -0,0 +1,350 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 闲鱼自动回复系统 Docker 部署脚本
|
||||
# 支持快速部署和管理
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 项目配置
|
||||
PROJECT_NAME="xianyu-auto-reply"
|
||||
COMPOSE_FILE="docker-compose.yml"
|
||||
ENV_FILE=".env"
|
||||
|
||||
# 打印带颜色的消息
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
# 检查依赖
|
||||
check_dependencies() {
|
||||
print_info "检查系统依赖..."
|
||||
|
||||
if ! command -v docker &> /dev/null; then
|
||||
print_error "Docker 未安装,请先安装 Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
print_error "Docker Compose 未安装,请先安装 Docker Compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "系统依赖检查通过"
|
||||
}
|
||||
|
||||
# 初始化配置
|
||||
init_config() {
|
||||
print_info "初始化配置文件..."
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
if [ -f ".env.example" ]; then
|
||||
cp .env.example "$ENV_FILE"
|
||||
print_success "已创建 $ENV_FILE 配置文件"
|
||||
else
|
||||
print_error ".env.example 文件不存在"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_warning "$ENV_FILE 已存在,跳过创建"
|
||||
fi
|
||||
|
||||
# 创建必要的目录
|
||||
mkdir -p data logs backups
|
||||
print_success "已创建必要的目录"
|
||||
}
|
||||
|
||||
# 构建镜像
|
||||
build_image() {
|
||||
print_info "构建 Docker 镜像..."
|
||||
docker-compose build --no-cache
|
||||
print_success "镜像构建完成"
|
||||
}
|
||||
|
||||
# 启动服务
|
||||
start_services() {
|
||||
local profile=""
|
||||
if [ "$1" = "with-nginx" ]; then
|
||||
profile="--profile with-nginx"
|
||||
print_info "启动服务(包含 Nginx)..."
|
||||
else
|
||||
print_info "启动基础服务..."
|
||||
fi
|
||||
|
||||
docker-compose $profile up -d
|
||||
print_success "服务启动完成"
|
||||
|
||||
# 等待服务就绪
|
||||
print_info "等待服务就绪..."
|
||||
sleep 10
|
||||
|
||||
# 检查服务状态
|
||||
if docker-compose ps | grep -q "Up"; then
|
||||
print_success "服务运行正常"
|
||||
show_access_info "$1"
|
||||
else
|
||||
print_error "服务启动失败"
|
||||
docker-compose logs
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 停止服务
|
||||
stop_services() {
|
||||
print_info "停止服务..."
|
||||
docker-compose down
|
||||
print_success "服务已停止"
|
||||
}
|
||||
|
||||
# 重启服务
|
||||
restart_services() {
|
||||
print_info "重启服务..."
|
||||
docker-compose restart
|
||||
print_success "服务已重启"
|
||||
}
|
||||
|
||||
# 查看日志
|
||||
show_logs() {
|
||||
local service="$1"
|
||||
if [ -z "$service" ]; then
|
||||
docker-compose logs -f
|
||||
else
|
||||
docker-compose logs -f "$service"
|
||||
fi
|
||||
}
|
||||
|
||||
# 查看状态
|
||||
show_status() {
|
||||
print_info "服务状态:"
|
||||
docker-compose ps
|
||||
|
||||
print_info "资源使用:"
|
||||
docker stats --no-stream $(docker-compose ps -q)
|
||||
}
|
||||
|
||||
# 显示访问信息
|
||||
show_access_info() {
|
||||
local with_nginx="$1"
|
||||
|
||||
echo ""
|
||||
print_success "🎉 部署完成!"
|
||||
echo ""
|
||||
|
||||
if [ "$with_nginx" = "with-nginx" ]; then
|
||||
echo "📱 访问地址:"
|
||||
echo " HTTP: http://localhost"
|
||||
echo " HTTPS: https://localhost (如果配置了SSL)"
|
||||
else
|
||||
echo "📱 访问地址:"
|
||||
echo " HTTP: http://localhost:8080"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔐 默认登录信息:"
|
||||
echo " 用户名: admin"
|
||||
echo " 密码: admin123"
|
||||
echo ""
|
||||
echo "📊 管理命令:"
|
||||
echo " 查看状态: $0 status"
|
||||
echo " 查看日志: $0 logs"
|
||||
echo " 重启服务: $0 restart"
|
||||
echo " 停止服务: $0 stop"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
health_check() {
|
||||
print_info "执行健康检查..."
|
||||
|
||||
local url="http://localhost:8080/health"
|
||||
local max_attempts=30
|
||||
local attempt=1
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if curl -f -s "$url" > /dev/null 2>&1; then
|
||||
print_success "健康检查通过"
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_info "等待服务就绪... ($attempt/$max_attempts)"
|
||||
sleep 2
|
||||
((attempt++))
|
||||
done
|
||||
|
||||
print_error "健康检查失败"
|
||||
return 1
|
||||
}
|
||||
|
||||
# 备份数据
|
||||
backup_data() {
|
||||
print_info "备份数据..."
|
||||
|
||||
local backup_dir="backups/$(date +%Y%m%d_%H%M%S)"
|
||||
mkdir -p "$backup_dir"
|
||||
|
||||
# 备份数据库
|
||||
if [ -f "data/xianyu_data.db" ]; then
|
||||
cp data/xianyu_data.db "$backup_dir/"
|
||||
print_success "数据库备份完成"
|
||||
fi
|
||||
|
||||
# 备份配置
|
||||
cp "$ENV_FILE" "$backup_dir/"
|
||||
cp global_config.yml "$backup_dir/" 2>/dev/null || true
|
||||
|
||||
print_success "数据备份完成: $backup_dir"
|
||||
}
|
||||
|
||||
# 更新部署
|
||||
update_deployment() {
|
||||
print_info "更新部署..."
|
||||
|
||||
# 备份数据
|
||||
backup_data
|
||||
|
||||
# 停止服务
|
||||
stop_services
|
||||
|
||||
# 拉取最新代码(如果是git仓库)
|
||||
if [ -d ".git" ]; then
|
||||
print_info "拉取最新代码..."
|
||||
git pull
|
||||
fi
|
||||
|
||||
# 重新构建
|
||||
build_image
|
||||
|
||||
# 启动服务
|
||||
start_services
|
||||
|
||||
print_success "更新完成"
|
||||
}
|
||||
|
||||
# 清理环境
|
||||
cleanup() {
|
||||
print_warning "这将删除所有容器、镜像和数据,确定要继续吗?(y/N)"
|
||||
read -r response
|
||||
|
||||
if [[ "$response" =~ ^[Yy]$ ]]; then
|
||||
print_info "清理环境..."
|
||||
|
||||
# 停止并删除容器
|
||||
docker-compose down -v --rmi all
|
||||
|
||||
# 删除数据目录
|
||||
rm -rf data logs backups
|
||||
|
||||
print_success "环境清理完成"
|
||||
else
|
||||
print_info "取消清理操作"
|
||||
fi
|
||||
}
|
||||
|
||||
# 显示帮助信息
|
||||
show_help() {
|
||||
echo "闲鱼自动回复系统 Docker 部署脚本"
|
||||
echo ""
|
||||
echo "用法: $0 [命令] [选项]"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " init 初始化配置文件"
|
||||
echo " build 构建 Docker 镜像"
|
||||
echo " start [with-nginx] 启动服务(可选包含 Nginx)"
|
||||
echo " stop 停止服务"
|
||||
echo " restart 重启服务"
|
||||
echo " status 查看服务状态"
|
||||
echo " logs [service] 查看日志"
|
||||
echo " health 健康检查"
|
||||
echo " backup 备份数据"
|
||||
echo " update 更新部署"
|
||||
echo " cleanup 清理环境"
|
||||
echo " help 显示帮助信息"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 init # 初始化配置"
|
||||
echo " $0 start # 启动基础服务"
|
||||
echo " $0 start with-nginx # 启动包含 Nginx 的服务"
|
||||
echo " $0 logs xianyu-app # 查看应用日志"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
case "$1" in
|
||||
"init")
|
||||
check_dependencies
|
||||
init_config
|
||||
;;
|
||||
"build")
|
||||
check_dependencies
|
||||
build_image
|
||||
;;
|
||||
"start")
|
||||
check_dependencies
|
||||
init_config
|
||||
build_image
|
||||
start_services "$2"
|
||||
;;
|
||||
"stop")
|
||||
stop_services
|
||||
;;
|
||||
"restart")
|
||||
restart_services
|
||||
;;
|
||||
"status")
|
||||
show_status
|
||||
;;
|
||||
"logs")
|
||||
show_logs "$2"
|
||||
;;
|
||||
"health")
|
||||
health_check
|
||||
;;
|
||||
"backup")
|
||||
backup_data
|
||||
;;
|
||||
"update")
|
||||
check_dependencies
|
||||
update_deployment
|
||||
;;
|
||||
"cleanup")
|
||||
cleanup
|
||||
;;
|
||||
"help"|"--help"|"-h")
|
||||
show_help
|
||||
;;
|
||||
"")
|
||||
print_info "快速部署模式"
|
||||
check_dependencies
|
||||
init_config
|
||||
build_image
|
||||
start_services
|
||||
;;
|
||||
*)
|
||||
print_error "未知命令: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 执行主函数
|
||||
main "$@"
|
240
file_log_collector.py
Normal file
240
file_log_collector.py
Normal file
@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
基于文件监控的日志收集器
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import threading
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
from pathlib import Path
|
||||
|
||||
class FileLogCollector:
|
||||
"""基于文件监控的日志收集器"""
|
||||
|
||||
def __init__(self, max_logs: int = 2000):
|
||||
self.max_logs = max_logs
|
||||
self.logs = deque(maxlen=max_logs)
|
||||
self.lock = threading.Lock()
|
||||
|
||||
# 日志文件路径
|
||||
self.log_file = None
|
||||
self.last_position = 0
|
||||
|
||||
# 启动文件监控
|
||||
self.setup_file_monitoring()
|
||||
|
||||
def setup_file_monitoring(self):
|
||||
"""设置文件监控"""
|
||||
# 查找日志文件
|
||||
possible_files = [
|
||||
"xianyu.log",
|
||||
"app.log",
|
||||
"system.log",
|
||||
"logs/xianyu.log",
|
||||
"logs/app.log"
|
||||
]
|
||||
|
||||
for file_path in possible_files:
|
||||
if os.path.exists(file_path):
|
||||
self.log_file = file_path
|
||||
break
|
||||
|
||||
if not self.log_file:
|
||||
# 如果没有找到现有文件,创建一个新的
|
||||
self.log_file = "realtime.log"
|
||||
|
||||
# 设置loguru输出到文件
|
||||
self.setup_loguru_file_output()
|
||||
|
||||
# 启动文件监控线程
|
||||
self.monitor_thread = threading.Thread(target=self.monitor_file, daemon=True)
|
||||
self.monitor_thread.start()
|
||||
|
||||
def setup_loguru_file_output(self):
|
||||
"""设置loguru输出到文件"""
|
||||
try:
|
||||
from loguru import logger
|
||||
|
||||
# 添加文件输出
|
||||
logger.add(
|
||||
self.log_file,
|
||||
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level} | {name}:{function}:{line} - {message}",
|
||||
level="DEBUG",
|
||||
rotation="10 MB",
|
||||
retention="7 days",
|
||||
enqueue=False, # 改为False,避免队列延迟
|
||||
buffering=1 # 行缓冲,立即写入
|
||||
)
|
||||
|
||||
logger.info("文件日志收集器已启动")
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def monitor_file(self):
|
||||
"""监控日志文件变化"""
|
||||
while True:
|
||||
try:
|
||||
if os.path.exists(self.log_file):
|
||||
# 获取文件大小
|
||||
file_size = os.path.getsize(self.log_file)
|
||||
|
||||
if file_size > self.last_position:
|
||||
# 读取新增内容
|
||||
with open(self.log_file, 'r', encoding='utf-8') as f:
|
||||
f.seek(self.last_position)
|
||||
new_lines = f.readlines()
|
||||
self.last_position = f.tell()
|
||||
|
||||
# 解析新增的日志行
|
||||
for line in new_lines:
|
||||
self.parse_log_line(line.strip())
|
||||
|
||||
time.sleep(0.5) # 每0.5秒检查一次
|
||||
|
||||
except Exception as e:
|
||||
time.sleep(1) # 出错时等待1秒
|
||||
|
||||
def parse_log_line(self, line: str):
|
||||
"""解析日志行"""
|
||||
if not line:
|
||||
return
|
||||
|
||||
try:
|
||||
# 解析loguru格式的日志
|
||||
# 格式: 2025-07-23 15:46:03.430 | INFO | __main__:debug_collector:70 - 消息
|
||||
pattern = r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \| (\w+) \| ([^:]+):([^:]+):(\d+) - (.*)'
|
||||
match = re.match(pattern, line)
|
||||
|
||||
if match:
|
||||
timestamp_str, level, source, function, line_num, message = match.groups()
|
||||
|
||||
# 转换时间格式
|
||||
try:
|
||||
timestamp = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S.%f')
|
||||
except:
|
||||
timestamp = datetime.now()
|
||||
|
||||
log_entry = {
|
||||
"timestamp": timestamp.isoformat(),
|
||||
"level": level,
|
||||
"source": source,
|
||||
"function": function,
|
||||
"line": int(line_num),
|
||||
"message": message
|
||||
}
|
||||
|
||||
with self.lock:
|
||||
self.logs.append(log_entry)
|
||||
|
||||
except Exception as e:
|
||||
# 如果解析失败,作为普通消息处理
|
||||
log_entry = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"level": "INFO",
|
||||
"source": "system",
|
||||
"function": "unknown",
|
||||
"line": 0,
|
||||
"message": line
|
||||
}
|
||||
|
||||
with self.lock:
|
||||
self.logs.append(log_entry)
|
||||
|
||||
def get_logs(self, lines: int = 200, level_filter: str = None, source_filter: str = None) -> List[Dict]:
|
||||
"""获取日志记录"""
|
||||
with self.lock:
|
||||
logs_list = list(self.logs)
|
||||
|
||||
# 应用过滤器
|
||||
if level_filter:
|
||||
logs_list = [log for log in logs_list if log['level'] == level_filter]
|
||||
|
||||
if source_filter:
|
||||
logs_list = [log for log in logs_list if source_filter.lower() in log['source'].lower()]
|
||||
|
||||
# 返回最后N行
|
||||
return logs_list[-lines:] if len(logs_list) > lines else logs_list
|
||||
|
||||
def clear_logs(self):
|
||||
"""清空日志"""
|
||||
with self.lock:
|
||||
self.logs.clear()
|
||||
|
||||
def get_stats(self) -> Dict:
|
||||
"""获取日志统计信息"""
|
||||
with self.lock:
|
||||
total_logs = len(self.logs)
|
||||
|
||||
# 统计各级别日志数量
|
||||
level_counts = {}
|
||||
source_counts = {}
|
||||
|
||||
for log in self.logs:
|
||||
level = log['level']
|
||||
source = log['source']
|
||||
|
||||
level_counts[level] = level_counts.get(level, 0) + 1
|
||||
source_counts[source] = source_counts.get(source, 0) + 1
|
||||
|
||||
return {
|
||||
"total_logs": total_logs,
|
||||
"level_counts": level_counts,
|
||||
"source_counts": source_counts,
|
||||
"max_capacity": self.max_logs,
|
||||
"log_file": self.log_file
|
||||
}
|
||||
|
||||
|
||||
# 全局文件日志收集器实例
|
||||
_file_collector = None
|
||||
_file_collector_lock = threading.Lock()
|
||||
|
||||
|
||||
def get_file_log_collector() -> FileLogCollector:
|
||||
"""获取全局文件日志收集器实例"""
|
||||
global _file_collector
|
||||
|
||||
if _file_collector is None:
|
||||
with _file_collector_lock:
|
||||
if _file_collector is None:
|
||||
_file_collector = FileLogCollector(max_logs=2000)
|
||||
|
||||
return _file_collector
|
||||
|
||||
|
||||
def setup_file_logging():
|
||||
"""设置文件日志系统"""
|
||||
collector = get_file_log_collector()
|
||||
return collector
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试文件日志收集器
|
||||
collector = setup_file_logging()
|
||||
|
||||
# 生成一些测试日志
|
||||
from loguru import logger
|
||||
|
||||
logger.info("文件日志收集器测试开始")
|
||||
logger.debug("这是调试信息")
|
||||
logger.warning("这是警告信息")
|
||||
logger.error("这是错误信息")
|
||||
logger.info("文件日志收集器测试结束")
|
||||
|
||||
# 等待文件写入和监控
|
||||
time.sleep(2)
|
||||
|
||||
# 获取日志
|
||||
logs = collector.get_logs(10)
|
||||
print(f"收集到 {len(logs)} 条日志:")
|
||||
for log in logs:
|
||||
print(f" [{log['level']}] {log['source']}: {log['message']}")
|
||||
|
||||
# 获取统计信息
|
||||
stats = collector.get_stats()
|
||||
print(f"\n统计信息: {stats}")
|
156
fix-db-permissions.bat
Normal file
156
fix-db-permissions.bat
Normal file
@ -0,0 +1,156 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: 修复数据库权限问题的脚本 (Windows版本)
|
||||
:: 解决Docker容器中数据库无法创建的问题
|
||||
|
||||
title 数据库权限修复脚本
|
||||
|
||||
:: 颜色定义
|
||||
set "RED=[91m"
|
||||
set "GREEN=[92m"
|
||||
set "YELLOW=[93m"
|
||||
set "BLUE=[94m"
|
||||
set "NC=[0m"
|
||||
|
||||
:: 打印带颜色的消息
|
||||
:print_info
|
||||
echo %BLUE%[INFO]%NC% %~1
|
||||
goto :eof
|
||||
|
||||
:print_success
|
||||
echo %GREEN%[SUCCESS]%NC% %~1
|
||||
goto :eof
|
||||
|
||||
:print_warning
|
||||
echo %YELLOW%[WARNING]%NC% %~1
|
||||
goto :eof
|
||||
|
||||
:print_error
|
||||
echo %RED%[ERROR]%NC% %~1
|
||||
goto :eof
|
||||
|
||||
echo ========================================
|
||||
echo 数据库权限修复脚本
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
:: 1. 停止现有容器
|
||||
call :print_info "停止现有容器..."
|
||||
docker-compose down >nul 2>&1
|
||||
|
||||
:: 2. 检查并创建目录
|
||||
call :print_info "检查并创建必要目录..."
|
||||
|
||||
for %%d in (data logs backups) do (
|
||||
if not exist "%%d" (
|
||||
call :print_info "创建目录: %%d"
|
||||
mkdir "%%d"
|
||||
)
|
||||
|
||||
if not exist "%%d" (
|
||||
call :print_error "目录 %%d 创建失败"
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
call :print_success "目录 %%d 权限正常"
|
||||
)
|
||||
|
||||
:: 3. 检查现有数据库文件
|
||||
if exist "data\xianyu_data.db" (
|
||||
call :print_info "检查现有数据库文件..."
|
||||
call :print_success "数据库文件存在"
|
||||
) else (
|
||||
call :print_info "数据库文件不存在,将在启动时创建"
|
||||
)
|
||||
|
||||
:: 4. 测试数据库创建
|
||||
call :print_info "测试数据库创建..."
|
||||
python -c "
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
db_path = 'data/test_db.sqlite'
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY)')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print('✅ 数据库创建测试成功')
|
||||
if os.path.exists(db_path):
|
||||
os.remove(db_path)
|
||||
except Exception as e:
|
||||
print(f'❌ 数据库创建测试失败: {e}')
|
||||
exit(1)
|
||||
"
|
||||
|
||||
if !errorlevel! neq 0 (
|
||||
call :print_error "数据库创建测试失败"
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: 5. 重新构建并启动
|
||||
call :print_info "重新构建并启动服务..."
|
||||
docker-compose build --no-cache
|
||||
if !errorlevel! neq 0 (
|
||||
call :print_error "Docker镜像构建失败"
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
docker-compose up -d
|
||||
if !errorlevel! neq 0 (
|
||||
call :print_error "服务启动失败"
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: 6. 等待服务启动
|
||||
call :print_info "等待服务启动..."
|
||||
timeout /t 15 /nobreak >nul
|
||||
|
||||
:: 7. 检查服务状态
|
||||
call :print_info "检查服务状态..."
|
||||
docker-compose ps | findstr "Up" >nul
|
||||
if !errorlevel! equ 0 (
|
||||
call :print_success "服务启动成功"
|
||||
|
||||
:: 检查日志
|
||||
call :print_info "检查启动日志..."
|
||||
docker-compose logs --tail=20 xianyu-app
|
||||
|
||||
:: 测试健康检查
|
||||
call :print_info "测试健康检查..."
|
||||
timeout /t 5 /nobreak >nul
|
||||
curl -f http://localhost:8080/health >nul 2>&1
|
||||
if !errorlevel! equ 0 (
|
||||
call :print_success "健康检查通过"
|
||||
) else (
|
||||
call :print_warning "健康检查失败,但服务可能仍在启动中"
|
||||
)
|
||||
) else (
|
||||
call :print_error "服务启动失败"
|
||||
call :print_info "查看错误日志:"
|
||||
docker-compose logs xianyu-app
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
call :print_success "数据库权限修复完成!"
|
||||
echo.
|
||||
call :print_info "服务信息:"
|
||||
echo Web界面: http://localhost:8080
|
||||
echo 健康检查: http://localhost:8080/health
|
||||
echo 默认账号: admin / admin123
|
||||
echo.
|
||||
call :print_info "常用命令:"
|
||||
echo 查看日志: docker-compose logs -f
|
||||
echo 重启服务: docker-compose restart
|
||||
echo 停止服务: docker-compose down
|
||||
echo.
|
||||
|
||||
pause
|
167
fix-db-permissions.sh
Normal file
167
fix-db-permissions.sh
Normal file
@ -0,0 +1,167 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 修复数据库权限问题的脚本
|
||||
# 解决Docker容器中数据库无法创建的问题
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
echo "========================================"
|
||||
echo " 数据库权限修复脚本"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# 1. 停止现有容器
|
||||
print_info "停止现有容器..."
|
||||
docker-compose down 2>/dev/null || true
|
||||
|
||||
# 2. 检查并创建目录
|
||||
print_info "检查并创建必要目录..."
|
||||
for dir in data logs backups; do
|
||||
if [ ! -d "$dir" ]; then
|
||||
print_info "创建目录: $dir"
|
||||
mkdir -p "$dir"
|
||||
fi
|
||||
|
||||
# 设置权限
|
||||
chmod 755 "$dir"
|
||||
|
||||
# 检查权限
|
||||
if [ ! -w "$dir" ]; then
|
||||
print_error "目录 $dir 没有写权限"
|
||||
|
||||
# 尝试修复权限
|
||||
print_info "尝试修复权限..."
|
||||
sudo chmod 755 "$dir" 2>/dev/null || {
|
||||
print_error "无法修复权限,请手动执行: sudo chmod 755 $dir"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
print_success "目录 $dir 权限正常"
|
||||
done
|
||||
|
||||
# 3. 检查现有数据库文件
|
||||
if [ -f "data/xianyu_data.db" ]; then
|
||||
print_info "检查现有数据库文件权限..."
|
||||
if [ ! -w "data/xianyu_data.db" ]; then
|
||||
print_warning "数据库文件没有写权限,尝试修复..."
|
||||
chmod 644 "data/xianyu_data.db"
|
||||
print_success "数据库文件权限已修复"
|
||||
else
|
||||
print_success "数据库文件权限正常"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. 检查Docker用户映射
|
||||
print_info "检查Docker用户映射..."
|
||||
CURRENT_UID=$(id -u)
|
||||
CURRENT_GID=$(id -g)
|
||||
|
||||
print_info "当前用户 UID:GID = $CURRENT_UID:$CURRENT_GID"
|
||||
|
||||
# 5. 创建测试数据库
|
||||
print_info "测试数据库创建..."
|
||||
python3 -c "
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
db_path = 'data/test_db.sqlite'
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY)')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print('✅ 数据库创建测试成功')
|
||||
os.remove(db_path)
|
||||
except Exception as e:
|
||||
print(f'❌ 数据库创建测试失败: {e}')
|
||||
exit(1)
|
||||
" || {
|
||||
print_error "数据库创建测试失败"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 6. 更新docker-compose.yml用户映射
|
||||
print_info "检查docker-compose.yml用户映射..."
|
||||
if ! grep -q "user:" docker-compose.yml; then
|
||||
print_info "添加用户映射到docker-compose.yml..."
|
||||
|
||||
# 备份原文件
|
||||
cp docker-compose.yml docker-compose.yml.backup
|
||||
|
||||
# 在xianyu-app服务中添加user配置
|
||||
sed -i '/container_name: xianyu-auto-reply/a\ user: "'$CURRENT_UID':'$CURRENT_GID'"' docker-compose.yml
|
||||
|
||||
print_success "用户映射已添加"
|
||||
else
|
||||
print_info "用户映射已存在"
|
||||
fi
|
||||
|
||||
# 7. 重新构建并启动
|
||||
print_info "重新构建并启动服务..."
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
|
||||
# 8. 等待服务启动
|
||||
print_info "等待服务启动..."
|
||||
sleep 10
|
||||
|
||||
# 9. 检查服务状态
|
||||
print_info "检查服务状态..."
|
||||
if docker-compose ps | grep -q "Up"; then
|
||||
print_success "服务启动成功"
|
||||
|
||||
# 检查日志
|
||||
print_info "检查启动日志..."
|
||||
docker-compose logs --tail=20 xianyu-app
|
||||
|
||||
# 测试健康检查
|
||||
print_info "测试健康检查..."
|
||||
sleep 5
|
||||
if curl -f http://localhost:8080/health >/dev/null 2>&1; then
|
||||
print_success "健康检查通过"
|
||||
else
|
||||
print_warning "健康检查失败,但服务可能仍在启动中"
|
||||
fi
|
||||
else
|
||||
print_error "服务启动失败"
|
||||
print_info "查看错误日志:"
|
||||
docker-compose logs xianyu-app
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_success "数据库权限修复完成!"
|
||||
echo ""
|
||||
print_info "服务信息:"
|
||||
echo " Web界面: http://localhost:8080"
|
||||
echo " 健康检查: http://localhost:8080/health"
|
||||
echo " 默认账号: admin / admin123"
|
||||
echo ""
|
||||
print_info "常用命令:"
|
||||
echo " 查看日志: docker-compose logs -f"
|
||||
echo " 重启服务: docker-compose restart"
|
||||
echo " 停止服务: docker-compose down"
|
144
fix-docker-warnings.bat
Normal file
144
fix-docker-warnings.bat
Normal file
@ -0,0 +1,144 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: 修复Docker部署警告的快速脚本 (Windows版本)
|
||||
:: 解决version过时和.env文件缺失问题
|
||||
|
||||
title Docker部署警告修复脚本
|
||||
|
||||
:: 颜色定义
|
||||
set "RED=[91m"
|
||||
set "GREEN=[92m"
|
||||
set "YELLOW=[93m"
|
||||
set "BLUE=[94m"
|
||||
set "NC=[0m"
|
||||
|
||||
:: 打印带颜色的消息
|
||||
:print_info
|
||||
echo %BLUE%[INFO]%NC% %~1
|
||||
goto :eof
|
||||
|
||||
:print_success
|
||||
echo %GREEN%[SUCCESS]%NC% %~1
|
||||
goto :eof
|
||||
|
||||
:print_warning
|
||||
echo %YELLOW%[WARNING]%NC% %~1
|
||||
goto :eof
|
||||
|
||||
:print_error
|
||||
echo %RED%[ERROR]%NC% %~1
|
||||
goto :eof
|
||||
|
||||
echo ========================================
|
||||
echo Docker部署警告修复脚本
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
:: 1. 检查并创建.env文件
|
||||
call :print_info "检查 .env 文件..."
|
||||
if not exist ".env" (
|
||||
if exist ".env.example" (
|
||||
call :print_info "从 .env.example 创建 .env 文件..."
|
||||
copy ".env.example" ".env" >nul
|
||||
call :print_success ".env 文件已创建"
|
||||
) else (
|
||||
call :print_warning ".env.example 文件不存在"
|
||||
call :print_info "创建基本的 .env 文件..."
|
||||
|
||||
(
|
||||
echo # 闲鱼自动回复系统 Docker 环境变量配置文件
|
||||
echo.
|
||||
echo # 基础配置
|
||||
echo TZ=Asia/Shanghai
|
||||
echo PYTHONUNBUFFERED=1
|
||||
echo LOG_LEVEL=INFO
|
||||
echo.
|
||||
echo # 数据库配置
|
||||
echo DB_PATH=/app/data/xianyu_data.db
|
||||
echo.
|
||||
echo # 服务配置
|
||||
echo WEB_PORT=8080
|
||||
echo.
|
||||
echo # 安全配置
|
||||
echo ADMIN_USERNAME=admin
|
||||
echo ADMIN_PASSWORD=admin123
|
||||
echo JWT_SECRET_KEY=xianyu-auto-reply-secret-key-2024
|
||||
echo.
|
||||
echo # 资源限制
|
||||
echo MEMORY_LIMIT=512
|
||||
echo CPU_LIMIT=0.5
|
||||
echo MEMORY_RESERVATION=256
|
||||
echo CPU_RESERVATION=0.25
|
||||
echo.
|
||||
echo # 自动回复配置
|
||||
echo AUTO_REPLY_ENABLED=true
|
||||
echo WEBSOCKET_URL=wss://wss-goofish.dingtalk.com/
|
||||
echo HEARTBEAT_INTERVAL=15
|
||||
echo TOKEN_REFRESH_INTERVAL=3600
|
||||
) > .env
|
||||
|
||||
call :print_success "基本 .env 文件已创建"
|
||||
)
|
||||
) else (
|
||||
call :print_success ".env 文件已存在"
|
||||
)
|
||||
|
||||
:: 2. 检查docker-compose.yml版本问题
|
||||
call :print_info "检查 docker-compose.yml 配置..."
|
||||
findstr /B "version:" docker-compose.yml >nul 2>&1
|
||||
if !errorlevel! equ 0 (
|
||||
call :print_warning "发现过时的 version 字段"
|
||||
call :print_info "移除 version 字段..."
|
||||
|
||||
REM 备份原文件
|
||||
copy docker-compose.yml docker-compose.yml.backup >nul
|
||||
|
||||
REM 创建临时文件,移除version行
|
||||
(
|
||||
for /f "tokens=*" %%a in (docker-compose.yml) do (
|
||||
echo %%a | findstr /B "version:" >nul
|
||||
if !errorlevel! neq 0 (
|
||||
echo %%a
|
||||
)
|
||||
)
|
||||
) > docker-compose.yml.tmp
|
||||
|
||||
REM 替换原文件
|
||||
move docker-compose.yml.tmp docker-compose.yml >nul
|
||||
|
||||
call :print_success "已移除过时的 version 字段"
|
||||
call :print_info "原文件已备份为 docker-compose.yml.backup"
|
||||
) else (
|
||||
call :print_success "docker-compose.yml 配置正确"
|
||||
)
|
||||
|
||||
:: 3. 验证修复结果
|
||||
call :print_info "验证修复结果..."
|
||||
|
||||
echo.
|
||||
call :print_info "测试 Docker Compose 配置..."
|
||||
docker-compose config >nul 2>&1
|
||||
if !errorlevel! equ 0 (
|
||||
call :print_success "Docker Compose 配置验证通过"
|
||||
) else (
|
||||
call :print_error "Docker Compose 配置验证失败"
|
||||
echo 请检查 docker-compose.yml 文件
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
call :print_success "所有警告已修复!"
|
||||
echo.
|
||||
call :print_info "现在可以正常使用以下命令:"
|
||||
echo docker-compose up -d # 启动服务
|
||||
echo docker-compose ps # 查看状态
|
||||
echo docker-compose logs -f # 查看日志
|
||||
echo.
|
||||
call :print_info "如果需要恢复原配置:"
|
||||
echo move docker-compose.yml.backup docker-compose.yml
|
||||
echo.
|
||||
|
||||
pause
|
145
fix-docker-warnings.sh
Normal file
145
fix-docker-warnings.sh
Normal file
@ -0,0 +1,145 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 修复Docker部署警告的快速脚本
|
||||
# 解决version过时和.env文件缺失问题
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
echo "========================================"
|
||||
echo " Docker部署警告修复脚本"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# 1. 检查并创建.env文件
|
||||
print_info "检查 .env 文件..."
|
||||
if [ ! -f ".env" ]; then
|
||||
if [ -f ".env.example" ]; then
|
||||
print_info "从 .env.example 创建 .env 文件..."
|
||||
cp .env.example .env
|
||||
print_success ".env 文件已创建"
|
||||
else
|
||||
print_warning ".env.example 文件不存在"
|
||||
print_info "创建基本的 .env 文件..."
|
||||
cat > .env << 'EOF'
|
||||
# 闲鱼自动回复系统 Docker 环境变量配置文件
|
||||
|
||||
# 基础配置
|
||||
TZ=Asia/Shanghai
|
||||
PYTHONUNBUFFERED=1
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# 数据库配置
|
||||
DB_PATH=/app/data/xianyu_data.db
|
||||
|
||||
# 服务配置
|
||||
WEB_PORT=8080
|
||||
|
||||
# 安全配置
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=admin123
|
||||
JWT_SECRET_KEY=xianyu-auto-reply-secret-key-2024
|
||||
|
||||
# 资源限制
|
||||
MEMORY_LIMIT=512
|
||||
CPU_LIMIT=0.5
|
||||
MEMORY_RESERVATION=256
|
||||
CPU_RESERVATION=0.25
|
||||
|
||||
# 自动回复配置
|
||||
AUTO_REPLY_ENABLED=true
|
||||
WEBSOCKET_URL=wss://wss-goofish.dingtalk.com/
|
||||
HEARTBEAT_INTERVAL=15
|
||||
TOKEN_REFRESH_INTERVAL=3600
|
||||
EOF
|
||||
print_success "基本 .env 文件已创建"
|
||||
fi
|
||||
else
|
||||
print_success ".env 文件已存在"
|
||||
fi
|
||||
|
||||
# 2. 检查docker-compose.yml版本问题
|
||||
print_info "检查 docker-compose.yml 配置..."
|
||||
if grep -q "^version:" docker-compose.yml 2>/dev/null; then
|
||||
print_warning "发现过时的 version 字段"
|
||||
print_info "移除 version 字段..."
|
||||
|
||||
# 备份原文件
|
||||
cp docker-compose.yml docker-compose.yml.backup
|
||||
|
||||
# 移除version行
|
||||
sed -i '/^version:/d' docker-compose.yml
|
||||
sed -i '/^$/N;/^\n$/d' docker-compose.yml # 移除空行
|
||||
|
||||
print_success "已移除过时的 version 字段"
|
||||
print_info "原文件已备份为 docker-compose.yml.backup"
|
||||
else
|
||||
print_success "docker-compose.yml 配置正确"
|
||||
fi
|
||||
|
||||
# 3. 检查env_file配置
|
||||
print_info "检查 env_file 配置..."
|
||||
if grep -A1 "env_file:" docker-compose.yml | grep -q "required: false"; then
|
||||
print_success "env_file 配置正确"
|
||||
else
|
||||
print_info "更新 env_file 配置为可选..."
|
||||
|
||||
# 备份文件(如果还没备份)
|
||||
if [ ! -f "docker-compose.yml.backup" ]; then
|
||||
cp docker-compose.yml docker-compose.yml.backup
|
||||
fi
|
||||
|
||||
# 更新env_file配置
|
||||
sed -i '/env_file:/,+1c\
|
||||
env_file:\
|
||||
- path: .env\
|
||||
required: false' docker-compose.yml
|
||||
|
||||
print_success "env_file 配置已更新"
|
||||
fi
|
||||
|
||||
# 4. 验证修复结果
|
||||
print_info "验证修复结果..."
|
||||
|
||||
echo ""
|
||||
print_info "测试 Docker Compose 配置..."
|
||||
if docker-compose config >/dev/null 2>&1; then
|
||||
print_success "Docker Compose 配置验证通过"
|
||||
else
|
||||
print_error "Docker Compose 配置验证失败"
|
||||
echo "请检查 docker-compose.yml 文件"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_success "所有警告已修复!"
|
||||
echo ""
|
||||
print_info "现在可以正常使用以下命令:"
|
||||
echo " docker-compose up -d # 启动服务"
|
||||
echo " docker-compose ps # 查看状态"
|
||||
echo " docker-compose logs -f # 查看日志"
|
||||
echo ""
|
||||
print_info "如果需要恢复原配置:"
|
||||
echo " mv docker-compose.yml.backup docker-compose.yml"
|
121
fix-websocket-issue.sh
Normal file
121
fix-websocket-issue.sh
Normal file
@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 快速修复WebSocket兼容性问题
|
||||
# 解决 "extra_headers" 参数不支持的问题
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
echo "🔧 WebSocket兼容性问题修复"
|
||||
echo "================================"
|
||||
|
||||
# 1. 检查当前websockets版本
|
||||
print_info "检查当前websockets版本..."
|
||||
if command -v python3 &> /dev/null; then
|
||||
PYTHON_CMD="python3"
|
||||
elif command -v python &> /dev/null; then
|
||||
PYTHON_CMD="python"
|
||||
else
|
||||
print_error "未找到Python解释器"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_VERSION=$($PYTHON_CMD -c "import websockets; print(websockets.__version__)" 2>/dev/null || echo "未安装")
|
||||
print_info "当前websockets版本: $CURRENT_VERSION"
|
||||
|
||||
# 2. 测试WebSocket兼容性
|
||||
print_info "测试WebSocket兼容性..."
|
||||
$PYTHON_CMD test-websocket-compatibility.py
|
||||
|
||||
# 3. 停止现有服务
|
||||
print_info "停止现有Docker服务..."
|
||||
docker-compose down 2>/dev/null || true
|
||||
|
||||
# 4. 更新websockets版本
|
||||
print_info "更新websockets版本到兼容版本..."
|
||||
if [ -f "requirements.txt" ]; then
|
||||
# 备份原文件
|
||||
cp requirements.txt requirements.txt.backup
|
||||
|
||||
# 更新websockets版本
|
||||
sed -i 's/websockets>=.*/websockets>=10.0,<13.0 # 兼容性版本范围/' requirements.txt
|
||||
|
||||
print_success "requirements.txt已更新"
|
||||
else
|
||||
print_warning "requirements.txt文件不存在"
|
||||
fi
|
||||
|
||||
# 5. 重新构建Docker镜像
|
||||
print_info "重新构建Docker镜像..."
|
||||
docker-compose build --no-cache
|
||||
|
||||
# 6. 启动服务
|
||||
print_info "启动服务..."
|
||||
docker-compose up -d
|
||||
|
||||
# 7. 等待服务启动
|
||||
print_info "等待服务启动..."
|
||||
sleep 15
|
||||
|
||||
# 8. 检查服务状态
|
||||
print_info "检查服务状态..."
|
||||
if docker-compose ps | grep -q "Up"; then
|
||||
print_success "✅ 服务启动成功!"
|
||||
|
||||
# 检查WebSocket错误
|
||||
print_info "检查WebSocket连接状态..."
|
||||
sleep 5
|
||||
|
||||
# 查看最近的日志
|
||||
echo ""
|
||||
print_info "最近的服务日志:"
|
||||
docker-compose logs --tail=20 xianyu-app | grep -E "(WebSocket|extra_headers|ERROR)" || echo "未发现WebSocket相关错误"
|
||||
|
||||
# 测试健康检查
|
||||
if curl -f http://localhost:8080/health >/dev/null 2>&1; then
|
||||
print_success "健康检查通过"
|
||||
else
|
||||
print_warning "健康检查失败,服务可能仍在启动中"
|
||||
fi
|
||||
|
||||
else
|
||||
print_error "❌ 服务启动失败"
|
||||
print_info "查看错误日志:"
|
||||
docker-compose logs --tail=30 xianyu-app
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_success "🎉 WebSocket兼容性问题修复完成!"
|
||||
echo ""
|
||||
print_info "服务信息:"
|
||||
echo " Web界面: http://localhost:8080"
|
||||
echo " 健康检查: http://localhost:8080/health"
|
||||
echo " 默认账号: admin / admin123"
|
||||
echo ""
|
||||
print_info "如果仍有WebSocket问题,请:"
|
||||
echo " 1. 查看日志: docker-compose logs -f xianyu-app"
|
||||
echo " 2. 运行测试: python test-websocket-compatibility.py"
|
||||
echo " 3. 检查网络连接和防火墙设置"
|
73
global_config.yml
Normal file
73
global_config.yml
Normal file
@ -0,0 +1,73 @@
|
||||
API_ENDPOINTS:
|
||||
login_check: https://passport.goofish.com/newlogin/hasLogin.do
|
||||
message_headinfo: https://h5api.m.goofish.com/h5/mtop.idle.trade.pc.message.headinfo/1.0/
|
||||
token: https://h5api.m.goofish.com/h5/mtop.taobao.idlemessage.pc.login.token/1.0/
|
||||
APP_CONFIG:
|
||||
api_version: '1.0'
|
||||
app_key: 444e9908a51d1cb236a27862abc769c9
|
||||
app_version: '1.0'
|
||||
platform: web
|
||||
AUTO_REPLY:
|
||||
api:
|
||||
enabled: true
|
||||
host: 0.0.0.0 # 绑定所有网络接口,支持IP访问
|
||||
port: 8080 # Web服务端口
|
||||
timeout: 10
|
||||
url: http://0.0.0.0:8080/xianyu/reply
|
||||
default_message: 亲爱的"{send_user_name}" 老板你好!所有宝贝都可以拍,秒发货的哈~不满意的话可以直接申请退款哈~
|
||||
enabled: true
|
||||
max_retry: 3
|
||||
retry_interval: 5
|
||||
ITEM_DETAIL:
|
||||
auto_fetch:
|
||||
enabled: true # 是否启用自动获取商品详情
|
||||
api_url: https://selfapi.zhinianboke.com/api/getItemDetail # 外部API地址
|
||||
timeout: 30 # 请求超时时间(秒)
|
||||
max_concurrent: 3 # 最大并发请求数
|
||||
retry_delay: 0.5 # 请求间隔(秒)
|
||||
COOKIES:
|
||||
last_update_time: ''
|
||||
value: ''
|
||||
DEFAULT_HEADERS:
|
||||
accept: application/json
|
||||
accept-language: zh-CN,zh;q=0.9
|
||||
cache-control: no-cache
|
||||
origin: https://www.goofish.com
|
||||
pragma: no-cache
|
||||
priority: u=1, i
|
||||
referer: https://www.goofish.com/
|
||||
sec-ch-ua: '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"'
|
||||
sec-ch-ua-mobile: ?0
|
||||
sec-ch-ua-platform: '"Windows"'
|
||||
sec-fetch-dest: empty
|
||||
sec-fetch-mode: cors
|
||||
sec-fetch-site: same-site
|
||||
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
|
||||
like Gecko) Chrome/133.0.0.0 Safari/537.36
|
||||
HEARTBEAT_INTERVAL: 15
|
||||
HEARTBEAT_TIMEOUT: 5
|
||||
LOG_CONFIG:
|
||||
compression: zip
|
||||
format: '<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level>
|
||||
| <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>'
|
||||
level: INFO
|
||||
retention: 7 days
|
||||
rotation: 1 day
|
||||
MANUAL_MODE:
|
||||
enabled: false
|
||||
timeout: 3600
|
||||
toggle_keywords: []
|
||||
MESSAGE_EXPIRE_TIME: 300000
|
||||
TOKEN_REFRESH_INTERVAL: 3600
|
||||
TOKEN_RETRY_INTERVAL: 300
|
||||
WEBSOCKET_HEADERS:
|
||||
Accept-Encoding: gzip, deflate, br, zstd
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Cache-Control: no-cache
|
||||
Connection: Upgrade
|
||||
Host: wss-goofish.dingtalk.com
|
||||
Origin: https://www.goofish.com
|
||||
Pragma: no-cache
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
|
||||
like Gecko) Chrome/133.0.0.0 Safari/537.36
|
||||
WEBSOCKET_URL: wss://wss-goofish.dingtalk.com/
|
123
nginx/nginx.conf
Normal file
123
nginx/nginx.conf
Normal file
@ -0,0 +1,123 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# 日志格式
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
# 基本设置
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
client_max_body_size 10M;
|
||||
|
||||
# Gzip压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/json
|
||||
application/javascript
|
||||
application/xml+rss
|
||||
application/atom+xml
|
||||
image/svg+xml;
|
||||
|
||||
# 上游服务器
|
||||
upstream xianyu_backend {
|
||||
server xianyu-app:8080;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# HTTP服务器配置
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# 安全头
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
|
||||
|
||||
# 代理到后端应用
|
||||
location / {
|
||||
proxy_pass http://xianyu_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
# 静态文件缓存
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
proxy_pass http://xianyu_backend;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
location /health {
|
||||
proxy_pass http://xianyu_backend/health;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# 错误页面
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS服务器配置(可选)
|
||||
# server {
|
||||
# listen 443 ssl http2;
|
||||
# server_name localhost;
|
||||
#
|
||||
# ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||
# ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||
# ssl_session_timeout 1d;
|
||||
# ssl_session_cache shared:MozTLS:10m;
|
||||
# ssl_session_tickets off;
|
||||
#
|
||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
# ssl_prefer_server_ciphers off;
|
||||
#
|
||||
# location / {
|
||||
# proxy_pass http://xianyu_backend;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection 'upgrade';
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_cache_bypass $http_upgrade;
|
||||
# }
|
||||
# }
|
||||
}
|
116
quick-fix-permissions.bat
Normal file
116
quick-fix-permissions.bat
Normal file
@ -0,0 +1,116 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: 快速修复Docker权限问题 (Windows版本)
|
||||
|
||||
title 快速修复Docker权限问题
|
||||
|
||||
:: 颜色定义
|
||||
set "RED=[91m"
|
||||
set "GREEN=[92m"
|
||||
set "YELLOW=[93m"
|
||||
set "BLUE=[94m"
|
||||
set "NC=[0m"
|
||||
|
||||
:print_info
|
||||
echo %BLUE%[INFO]%NC% %~1
|
||||
goto :eof
|
||||
|
||||
:print_success
|
||||
echo %GREEN%[SUCCESS]%NC% %~1
|
||||
goto :eof
|
||||
|
||||
:print_error
|
||||
echo %RED%[ERROR]%NC% %~1
|
||||
goto :eof
|
||||
|
||||
echo 🚀 快速修复Docker权限问题
|
||||
echo ================================
|
||||
echo.
|
||||
|
||||
:: 1. 停止容器
|
||||
call :print_info "停止现有容器..."
|
||||
docker-compose down >nul 2>&1
|
||||
|
||||
:: 2. 确保目录存在
|
||||
call :print_info "创建必要目录..."
|
||||
if not exist "data" mkdir data
|
||||
if not exist "logs" mkdir logs
|
||||
if not exist "backups" mkdir backups
|
||||
|
||||
:: 3. 检查并修复docker-compose.yml
|
||||
call :print_info "检查docker-compose.yml配置..."
|
||||
findstr /C:"user.*0:0" docker-compose.yml >nul 2>&1
|
||||
if !errorlevel! neq 0 (
|
||||
call :print_info "添加root用户配置..."
|
||||
|
||||
REM 备份原文件
|
||||
copy docker-compose.yml docker-compose.yml.backup >nul
|
||||
|
||||
REM 创建临时文件添加user配置
|
||||
(
|
||||
for /f "tokens=*" %%a in (docker-compose.yml) do (
|
||||
echo %%a
|
||||
echo %%a | findstr /C:"container_name: xianyu-auto-reply" >nul
|
||||
if !errorlevel! equ 0 (
|
||||
echo user: "0:0"
|
||||
)
|
||||
)
|
||||
) > docker-compose.yml.tmp
|
||||
|
||||
REM 替换原文件
|
||||
move docker-compose.yml.tmp docker-compose.yml >nul
|
||||
|
||||
call :print_success "已配置使用root用户运行"
|
||||
)
|
||||
|
||||
:: 4. 重新构建镜像
|
||||
call :print_info "重新构建Docker镜像..."
|
||||
docker-compose build --no-cache
|
||||
if !errorlevel! neq 0 (
|
||||
call :print_error "Docker镜像构建失败"
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: 5. 启动服务
|
||||
call :print_info "启动服务..."
|
||||
docker-compose up -d
|
||||
if !errorlevel! neq 0 (
|
||||
call :print_error "服务启动失败"
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: 6. 等待启动
|
||||
call :print_info "等待服务启动..."
|
||||
timeout /t 15 /nobreak >nul
|
||||
|
||||
:: 7. 检查状态
|
||||
call :print_info "检查服务状态..."
|
||||
docker-compose ps | findstr "Up" >nul
|
||||
if !errorlevel! equ 0 (
|
||||
call :print_success "✅ 服务启动成功!"
|
||||
|
||||
echo.
|
||||
call :print_info "最近的日志:"
|
||||
docker-compose logs --tail=10 xianyu-app
|
||||
|
||||
echo.
|
||||
call :print_success "🎉 权限问题已修复!"
|
||||
echo.
|
||||
echo 访问信息:
|
||||
echo Web界面: http://localhost:8080
|
||||
echo 健康检查: http://localhost:8080/health
|
||||
echo 默认账号: admin / admin123
|
||||
|
||||
) else (
|
||||
call :print_error "❌ 服务启动失败"
|
||||
echo.
|
||||
call :print_info "错误日志:"
|
||||
docker-compose logs xianyu-app
|
||||
)
|
||||
|
||||
echo.
|
||||
pause
|
88
quick-fix-permissions.sh
Normal file
88
quick-fix-permissions.sh
Normal file
@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 快速修复Docker权限问题
|
||||
# 这个脚本会立即解决权限问题并重启服务
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
echo "🚀 快速修复Docker权限问题"
|
||||
echo "================================"
|
||||
|
||||
# 1. 停止容器
|
||||
print_info "停止现有容器..."
|
||||
docker-compose down
|
||||
|
||||
# 2. 确保目录存在并设置权限
|
||||
print_info "设置目录权限..."
|
||||
mkdir -p data logs backups
|
||||
chmod 777 data logs backups
|
||||
|
||||
# 3. 检查并修复docker-compose.yml
|
||||
print_info "检查docker-compose.yml配置..."
|
||||
if ! grep -q "user.*0:0" docker-compose.yml; then
|
||||
print_info "添加root用户配置..."
|
||||
|
||||
# 备份原文件
|
||||
cp docker-compose.yml docker-compose.yml.backup
|
||||
|
||||
# 在container_name后添加user配置
|
||||
sed -i '/container_name: xianyu-auto-reply/a\ user: "0:0"' docker-compose.yml
|
||||
|
||||
print_success "已配置使用root用户运行"
|
||||
fi
|
||||
|
||||
# 4. 重新构建镜像
|
||||
print_info "重新构建Docker镜像..."
|
||||
docker-compose build --no-cache
|
||||
|
||||
# 5. 启动服务
|
||||
print_info "启动服务..."
|
||||
docker-compose up -d
|
||||
|
||||
# 6. 等待启动
|
||||
print_info "等待服务启动..."
|
||||
sleep 15
|
||||
|
||||
# 7. 检查状态
|
||||
print_info "检查服务状态..."
|
||||
if docker-compose ps | grep -q "Up"; then
|
||||
print_success "✅ 服务启动成功!"
|
||||
|
||||
# 显示日志
|
||||
echo ""
|
||||
print_info "最近的日志:"
|
||||
docker-compose logs --tail=10 xianyu-app
|
||||
|
||||
echo ""
|
||||
print_success "🎉 权限问题已修复!"
|
||||
echo ""
|
||||
echo "访问信息:"
|
||||
echo " Web界面: http://localhost:8080"
|
||||
echo " 健康检查: http://localhost:8080/health"
|
||||
echo " 默认账号: admin / admin123"
|
||||
|
||||
else
|
||||
print_error "❌ 服务启动失败"
|
||||
echo ""
|
||||
print_info "错误日志:"
|
||||
docker-compose logs xianyu-app
|
||||
fi
|
1152
reply_server.py
Normal file
1152
reply_server.py
Normal file
File diff suppressed because it is too large
Load Diff
29
requirements.txt
Normal file
29
requirements.txt
Normal file
@ -0,0 +1,29 @@
|
||||
# Web框架和API相关
|
||||
fastapi>=0.111
|
||||
uvicorn[standard]>=0.29
|
||||
pydantic>=2.7
|
||||
|
||||
# 日志记录
|
||||
loguru>=0.7
|
||||
|
||||
# 网络通信
|
||||
websockets>=10.0,<13.0 # 兼容性版本范围
|
||||
aiohttp>=3.9
|
||||
|
||||
# 配置文件处理
|
||||
PyYAML>=6.0
|
||||
|
||||
# JavaScript执行引擎
|
||||
PyExecJS>=1.5.1
|
||||
|
||||
# 协议缓冲区解析
|
||||
blackboxprotobuf>=1.0.1
|
||||
|
||||
# 系统监控
|
||||
psutil>=5.9.0
|
||||
|
||||
# HTTP客户端(用于测试)
|
||||
requests>=2.31.0
|
||||
|
||||
# 文件上传支持
|
||||
python-multipart>=0.0.6
|
6368
static/index.html
Normal file
6368
static/index.html
Normal file
File diff suppressed because it is too large
Load Diff
281
static/login.html
Normal file
281
static/login.html
Normal file
@ -0,0 +1,281 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - 闲鱼自动回复管理系统</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #0d6efd;
|
||||
--secondary-color: #6c757d;
|
||||
--success-color: #198754;
|
||||
--danger-color: #dc3545;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-header h2 {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
margin: 10px 0 0 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.login-body {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 10px;
|
||||
border: 2px solid #e9ecef;
|
||||
padding: 12px 15px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
background: var(--primary-color);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
background: #0b5ed7;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-group i {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--secondary-color);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.input-group .form-control {
|
||||
padding-left: 45px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<i class="bi bi-chat-dots-fill fs-1 mb-3"></i>
|
||||
<h2>闲鱼自动回复</h2>
|
||||
<p>管理系统登录</p>
|
||||
</div>
|
||||
|
||||
<div class="login-body">
|
||||
<form id="loginForm">
|
||||
<div class="input-group">
|
||||
<i class="bi bi-person-fill"></i>
|
||||
<input type="text" class="form-control" id="username" placeholder="请输入用户名" required>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<i class="bi bi-lock-fill"></i>
|
||||
<input type="password" class="form-control" id="password" placeholder="请输入密码" required>
|
||||
</div>
|
||||
|
||||
<div id="errorAlert" class="alert alert-danger d-none" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<span id="errorMessage"></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-login">
|
||||
<span class="loading spinner-border spinner-border-sm me-2" role="status"></span>
|
||||
<span id="loginText">登录</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- 默认账号提示 -->
|
||||
<div class="mt-4 p-3 bg-light rounded-3">
|
||||
<div class="text-center">
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
默认登录账号
|
||||
</small>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<small class="text-muted">用户名:</small>
|
||||
<code class="bg-white px-2 py-1 rounded">admin</code>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">密码:</small>
|
||||
<code class="bg-white px-2 py-1 rounded">admin123</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="fillDefaultCredentials()">
|
||||
<i class="bi bi-arrow-down-circle me-1"></i>
|
||||
使用默认账号
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const errorAlert = document.getElementById('errorAlert');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
const loading = document.querySelector('.loading');
|
||||
const loginText = document.getElementById('loginText');
|
||||
|
||||
function showError(message) {
|
||||
errorMessage.textContent = message;
|
||||
errorAlert.classList.remove('d-none');
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
errorAlert.classList.add('d-none');
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
if (isLoading) {
|
||||
loading.classList.add('show');
|
||||
loginText.textContent = '登录中...';
|
||||
loginForm.querySelector('button').disabled = true;
|
||||
} else {
|
||||
loading.classList.remove('show');
|
||||
loginText.textContent = '登录';
|
||||
loginForm.querySelector('button').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 填充默认登录凭据
|
||||
function fillDefaultCredentials() {
|
||||
document.getElementById('username').value = 'admin';
|
||||
document.getElementById('password').value = 'admin123';
|
||||
hideError();
|
||||
|
||||
// 添加一个小动画效果
|
||||
const usernameInput = document.getElementById('username');
|
||||
const passwordInput = document.getElementById('password');
|
||||
|
||||
usernameInput.style.backgroundColor = '#e3f2fd';
|
||||
passwordInput.style.backgroundColor = '#e3f2fd';
|
||||
|
||||
setTimeout(() => {
|
||||
usernameInput.style.backgroundColor = '';
|
||||
passwordInput.style.backgroundColor = '';
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideError();
|
||||
setLoading(true);
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// 保存token到localStorage
|
||||
localStorage.setItem('auth_token', result.token);
|
||||
// 跳转到管理页面
|
||||
window.location.href = '/admin';
|
||||
} else {
|
||||
showError(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('登录失败,请检查网络连接');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否已经登录
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token) {
|
||||
// 验证token是否有效
|
||||
fetch('/verify', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.authenticated) {
|
||||
window.location.href = '/admin';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// token无效,清除
|
||||
localStorage.removeItem('auth_token');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
568
static/xianyu_js_version_2.js
Normal file
568
static/xianyu_js_version_2.js
Normal file
@ -0,0 +1,568 @@
|
||||
const crypto = require('crypto')
|
||||
|
||||
const generate_mid = () => {
|
||||
return "" + Math.floor(1e3 * Math.random()) + new Date().getTime() + " 0"
|
||||
}
|
||||
|
||||
const generate_uuid = () => {
|
||||
return "-" + Date.now() + "1"
|
||||
}
|
||||
|
||||
const generate_device_id = (user_id) => {
|
||||
for (var ee, et = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split(""), en = [], eo = Math.random, ei = 0; ei < 36; ei++)
|
||||
8 === ei || 13 === ei || 18 === ei || 23 === ei ? en[ei] = "-" : 14 === ei ? en[ei] = "4" : (ee = 0 | 16 * eo(),
|
||||
en[ei] = et[19 === ei ? 3 & ee | 8 : ee]);
|
||||
return en.join("") + "-" + user_id
|
||||
}
|
||||
|
||||
|
||||
const generate_sign = (t, token, data) => {
|
||||
const j = t
|
||||
const h = 34839810
|
||||
const msg = token + "&" + j + "&" + h + "&" + data
|
||||
const md5 = crypto.createHash('md5')
|
||||
md5.update(msg)
|
||||
return md5.digest('hex')
|
||||
}
|
||||
eM = new TextDecoder("utf-8");
|
||||
eL = null
|
||||
var sv = Array(4096);
|
||||
|
||||
function sg() {
|
||||
var ee, et = eI[aG++];
|
||||
if (!(et >= 160) || !(et < 192))
|
||||
return aG--,
|
||||
sy(a5());
|
||||
if (et -= 160,
|
||||
aX >= aG)
|
||||
return eR.slice(aG - aq, (aG += et) - aq);
|
||||
if (!(0 == aX && eO < 180))
|
||||
return se(et);
|
||||
var en = (et << 5 ^ (et > 1 ? eZ.getUint16(aG) : et > 0 ? eI[aG] : 0)) & 4095
|
||||
, eo = sv[en]
|
||||
, ei = aG
|
||||
, ec = aG + et - 3
|
||||
, eu = 0;
|
||||
if (eo && eo.bytes == et) {
|
||||
for (; ei < ec;) {
|
||||
if ((ee = eZ.getUint32(ei)) != eo[eu++]) {
|
||||
ei = 1879048192;
|
||||
break
|
||||
}
|
||||
ei += 4
|
||||
}
|
||||
for (ec += 3; ei < ec;)
|
||||
if ((ee = eI[ei++]) != eo[eu++]) {
|
||||
ei = 1879048192;
|
||||
break
|
||||
}
|
||||
if (ei === ec)
|
||||
return aG = ei,
|
||||
eo.string;
|
||||
ec -= 3,
|
||||
ei = aG
|
||||
}
|
||||
for (eo = [],
|
||||
sv[en] = eo,
|
||||
eo.bytes = et; ei < ec;)
|
||||
ee = eZ.getUint32(ei),
|
||||
eo.push(ee),
|
||||
ei += 4;
|
||||
for (ec += 3; ei < ec;)
|
||||
ee = eI[ei++],
|
||||
eo.push(ee);
|
||||
var ed = et < 16 ? su(et) : sl(et);
|
||||
return null != ed ? eo.string = ed : eo.string = se(et)
|
||||
}
|
||||
|
||||
function sy(ee) {
|
||||
if ("string" == typeof ee)
|
||||
return ee;
|
||||
if ("number" == typeof ee || "boolean" == typeof ee || "bigint" == typeof ee)
|
||||
return ee.toString();
|
||||
if (null == ee)
|
||||
return ee + "";
|
||||
throw Error("Invalid property type for record", typeof ee)
|
||||
}
|
||||
|
||||
r9 = function (ee) {
|
||||
return atob(rW(ee))
|
||||
}
|
||||
rW = function (ee) {
|
||||
return ee.replace(/[^A-Za-z0-9\+\/]/g, "")
|
||||
}
|
||||
aW = {
|
||||
"useRecords": false,
|
||||
"mapsAsObjects": true
|
||||
}
|
||||
aJ = false
|
||||
eP = []
|
||||
|
||||
function a3(ee) {
|
||||
if (!aW.trusted && !aJ) {
|
||||
var et, en, eo = eP.sharedLength || 0;
|
||||
eo < eP.length && (eP.length = eo)
|
||||
}
|
||||
if (aW.randomAccessStructure && eI[aG] < 64 && eI[aG] >= 32 && ej ? (et = ej(eI, aG, eO, aW),
|
||||
eI = null,
|
||||
!(ee && ee.lazy) && et && (et = et.toJSON()),
|
||||
aG = eO) : et = a5(),
|
||||
eD && (aG = eD.postBundlePosition,
|
||||
eD = null),
|
||||
aJ && (eP.restoreStructures = null),
|
||||
aG == eO)
|
||||
eP && eP.restoreStructures && a4(),
|
||||
eP = null,
|
||||
eI = null,
|
||||
eL && (eL = null);
|
||||
else if (aG > eO)
|
||||
throw Error("Unexpected end of MessagePack data");
|
||||
else if (!aJ) {
|
||||
try {
|
||||
en = JSON.stringify(et, function (ee, et) {
|
||||
return "bigint" == typeof et ? "".concat(et, "n") : et
|
||||
}).slice(0, 100)
|
||||
} catch (ee) {
|
||||
en = "(JSON view not available " + ee + ")"
|
||||
}
|
||||
throw Error("Data read, but end of buffer not reached " + en)
|
||||
}
|
||||
return et
|
||||
|
||||
}
|
||||
|
||||
function so(ee) {
|
||||
if (ee < 16 && (et = su(ee)))
|
||||
return et;
|
||||
if (ee > 64 && eM)
|
||||
return eM.decode(eI.subarray(aG, aG += ee));
|
||||
var et, en = aG + ee, eo = [];
|
||||
for (et = ""; aG < en;) {
|
||||
var ei = eI[aG++];
|
||||
if ((128 & ei) === 0)
|
||||
eo.push(ei);
|
||||
else if ((224 & ei) === 192) {
|
||||
var ec = 63 & eI[aG++];
|
||||
eo.push((31 & ei) << 6 | ec)
|
||||
} else if ((240 & ei) === 224) {
|
||||
var eu = 63 & eI[aG++]
|
||||
, ed = 63 & eI[aG++];
|
||||
eo.push((31 & ei) << 12 | eu << 6 | ed)
|
||||
} else if ((248 & ei) === 240) {
|
||||
var ef = (7 & ei) << 18 | (63 & eI[aG++]) << 12 | (63 & eI[aG++]) << 6 | 63 & eI[aG++];
|
||||
ef > 65535 && (ef -= 65536,
|
||||
eo.push(ef >>> 10 & 1023 | 55296),
|
||||
ef = 56320 | 1023 & ef),
|
||||
eo.push(ef)
|
||||
} else
|
||||
eo.push(ei);
|
||||
eo.length >= 4096 && (et += sc.apply(String, eo),
|
||||
eo.length = 0)
|
||||
}
|
||||
return eo.length > 0 && (et += sc.apply(String, eo)),
|
||||
et
|
||||
}
|
||||
|
||||
se = so;
|
||||
var sc = String.fromCharCode;
|
||||
|
||||
function sl(ee) {
|
||||
for (var et = aG, en = Array(ee), eo = 0; eo < ee; eo++) {
|
||||
var ei = eI[aG++];
|
||||
if ((128 & ei) > 0) {
|
||||
aG = et;
|
||||
return
|
||||
}
|
||||
en[eo] = ei
|
||||
}
|
||||
return sc.apply(String, en)
|
||||
}
|
||||
|
||||
function su(ee) {
|
||||
if (ee < 4) {
|
||||
if (ee < 2) {
|
||||
if (0 === ee)
|
||||
return "";
|
||||
var et = eI[aG++];
|
||||
if ((128 & et) > 1) {
|
||||
aG -= 1;
|
||||
return
|
||||
}
|
||||
return sc(et)
|
||||
}
|
||||
var en = eI[aG++]
|
||||
, eo = eI[aG++];
|
||||
if ((128 & en) > 0 || (128 & eo) > 0) {
|
||||
aG -= 2;
|
||||
return
|
||||
}
|
||||
if (ee < 3)
|
||||
return sc(en, eo);
|
||||
var ei = eI[aG++];
|
||||
if ((128 & ei) > 0) {
|
||||
aG -= 3;
|
||||
return
|
||||
}
|
||||
return sc(en, eo, ei)
|
||||
}
|
||||
var ec = eI[aG++]
|
||||
, eu = eI[aG++]
|
||||
, ed = eI[aG++]
|
||||
, ef = eI[aG++];
|
||||
if ((128 & ec) > 0 || (128 & eu) > 0 || (128 & ed) > 0 || (128 & ef) > 0) {
|
||||
aG -= 4;
|
||||
return
|
||||
}
|
||||
if (ee < 6) {
|
||||
if (4 === ee)
|
||||
return sc(ec, eu, ed, ef);
|
||||
var ep = eI[aG++];
|
||||
if ((128 & ep) > 0) {
|
||||
aG -= 5;
|
||||
return
|
||||
}
|
||||
return sc(ec, eu, ed, ef, ep)
|
||||
}
|
||||
if (ee < 8) {
|
||||
var em = eI[aG++]
|
||||
, eh = eI[aG++];
|
||||
if ((128 & em) > 0 || (128 & eh) > 0) {
|
||||
aG -= 6;
|
||||
return
|
||||
}
|
||||
if (ee < 7)
|
||||
return sc(ec, eu, ed, ef, em, eh);
|
||||
var ev = eI[aG++];
|
||||
if ((128 & ev) > 0) {
|
||||
aG -= 7;
|
||||
return
|
||||
}
|
||||
return sc(ec, eu, ed, ef, em, eh, ev)
|
||||
}
|
||||
var eg = eI[aG++]
|
||||
, eC = eI[aG++]
|
||||
, eS = eI[aG++]
|
||||
, ew = eI[aG++];
|
||||
if ((128 & eg) > 0 || (128 & eC) > 0 || (128 & eS) > 0 || (128 & ew) > 0) {
|
||||
aG -= 8;
|
||||
return
|
||||
}
|
||||
if (ee < 10) {
|
||||
if (8 === ee)
|
||||
return sc(ec, eu, ed, ef, eg, eC, eS, ew);
|
||||
var eE = eI[aG++];
|
||||
if ((128 & eE) > 0) {
|
||||
aG -= 9;
|
||||
return
|
||||
}
|
||||
return sc(ec, eu, ed, ef, eg, eC, eS, ew, eE)
|
||||
}
|
||||
if (ee < 12) {
|
||||
var ek = eI[aG++]
|
||||
, e_ = eI[aG++];
|
||||
if ((128 & ek) > 0 || (128 & e_) > 0) {
|
||||
aG -= 10;
|
||||
return
|
||||
}
|
||||
if (ee < 11)
|
||||
return sc(ec, eu, ed, ef, eg, eC, eS, ew, ek, e_);
|
||||
var eT = eI[aG++];
|
||||
if ((128 & eT) > 0) {
|
||||
aG -= 11;
|
||||
return
|
||||
}
|
||||
return sc(ec, eu, ed, ef, eg, eC, eS, ew, ek, e_, eT)
|
||||
}
|
||||
var eN = eI[aG++]
|
||||
, eM = eI[aG++]
|
||||
, eO = eI[aG++]
|
||||
, eP = eI[aG++];
|
||||
if ((128 & eN) > 0 || (128 & eM) > 0 || (128 & eO) > 0 || (128 & eP) > 0) {
|
||||
aG -= 12;
|
||||
return
|
||||
}
|
||||
if (ee < 14) {
|
||||
if (12 === ee)
|
||||
return sc(ec, eu, ed, ef, eg, eC, eS, ew, eN, eM, eO, eP);
|
||||
var eR = eI[aG++];
|
||||
if ((128 & eR) > 0) {
|
||||
aG -= 13;
|
||||
return
|
||||
}
|
||||
return sc(ec, eu, ed, ef, eg, eC, eS, ew, eN, eM, eO, eP, eR)
|
||||
}
|
||||
var eD = eI[aG++]
|
||||
, eL = eI[aG++];
|
||||
if ((128 & eD) > 0 || (128 & eL) > 0) {
|
||||
aG -= 14;
|
||||
return
|
||||
}
|
||||
if (ee < 15)
|
||||
return sc(ec, eu, ed, ef, eg, eC, eS, ew, eN, eM, eO, eP, eD, eL);
|
||||
var eZ = eI[aG++];
|
||||
if ((128 & eZ) > 0) {
|
||||
aG -= 15;
|
||||
return
|
||||
}
|
||||
return sc(ec, eu, ed, ef, eg, eC, eS, ew, eN, eM, eO, eP, eD, eL, eZ)
|
||||
}
|
||||
|
||||
function ss(ee) {
|
||||
if (aW.mapsAsObjects) {
|
||||
for (var et = {}, en = 0; en < ee; en++) {
|
||||
var eo = sg();
|
||||
"__proto__" === eo && (eo = "__proto_"),
|
||||
et[eo] = a5()
|
||||
}
|
||||
return et
|
||||
}
|
||||
for (var ei = new Map, ec = 0; ec < ee; ec++)
|
||||
ei.set(a5(), a5());
|
||||
return ei
|
||||
}
|
||||
|
||||
function a5() {
|
||||
var ee, et = eI[aG++];
|
||||
if (et < 160) {
|
||||
if (et < 128) {
|
||||
if (et < 64)
|
||||
return et;
|
||||
var en = eP[63 & et] || aW.getStructures && a9()[63 & et];
|
||||
return en ? (en.read || (en.read = a8(en, 63 & et)),
|
||||
en.read()) : et
|
||||
}
|
||||
if (et < 144) {
|
||||
if (et -= 128,
|
||||
aW.mapsAsObjects) {
|
||||
for (var eo = {}, ei = 0; ei < et; ei++) {
|
||||
var ec = sg();
|
||||
"__proto__" === ec && (ec = "__proto_"),
|
||||
eo[ec] = a5()
|
||||
}
|
||||
return eo
|
||||
}
|
||||
for (var eu = new Map, ed = 0; ed < et; ed++)
|
||||
eu.set(a5(), a5());
|
||||
return eu
|
||||
}
|
||||
for (var ef = Array(et -= 144), ep = 0; ep < et; ep++)
|
||||
ef[ep] = a5();
|
||||
return aW.freezeData ? Object.freeze(ef) : ef
|
||||
}
|
||||
if (et < 192) {
|
||||
var em = et - 160;
|
||||
if (aX >= aG)
|
||||
return eR.slice(aG - aq, (aG += em) - aq);
|
||||
if (0 == aX && eO < 140) {
|
||||
var eh = em < 16 ? su(em) : sl(em);
|
||||
if (null != eh)
|
||||
return eh
|
||||
}
|
||||
return se(em)
|
||||
}
|
||||
switch (et) {
|
||||
case 192:
|
||||
return null;
|
||||
case 193:
|
||||
if (eD) {
|
||||
if ((ee = a5()) > 0)
|
||||
return eD[1].slice(eD.position1, eD.position1 += ee);
|
||||
return eD[0].slice(eD.position0, eD.position0 -= ee)
|
||||
}
|
||||
return aY;
|
||||
case 194:
|
||||
return !1;
|
||||
case 195:
|
||||
return !0;
|
||||
case 196:
|
||||
if (void 0 === (ee = eI[aG++]))
|
||||
throw Error("Unexpected end of buffer");
|
||||
return sm(ee);
|
||||
case 197:
|
||||
return ee = eZ.getUint16(aG),
|
||||
aG += 2,
|
||||
sm(ee);
|
||||
case 198:
|
||||
return ee = eZ.getUint32(aG),
|
||||
aG += 4,
|
||||
sm(ee);
|
||||
case 199:
|
||||
return sh(eI[aG++]);
|
||||
case 200:
|
||||
return ee = eZ.getUint16(aG),
|
||||
aG += 2,
|
||||
sh(ee);
|
||||
case 201:
|
||||
return ee = eZ.getUint32(aG),
|
||||
aG += 4,
|
||||
sh(ee);
|
||||
case 202:
|
||||
if (ee = eZ.getFloat32(aG),
|
||||
aW.useFloat32 > 2) {
|
||||
var ev = sT[(127 & eI[aG]) << 1 | eI[aG + 1] >> 7];
|
||||
return aG += 4,
|
||||
(ev * ee + (ee > 0 ? .5 : -.5) >> 0) / ev
|
||||
}
|
||||
return aG += 4,
|
||||
ee;
|
||||
case 203:
|
||||
return ee = eZ.getFloat64(aG),
|
||||
aG += 8,
|
||||
ee;
|
||||
case 204:
|
||||
return eI[aG++];
|
||||
case 205:
|
||||
return ee = eZ.getUint16(aG),
|
||||
aG += 2,
|
||||
ee;
|
||||
case 206:
|
||||
return ee = eZ.getUint32(aG),
|
||||
aG += 4,
|
||||
ee;
|
||||
case 207:
|
||||
return "number" === aW.int64AsType ? ee = 4294967296 * eZ.getUint32(aG) + eZ.getUint32(aG + 4) : "string" === aW.int64AsType ? ee = eZ.getBigUint64(aG).toString() : "auto" === aW.int64AsType ? (ee = eZ.getBigUint64(aG)) <= BigInt(2) << BigInt(52) && (ee = Number(ee)) : ee = eZ.getBigUint64(aG),
|
||||
aG += 8,
|
||||
ee;
|
||||
case 208:
|
||||
return eZ.getInt8(aG++);
|
||||
case 209:
|
||||
return ee = eZ.getInt16(aG),
|
||||
aG += 2,
|
||||
ee;
|
||||
case 210:
|
||||
return ee = eZ.getInt32(aG),
|
||||
aG += 4,
|
||||
ee;
|
||||
case 211:
|
||||
return "number" === aW.int64AsType ? ee = 4294967296 * eZ.getInt32(aG) + eZ.getUint32(aG + 4) : "string" === aW.int64AsType ? ee = eZ.getBigInt64(aG).toString() : "auto" === aW.int64AsType ? (ee = eZ.getBigInt64(aG)) >= BigInt(-2) << BigInt(52) && ee <= BigInt(2) << BigInt(52) && (ee = Number(ee)) : ee = eZ.getBigInt64(aG),
|
||||
aG += 8,
|
||||
ee;
|
||||
case 212:
|
||||
if (114 == (ee = eI[aG++]))
|
||||
return sC(63 & eI[aG++]);
|
||||
var eg = aK[ee];
|
||||
if (eg) {
|
||||
if (eg.read)
|
||||
return aG++,
|
||||
eg.read(a5());
|
||||
if (eg.noBuffer)
|
||||
return aG++,
|
||||
eg();
|
||||
return eg(eI.subarray(aG, ++aG))
|
||||
}
|
||||
throw Error("Unknown extension " + ee);
|
||||
case 213:
|
||||
if (114 == (ee = eI[aG]))
|
||||
return aG++,
|
||||
sC(63 & eI[aG++], eI[aG++]);
|
||||
return sh(2);
|
||||
case 214:
|
||||
return sh(4);
|
||||
case 215:
|
||||
return sh(8);
|
||||
case 216:
|
||||
return sh(16);
|
||||
case 217:
|
||||
if (ee = eI[aG++],
|
||||
aX >= aG)
|
||||
return eR.slice(aG - aq, (aG += ee) - aq);
|
||||
return st(ee);
|
||||
case 218:
|
||||
if (ee = eZ.getUint16(aG),
|
||||
aG += 2,
|
||||
aX >= aG)
|
||||
return eR.slice(aG - aq, (aG += ee) - aq);
|
||||
return sn(ee);
|
||||
case 219:
|
||||
if (ee = eZ.getUint32(aG),
|
||||
aG += 4,
|
||||
aX >= aG)
|
||||
return eR.slice(aG - aq, (aG += ee) - aq);
|
||||
return sr(ee);
|
||||
case 220:
|
||||
return ee = eZ.getUint16(aG),
|
||||
aG += 2,
|
||||
si(ee);
|
||||
case 221:
|
||||
return ee = eZ.getUint32(aG),
|
||||
aG += 4,
|
||||
si(ee);
|
||||
case 222:
|
||||
return ee = eZ.getUint16(aG),
|
||||
aG += 2,
|
||||
ss(ee);
|
||||
case 223:
|
||||
return ee = eZ.getUint32(aG),
|
||||
aG += 4,
|
||||
ss(ee);
|
||||
default:
|
||||
if (et >= 224)
|
||||
return et - 256;
|
||||
if (void 0 === et) {
|
||||
var eC = Error("Unexpected end of MessagePack data");
|
||||
throw eC.incomplete = !0,
|
||||
eC
|
||||
}
|
||||
throw Error("Unknown MessagePack token " + et)
|
||||
}
|
||||
}
|
||||
|
||||
sn = so
|
||||
var a6 = /^[a-zA-Z_$][a-zA-Z\d_$]*$/;
|
||||
const decrypt = (ee) => {
|
||||
for (var et, en, eo = r9(ee), ei = eo.length, ec = new Uint8Array(ei), eu = 0; eu < ei; eu++) {
|
||||
var ed = eo.charCodeAt(eu);
|
||||
ec[eu] = ed
|
||||
}
|
||||
en = {}
|
||||
et = ec;
|
||||
et.buffer || et.constructor !== ArrayBuffer || (et = "undefined" != typeof Buffer ? Buffer.from(et) : new Uint8Array(et)),
|
||||
"object" == typeof en ? (eO = en.end || et.length,
|
||||
aG = en.start || 0) : (aG = 0,
|
||||
eO = en > -1 ? en : et.length),
|
||||
aX = 0,
|
||||
eR = null,
|
||||
eD = null,
|
||||
eI = et;
|
||||
eM = new TextDecoder("utf-8");
|
||||
eL = null
|
||||
|
||||
eP = [];
|
||||
eZ = et.dataView || (et.dataView = new DataView(et.buffer, et.byteOffset, et.byteLength))
|
||||
res = a3(en)
|
||||
// ec = ecc;
|
||||
// rA = 0;
|
||||
// rB = 0;
|
||||
// ed = [];
|
||||
// eu = ec.length;
|
||||
// rj = {
|
||||
// "useRecords": false,
|
||||
// "mapsAsObjects": true
|
||||
// }
|
||||
// // ei 是 TextDecoder
|
||||
// ei = new TextDecoder("utf-8");
|
||||
// eh = new DataView(ec.buffer, ec.byteOffset, ec.byteLength);
|
||||
// ec.dataView = eh;
|
||||
// let res = rK()
|
||||
const replacer = (key, value) => {
|
||||
if (typeof value === 'bigint') {
|
||||
return value.toString();
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
res = JSON.stringify(res, replacer);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
// console.log(generate_mid())
|
||||
// console.log(generate_uuid())
|
||||
// console.log(generate_device_id())
|
||||
// console.log(generate_sign('5f65b00f83994987e334f97360d69557', '{"sessionTypes":"1,19"}'))
|
||||
// let msg = "ggGLAYEBtTIyMDI2NDA5MTgwNzlAZ29vZmlzaAKzNDc4MTI4NzAwMDBAZ29vZmlzaAOxMzQwMjM5MTQ3MjUwMy5QTk0EAAXPAAABlYW04bIGggFlA4UBoAKjMTExA6AEAQXaADR7ImF0VXNlcnMiOltdLCJjb250ZW50VHlwZSI6MSwidGV4dCI6eyJ0ZXh0IjoiMTExIn19BwIIAQkACoupX3BsYXRmb3Jtp2FuZHJvaWSmYml6VGFn2gBBeyJzb3VyY2VJZCI6IlM6MSIsIm1lc3NhZ2VJZCI6ImYzNjkwMmVmZjQ1NDQ1YmRiMmQxYjBmZDE2OGY4MjY0In2sZGV0YWlsTm90aWNlozExMadleHRKc29u2gBLeyJxdWlja1JlcGx5IjoiMSIsIm1lc3NhZ2VJZCI6ImYzNjkwMmVmZjQ1NDQ1YmRiMmQxYjBmZDE2OGY4MjY0IiwidGFnIjoidSJ9r3JlbWluZGVyQ29udGVudKMxMTGucmVtaW5kZXJOb3RpY2W15Y+R5p2l5LiA5p2h5paw5raI5oGvrXJlbWluZGVyVGl0bGWmc2hh5L+uq3JlbWluZGVyVXJs2gCbZmxlYW1hcmtldDovL21lc3NhZ2VfY2hhdD9pdGVtSWQ9ODk3NzQyNzQ4MDExJnBlZXJVc2VySWQ9MjIwMjY0MDkxODA3OSZwZWVyVXNlck5pY2s9dCoqKjEmc2lkPTQ3ODEyODcwMDAwJm1lc3NhZ2VJZD1mMzY5MDJlZmY0NTQ0NWJkYjJkMWIwZmQxNjhmODI2NCZhZHY9bm+sc2VuZGVyVXNlcklkrTIyMDI2NDA5MTgwNzmuc2VuZGVyVXNlclR5cGWhMKtzZXNzaW9uVHlwZaExDAEDgahuZWVkUHVzaKR0cnVl";
|
||||
//
|
||||
// msg = "ggGLAYEBsjMxNDk2MzcwNjNAZ29vZmlzaAKzNDc5ODMzODkwOTZAZ29vZmlzaAOxMzQxNjU2NTI3NDU0Mi5QTk0EAAXPAAABlbKji20GggFlA4UBoAK6W+aIkeW3suaLjeS4i++8jOW+heS7mOasvl0DoAQaBdoEKnsiY29udGVudFR5cGUiOjI2LCJkeENhcmQiOnsiaXRlbSI6eyJtYWluIjp7ImNsaWNrUGFyYW0iOnsiYXJnMSI6Ik1zZ0NhcmQiLCJhcmdzIjp7InNvdXJjZSI6ImltIiwidGFza19pZCI6IjNleFFKSE9UbVBVMSIsIm1zZ19pZCI6ImNjOGJjMmRmN2M5MzRkZjA4NmUwNTY3Y2I2OWYxNTczIn19LCJleENvbnRlbnQiOnsiYmdDb2xvciI6IiNGRkZGRkYiLCJidXR0b24iOnsiYmdDb2xvciI6IiNGRkU2MEYiLCJib3JkZXJDb2xvciI6IiNGRkU2MEYiLCJjbGlja1BhcmFtIjp7ImFyZzEiOiJNc2dDYXJkQWN0aW9uIiwiYXJncyI6eyJzb3VyY2UiOiJpbSIsInRhc2tfaWQiOiIzZXhRSkhPVG1QVTEiLCJtc2dfaWQiOiJjYzhiYzJkZjdjOTM0ZGYwODZlMDU2N2NiNjlmMTU3MyJ9fSwiZm9udENvbG9yIjoiIzMzMzMzMyIsInRhcmdldFVybCI6ImZsZWFtYXJrZXQ6Ly9hZGp1c3RfcHJpY2U/Zmx1dHRlcj10cnVlJmJpek9yZGVySWQ9MjUwMzY4ODEyNjM1NjYzNjM3MCIsInRleHQiOiLkv67mlLnku7fmoLwifSwiZGVzYyI6Iuivt+WPjOaWueayn+mAmuWPiuaXtuehruiupOS7t+agvCIsImRlc2NDb2xvciI6IiNBM0EzQTMiLCJ0aXRsZSI6IuaIkeW3suaLjeS4i++8jOW+heS7mOasviIsInVwZ3JhZGUiOnsidGFyZ2V0VXJsIjoiaHR0cHM6Ly9oNS5tLmdvb2Zpc2guY29tL2FwcC9pZGxlRmlzaC1GMmUvZm0tZG93bmxhb2QvaG9tZS5odG1sP25vUmVkcmllY3Q9dHJ1ZSZjYW5CYWNrPXRydWUmY2hlY2tWZXJzaW9uPXRydWUiLCJ2ZXJzaW9uIjoiNy43LjkwIn19LCJ0YXJnZXRVcmwiOiJmbGVhbWFya2V0Oi8vb3JkZXJfZGV0YWlsP2lkPTI1MDM2ODgxMjYzNTY2MzYzNzAmcm9sZT1zZWxsZXIifX0sInRlbXBsYXRlIjp7Im5hbWUiOiJpZGxlZmlzaF9tZXNzYWdlX3RyYWRlX2NoYXRfY2FyZCIsInVybCI6Imh0dHBzOi8vZGluYW1pY3guYWxpYmFiYXVzZXJjb250ZW50LmNvbS9wdWIvaWRsZWZpc2hfbWVzc2FnZV90cmFkZV9jaGF0X2NhcmQvMTY2NzIyMjA1Mjc2Ny9pZGxlZmlzaF9tZXNzYWdlX3RyYWRlX2NoYXRfY2FyZC56aXAiLCJ2ZXJzaW9uIjoiMTY2NzIyMjA1Mjc2NyJ9fX0HAQgBCQAK3gAQpmJpelRhZ9oAe3sic291cmNlSWQiOiJDMkM6M2V4UUpIT1RtUFUxIiwidGFza05hbWUiOiLlt7Lmi43kuItf5pyq5LuY5qy+X+WNluWutiIsIm1hdGVyaWFsSWQiOiIzZXhRSkhPVG1QVTEiLCJ0YXNrSWQiOiIzZXhRSkhPVG1QVTEifbFjbG9zZVB1c2hSZWNlaXZlcqVmYWxzZbFjbG9zZVVucmVhZE51bWJlcqVmYWxzZaxkZXRhaWxOb3RpY2W6W+aIkeW3suaLjeS4i++8jOW+heS7mOasvl2nZXh0SnNvbtoBr3sibXNnQXJncyI6eyJ0YXNrX2lkIjoiM2V4UUpIT1RtUFUxIiwic291cmNlIjoiaW0iLCJtc2dfaWQiOiJjYzhiYzJkZjdjOTM0ZGYwODZlMDU2N2NiNjlmMTU3MyJ9LCJxdWlja1JlcGx5IjoiMSIsIm1zZ0FyZzEiOiJNc2dDYXJkIiwidXBkYXRlS2V5IjoiNDc5ODMzODkwOTY6MjUwMzY4ODEyNjM1NjYzNjM3MDoxX25vdF9wYXlfc2VsbGVyIiwibWVzc2FnZUlkIjoiY2M4YmMyZGY3YzkzNGRmMDg2ZTA1NjdjYjY5ZjE1NzMiLCJtdWx0aUNoYW5uZWwiOnsiaHVhd2VpIjoiRVhQUkVTUyIsInhpYW9taSI6IjEwODAwMCIsIm9wcG8iOiJFWFBSRVNTIiwiaG9ub3IiOiJOT1JNQUwiLCJhZ29vIjoicHJvZHVjdCIsInZpdm8iOiJPUkRFUiJ9LCJjb250ZW50VHlwZSI6IjI2IiwiY29ycmVsYXRpb25Hcm91cElkIjoiM2V4UUpIT1RtUFUxX0ZGcjRHT1NuOE9RbyJ9qHJlY2VpdmVyrTIyMDI2NDA5MTgwNzmrcmVkUmVtaW5kZXKy562J5b6F5Lmw5a625LuY5qy+sHJlZFJlbWluZGVyU3R5bGWhMa9yZW1pbmRlckNvbnRlbnS6W+aIkeW3suaLjeS4i++8jOW+heS7mOasvl2ucmVtaW5kZXJOb3RpY2W75Lmw5a625bey5ouN5LiL77yM5b6F5LuY5qy+rXJlbWluZGVyVGl0bGW75Lmw5a625bey5ouN5LiL77yM5b6F5LuY5qy+q3JlbWluZGVyVXJs2gCaZmxlYW1hcmtldDovL21lc3NhZ2VfY2hhdD9pdGVtSWQ9OTAwMDUyNjQ0Mjc3JnBlZXJVc2VySWQ9MzE0OTYzNzA2MyZwZWVyVXNlck5pY2s955S3KioqeSZzaWQ9NDc5ODMzODkwOTYmbWVzc2FnZUlkPWNjOGJjMmRmN2M5MzRkZjA4NmUwNTY3Y2I2OWYxNTczJmFkdj1ub6xzZW5kZXJVc2VySWSqMzE0OTYzNzA2M65zZW5kZXJVc2VyVHlwZaEwq3Nlc3Npb25UeXBloTGqdXBkYXRlSGVhZKR0cnVlDAEDgahuZWVkUHVzaKR0cnVl"
|
||||
// msg = "hAGzNDc5ODMzODkwOTZAZ29vZmlzaAIBA4KrcmVkUmVtaW5kZXKy562J5b6F5Lmw5a625LuY5qy+sHJlZFJlbWluZGVyU3R5bGWhMQTPAAABlbMlNng="
|
||||
// res = decrypt(msg);
|
||||
// console.log(res);
|
27
test_fix.py
Normal file
27
test_fix.py
Normal file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试修复
|
||||
"""
|
||||
|
||||
def test_imports():
|
||||
print("🧪 测试导入修复")
|
||||
|
||||
try:
|
||||
from file_log_collector import setup_file_logging, get_file_log_collector
|
||||
print("✅ file_log_collector 导入成功")
|
||||
|
||||
# 测试初始化
|
||||
collector = setup_file_logging()
|
||||
print("✅ 文件日志收集器初始化成功")
|
||||
|
||||
# 生成测试日志
|
||||
from loguru import logger
|
||||
logger.info("测试日志修复")
|
||||
|
||||
print("✅ 所有导入和初始化都正常")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 导入失败: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_imports()
|
29
utils/message_utils.py
Normal file
29
utils/message_utils.py
Normal file
@ -0,0 +1,29 @@
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
|
||||
def format_message(message_data: Dict[str, Any], is_outgoing: bool = False, is_manual: bool = False) -> str:
|
||||
"""格式化消息输出"""
|
||||
try:
|
||||
# 获取消息内容
|
||||
content = message_data.get('content', '')
|
||||
if not content:
|
||||
return ''
|
||||
|
||||
# 获取发送时间
|
||||
timestamp = message_data.get('time', time.time() * 1000)
|
||||
time_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp / 1000))
|
||||
|
||||
# 确定消息方向
|
||||
direction = '【发出】' if is_outgoing else '【收到】'
|
||||
if is_manual:
|
||||
direction = '【手动发出】'
|
||||
|
||||
# 格式化输出
|
||||
return f"{time_str} {direction} {content}"
|
||||
except Exception as e:
|
||||
return f"消息格式化错误: {str(e)}"
|
||||
|
||||
def format_system_message(message: str) -> str:
|
||||
"""格式化系统消息输出"""
|
||||
time_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
|
||||
return f"{time_str} 【系统】 {message}"
|
89
utils/ws_utils.py
Normal file
89
utils/ws_utils.py
Normal file
@ -0,0 +1,89 @@
|
||||
import asyncio
|
||||
import websockets
|
||||
from typing import Optional, Dict, Any, Callable
|
||||
from loguru import logger
|
||||
|
||||
class WebSocketClient:
|
||||
def __init__(self, url: str, headers: Dict[str, str], on_message: Callable[[Dict[str, Any]], None]):
|
||||
"""初始化WebSocket客户端"""
|
||||
self.url = url
|
||||
self.headers = headers
|
||||
self.on_message = on_message
|
||||
self.websocket: Optional[websockets.WebSocketClientProtocol] = None
|
||||
self.is_connected = False
|
||||
self.reconnect_delay = 5 # 重连延迟,单位秒
|
||||
|
||||
async def connect(self):
|
||||
"""建立WebSocket连接"""
|
||||
try:
|
||||
self.websocket = await websockets.connect(
|
||||
self.url,
|
||||
extra_headers=self.headers,
|
||||
ping_interval=None,
|
||||
ping_timeout=None
|
||||
)
|
||||
self.is_connected = True
|
||||
logger.info("WebSocket连接建立成功")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket连接失败: {e}")
|
||||
return False
|
||||
|
||||
async def disconnect(self):
|
||||
"""关闭WebSocket连接"""
|
||||
if self.websocket:
|
||||
await self.websocket.close()
|
||||
self.is_connected = False
|
||||
logger.info("WebSocket连接已关闭")
|
||||
|
||||
async def send(self, message: str):
|
||||
"""发送消息"""
|
||||
if not self.is_connected:
|
||||
logger.warning("WebSocket未连接,无法发送消息")
|
||||
return False
|
||||
|
||||
try:
|
||||
await self.websocket.send(message)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"消息发送失败: {e}")
|
||||
self.is_connected = False
|
||||
return False
|
||||
|
||||
async def receive(self):
|
||||
"""接收消息"""
|
||||
if not self.is_connected:
|
||||
logger.warning("WebSocket未连接,无法接收消息")
|
||||
return None
|
||||
|
||||
try:
|
||||
message = await self.websocket.recv()
|
||||
return message
|
||||
except Exception as e:
|
||||
logger.error(f"消息接收失败: {e}")
|
||||
self.is_connected = False
|
||||
return None
|
||||
|
||||
async def reconnect(self):
|
||||
"""重新连接"""
|
||||
logger.info(f"准备在{self.reconnect_delay}秒后重新连接...")
|
||||
await asyncio.sleep(self.reconnect_delay)
|
||||
return await self.connect()
|
||||
|
||||
async def run(self):
|
||||
"""运行WebSocket客户端"""
|
||||
while True:
|
||||
if not self.is_connected:
|
||||
success = await self.connect()
|
||||
if not success:
|
||||
await self.reconnect()
|
||||
continue
|
||||
|
||||
try:
|
||||
message = await self.receive()
|
||||
if message:
|
||||
await self.on_message(message)
|
||||
except Exception as e:
|
||||
logger.error(f"消息处理失败: {e}")
|
||||
await self.disconnect()
|
||||
await self.reconnect()
|
332
utils/xianyu_utils.py
Normal file
332
utils/xianyu_utils.py
Normal file
@ -0,0 +1,332 @@
|
||||
import base64
|
||||
import json
|
||||
import subprocess
|
||||
from functools import partial
|
||||
import time
|
||||
import hashlib
|
||||
import struct
|
||||
import os
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import blackboxprotobuf
|
||||
|
||||
subprocess.Popen = partial(subprocess.Popen, encoding="utf-8")
|
||||
import execjs
|
||||
|
||||
def get_js_path():
|
||||
"""获取JavaScript文件的路径"""
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
root_dir = os.path.dirname(current_dir)
|
||||
js_path = os.path.join(root_dir, 'static', 'xianyu_js_version_2.js')
|
||||
return js_path
|
||||
|
||||
try:
|
||||
xianyu_js = execjs.compile(open(get_js_path(), 'r', encoding='utf-8').read())
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"无法加载JavaScript文件: {e}")
|
||||
|
||||
def trans_cookies(cookies_str: str) -> dict:
|
||||
"""将cookies字符串转换为字典"""
|
||||
if not cookies_str:
|
||||
raise ValueError("cookies不能为空")
|
||||
|
||||
cookies = {}
|
||||
for cookie in cookies_str.split("; "):
|
||||
if "=" in cookie:
|
||||
key, value = cookie.split("=", 1)
|
||||
cookies[key] = value
|
||||
return cookies
|
||||
|
||||
|
||||
def generate_mid() -> str:
|
||||
"""生成mid"""
|
||||
import random
|
||||
random_part = int(1000 * random.random())
|
||||
timestamp = int(time.time() * 1000)
|
||||
return f"{random_part}{timestamp} 0"
|
||||
|
||||
|
||||
def generate_uuid() -> str:
|
||||
"""生成uuid"""
|
||||
timestamp = int(time.time() * 1000)
|
||||
return f"-{timestamp}1"
|
||||
|
||||
|
||||
def generate_device_id(user_id: str) -> str:
|
||||
"""生成设备ID"""
|
||||
import random
|
||||
|
||||
# 字符集
|
||||
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
result = []
|
||||
|
||||
for i in range(36):
|
||||
if i in [8, 13, 18, 23]:
|
||||
result.append("-")
|
||||
elif i == 14:
|
||||
result.append("4")
|
||||
else:
|
||||
if i == 19:
|
||||
# 对于位置19,需要特殊处理
|
||||
rand_val = int(16 * random.random())
|
||||
result.append(chars[(rand_val & 0x3) | 0x8])
|
||||
else:
|
||||
rand_val = int(16 * random.random())
|
||||
result.append(chars[rand_val])
|
||||
|
||||
return ''.join(result) + "-" + user_id
|
||||
|
||||
|
||||
def generate_sign(t: str, token: str, data: str) -> str:
|
||||
"""生成签名"""
|
||||
app_key = "34839810"
|
||||
msg = f"{token}&{t}&{app_key}&{data}"
|
||||
|
||||
# 使用MD5生成签名
|
||||
md5_hash = hashlib.md5()
|
||||
md5_hash.update(msg.encode('utf-8'))
|
||||
return md5_hash.hexdigest()
|
||||
|
||||
|
||||
class MessagePackDecoder:
|
||||
"""MessagePack解码器的纯Python实现"""
|
||||
|
||||
def __init__(self, data: bytes):
|
||||
self.data = data
|
||||
self.pos = 0
|
||||
self.length = len(data)
|
||||
|
||||
def read_byte(self) -> int:
|
||||
if self.pos >= self.length:
|
||||
raise ValueError("Unexpected end of data")
|
||||
byte = self.data[self.pos]
|
||||
self.pos += 1
|
||||
return byte
|
||||
|
||||
def read_bytes(self, count: int) -> bytes:
|
||||
if self.pos + count > self.length:
|
||||
raise ValueError("Unexpected end of data")
|
||||
result = self.data[self.pos:self.pos + count]
|
||||
self.pos += count
|
||||
return result
|
||||
|
||||
def read_uint8(self) -> int:
|
||||
return self.read_byte()
|
||||
|
||||
def read_uint16(self) -> int:
|
||||
return struct.unpack('>H', self.read_bytes(2))[0]
|
||||
|
||||
def read_uint32(self) -> int:
|
||||
return struct.unpack('>I', self.read_bytes(4))[0]
|
||||
|
||||
def read_uint64(self) -> int:
|
||||
return struct.unpack('>Q', self.read_bytes(8))[0]
|
||||
|
||||
def read_int8(self) -> int:
|
||||
return struct.unpack('>b', self.read_bytes(1))[0]
|
||||
|
||||
def read_int16(self) -> int:
|
||||
return struct.unpack('>h', self.read_bytes(2))[0]
|
||||
|
||||
def read_int32(self) -> int:
|
||||
return struct.unpack('>i', self.read_bytes(4))[0]
|
||||
|
||||
def read_int64(self) -> int:
|
||||
return struct.unpack('>q', self.read_bytes(8))[0]
|
||||
|
||||
def read_float32(self) -> float:
|
||||
return struct.unpack('>f', self.read_bytes(4))[0]
|
||||
|
||||
def read_float64(self) -> float:
|
||||
return struct.unpack('>d', self.read_bytes(8))[0]
|
||||
|
||||
def read_string(self, length: int) -> str:
|
||||
return self.read_bytes(length).decode('utf-8')
|
||||
|
||||
def decode_value(self) -> Any:
|
||||
"""解码单个MessagePack值"""
|
||||
if self.pos >= self.length:
|
||||
raise ValueError("Unexpected end of data")
|
||||
|
||||
format_byte = self.read_byte()
|
||||
|
||||
# Positive fixint (0xxxxxxx)
|
||||
if format_byte <= 0x7f:
|
||||
return format_byte
|
||||
|
||||
# Fixmap (1000xxxx)
|
||||
elif 0x80 <= format_byte <= 0x8f:
|
||||
size = format_byte & 0x0f
|
||||
return self.decode_map(size)
|
||||
|
||||
# Fixarray (1001xxxx)
|
||||
elif 0x90 <= format_byte <= 0x9f:
|
||||
size = format_byte & 0x0f
|
||||
return self.decode_array(size)
|
||||
|
||||
# Fixstr (101xxxxx)
|
||||
elif 0xa0 <= format_byte <= 0xbf:
|
||||
size = format_byte & 0x1f
|
||||
return self.read_string(size)
|
||||
|
||||
# nil
|
||||
elif format_byte == 0xc0:
|
||||
return None
|
||||
|
||||
# false
|
||||
elif format_byte == 0xc2:
|
||||
return False
|
||||
|
||||
# true
|
||||
elif format_byte == 0xc3:
|
||||
return True
|
||||
|
||||
# bin 8
|
||||
elif format_byte == 0xc4:
|
||||
size = self.read_uint8()
|
||||
return self.read_bytes(size)
|
||||
|
||||
# bin 16
|
||||
elif format_byte == 0xc5:
|
||||
size = self.read_uint16()
|
||||
return self.read_bytes(size)
|
||||
|
||||
# bin 32
|
||||
elif format_byte == 0xc6:
|
||||
size = self.read_uint32()
|
||||
return self.read_bytes(size)
|
||||
|
||||
# float 32
|
||||
elif format_byte == 0xca:
|
||||
return self.read_float32()
|
||||
|
||||
# float 64
|
||||
elif format_byte == 0xcb:
|
||||
return self.read_float64()
|
||||
|
||||
# uint 8
|
||||
elif format_byte == 0xcc:
|
||||
return self.read_uint8()
|
||||
|
||||
# uint 16
|
||||
elif format_byte == 0xcd:
|
||||
return self.read_uint16()
|
||||
|
||||
# uint 32
|
||||
elif format_byte == 0xce:
|
||||
return self.read_uint32()
|
||||
|
||||
# uint 64
|
||||
elif format_byte == 0xcf:
|
||||
return self.read_uint64()
|
||||
|
||||
# int 8
|
||||
elif format_byte == 0xd0:
|
||||
return self.read_int8()
|
||||
|
||||
# int 16
|
||||
elif format_byte == 0xd1:
|
||||
return self.read_int16()
|
||||
|
||||
# int 32
|
||||
elif format_byte == 0xd2:
|
||||
return self.read_int32()
|
||||
|
||||
# int 64
|
||||
elif format_byte == 0xd3:
|
||||
return self.read_int64()
|
||||
|
||||
# str 8
|
||||
elif format_byte == 0xd9:
|
||||
size = self.read_uint8()
|
||||
return self.read_string(size)
|
||||
|
||||
# str 16
|
||||
elif format_byte == 0xda:
|
||||
size = self.read_uint16()
|
||||
return self.read_string(size)
|
||||
|
||||
# str 32
|
||||
elif format_byte == 0xdb:
|
||||
size = self.read_uint32()
|
||||
return self.read_string(size)
|
||||
|
||||
# array 16
|
||||
elif format_byte == 0xdc:
|
||||
size = self.read_uint16()
|
||||
return self.decode_array(size)
|
||||
|
||||
# array 32
|
||||
elif format_byte == 0xdd:
|
||||
size = self.read_uint32()
|
||||
return self.decode_array(size)
|
||||
|
||||
# map 16
|
||||
elif format_byte == 0xde:
|
||||
size = self.read_uint16()
|
||||
return self.decode_map(size)
|
||||
|
||||
# map 32
|
||||
elif format_byte == 0xdf:
|
||||
size = self.read_uint32()
|
||||
return self.decode_map(size)
|
||||
|
||||
# Negative fixint (111xxxxx)
|
||||
elif format_byte >= 0xe0:
|
||||
return format_byte - 0x100
|
||||
|
||||
raise ValueError(f"Unknown format byte: {format_byte:02x}")
|
||||
|
||||
def decode_array(self, size: int) -> List[Any]:
|
||||
"""解码数组"""
|
||||
return [self.decode_value() for _ in range(size)]
|
||||
|
||||
def decode_map(self, size: int) -> Dict[Any, Any]:
|
||||
"""解码字典"""
|
||||
result = {}
|
||||
for _ in range(size):
|
||||
key = self.decode_value()
|
||||
value = self.decode_value()
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
def decode(self) -> Any:
|
||||
"""解码整个MessagePack数据"""
|
||||
return self.decode_value()
|
||||
|
||||
|
||||
def decrypt(data: str) -> str:
|
||||
"""解密消息数据"""
|
||||
try:
|
||||
# Base64解码
|
||||
decoded_data = base64.b64decode(data)
|
||||
|
||||
# 使用MessagePack解码器解码数据
|
||||
decoder = MessagePackDecoder(decoded_data)
|
||||
decoded_value = decoder.decode()
|
||||
|
||||
# 如果解码后的值是字典,转换为JSON字符串
|
||||
if isinstance(decoded_value, dict):
|
||||
def json_serializer(obj):
|
||||
if isinstance(obj, bytes):
|
||||
return obj.decode('utf-8', errors='ignore')
|
||||
raise TypeError(f"Type {type(obj)} not serializable")
|
||||
|
||||
return json.dumps(decoded_value, default=json_serializer)
|
||||
|
||||
# 如果是其他类型,尝试转换为字符串
|
||||
return str(decoded_value)
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"解密失败: {str(e)}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
# t = 1741667630548
|
||||
# token = 'b7e897bf9767618a32b439c6103fe1cb'
|
||||
# data = '{"appKey":"444e9908a51d1cb236a27862abc769c9","deviceId":"ED4CBA2C-5DA0-4154-A902-BF5CB52409E2-3888777108"}'
|
||||
# print(generate_sign(t, token, data))
|
||||
msg = "ggGLAYEBsjMxNDk2MzcwNjNAZ29vZmlzaAKzNDc5ODMzODkwOTZAZ29vZmlzaAOxMzQxNjU2NTI3NDU0Mi5QTk0EAAXPAAABlbKji20GggFlA4UBoAK6W+aIkeW3suaLjeS4i++8jOW+heS7mOasvl0DoAQaBdoEKnsiY29udGVudFR5cGUiOjI2LCJkeENhcmQiOnsiaXRlbSI6eyJtYWluIjp7ImNsaWNrUGFyYW0iOnsiYXJnMSI6Ik1zZ0NhcmQiLCJhcmdzIjp7InNvdXJjZSI6ImltIiwidGFza19pZCI6IjNleFFKSE9UbVBVMSIsIm1zZ19pZCI6ImNjOGJjMmRmN2M5MzRkZjA4NmUwNTY3Y2I2OWYxNTczIn19LCJleENvbnRlbnQiOnsiYmdDb2xvciI6IiNGRkZGRkYiLCJidXR0b24iOnsiYmdDb2xvciI6IiNGRkU2MEYiLCJib3JkZXJDb2xvciI6IiNGRkU2MEYiLCJjbGlja1BhcmFtIjp7ImFyZzEiOiJNc2dDYXJkQWN0aW9uIiwiYXJncyI6eyJzb3VyY2UiOiJpbSIsInRhc2tfaWQiOiIzZXhRSkhPVG1QVTEiLCJtc2dfaWQiOiJjYzhiYzJkZjdjOTM0ZGYwODZlMDU2N2NiNjlmMTU3MyJ9fSwiZm9udENvbG9yIjoiIzMzMzMzMyIsInRhcmdldFVybCI6ImZsZWFtYXJrZXQ6Ly9hZGp1c3RfcHJpY2U/Zmx1dHRlcj10cnVlJmJpek9yZGVySWQ9MjUwMzY4ODEyNjM1NjYzNjM3MCIsInRleHQiOiLkv67mlLnku7fmoLwifSwiZGVzYyI6Iuivt+WPjOaWueayn+mAmuWPiuaXtuehruiupOS7t+agvCIsImRlc2NDb2xvciI6IiNBM0EzQTMiLCJ0aXRsZSI6IuaIkeW3suaLjeS4i++8jOW+heS7mOasviIsInVwZ3JhZGUiOnsidGFyZ2V0VXJsIjoiaHR0cHM6Ly9oNS5tLmdvb2Zpc2guY29tL2FwcC9pZGxlRmlzaC1GMmUvZm0tZG93bmxhb2QvaG9tZS5odG1sP25vUmVkcmllY3Q9dHJ1ZSZjYW5CYWNrPXRydWUmY2hlY2tWZXJzaW9uPXRydWUiLCJ2ZXJzaW9uIjoiNy43LjkwIn19LCJ0YXJnZXRVcmwiOiJmbGVhbWFya2V0Oi8vb3JkZXJfZGV0YWlsP2lkPTI1MDM2ODgxMjYzNTY2MzYzNzAmcm9sZT1zZWxsZXIifX0sInRlbXBsYXRlIjp7Im5hbWUiOiJpZGxlZmlzaF9tZXNzYWdlX3RyYWRlX2NoYXRfY2FyZCIsInVybCI6Imh0dHBzOi8vZGluYW1pY3guYWxpYmFiYXVzZXJjb250ZW50LmNvbS9wdWIvaWRsZWZpc2hfbWVzc2FnZV90cmFkZV9jaGF0X2NhcmQvMTY2NzIyMjA1Mjc2Ny9pZGxlZmlzaF9tZXNzYWdlX3RyYWRlX2NoYXRfY2FyZC56aXAiLCJ2ZXJzaW9uIjoiMTY2NzIyMjA1Mjc2NyJ9fX0HAQgBCQAK3gAQpmJpelRhZ9oAe3sic291cmNlSWQiOiJDMkM6M2V4UUpIT1RtUFUxIiwidGFza05hbWUiOiLlt7Lmi43kuItf5pyq5LuY5qy+X+WNluWutiIsIm1hdGVyaWFsSWQiOiIzZXhRSkhPVG1QVTEiLCJ0YXNrSWQiOiIzZXhRSkhPVG1QVTEifbFjbG9zZVB1c2hSZWNlaXZlcqVmYWxzZbFjbG9zZVVucmVhZE51bWJlcqVmYWxzZaxkZXRhaWxOb3RpY2W6W+aIkeW3suaLjeS4i++8jOW+heS7mOasvl2nZXh0SnNvbtoBr3sibXNnQXJncyI6eyJ0YXNrX2lkIjoiM2V4UUpIT1RtUFUxIiwic291cmNlIjoiaW0iLCJtc2dfaWQiOiJjYzhiYzJkZjdjOTM0ZGYwODZlMDU2N2NiNjlmMTU3MyJ9LCJxdWlja1JlcGx5IjoiMSIsIm1zZ0FyZzEiOiJNc2dDYXJkIiwidXBkYXRlS2V5IjoiNDc5ODMzODkwOTY6MjUwMzY4ODEyNjM1NjYzNjM3MDoxX25vdF9wYXlfc2VsbGVyIiwibWVzc2FnZUlkIjoiY2M4YmMyZGY3YzkzNGRmMDg2ZTA1NjdjYjY5ZjE1NzMiLCJtdWx0aUNoYW5uZWwiOnsiaHVhd2VpIjoiRVhQUkVTUyIsInhpYW9taSI6IjEwODAwMCIsIm9wcG8iOiJFWFBSRVNTIiwiaG9ub3IiOiJOT1JNQUwiLCJhZ29vIjoicHJvZHVjdCIsInZpdm8iOiJPUkRFUiJ9LCJjb250ZW50VHlwZSI6IjI2IiwiY29ycmVsYXRpb25Hcm91cElkIjoiM2V4UUpIT1RtUFUxX0ZGcjRHT1NuOE9RbyJ9qHJlY2VpdmVyrTIyMDI2NDA5MTgwNzmrcmVkUmVtaW5kZXKy562J5b6F5Lmw5a625LuY5qy+sHJlZFJlbWluZGVyU3R5bGWhMa9yZW1pbmRlckNvbnRlbnS6W+aIkeW3suaLjeS4i++8jOW+heS7mOasvl2ucmVtaW5kZXJOb3RpY2W75Lmw5a625bey5ouN5LiL77yM5b6F5LuY5qy+rXJlbWluZGVyVGl0bGW75Lmw5a625bey5ouN5LiL77yM5b6F5LuY5qy+q3JlbWluZGVyVXJs2gCaZmxlYW1hcmtldDovL21lc3NhZ2VfY2hhdD9pdGVtSWQ9OTAwMDUyNjQ0Mjc3JnBlZXJVc2VySWQ9MzE0OTYzNzA2MyZwZWVyVXNlck5pY2s955S3KioqeSZzaWQ9NDc5ODMzODkwOTYmbWVzc2FnZUlkPWNjOGJjMmRmN2M5MzRkZjA4NmUwNTY3Y2I2OWYxNTczJmFkdj1ub6xzZW5kZXJVc2VySWSqMzE0OTYzNzA2M65zZW5kZXJVc2VyVHlwZaEwq3Nlc3Npb25UeXBloTGqdXBkYXRlSGVhZKR0cnVlDAEDgahuZWVkUHVzaKR0cnVl"
|
||||
msg = "ggGLAYEBsjMxNDk2MzcwNjNAZ29vZmlzaAKzNDc5ODMzODkwOTZAZ29vZmlzaAOxMzQxNjU2NTI3NDU0Mi5QTk0EAAXPAAABlbKji20GggFlA4UBoAK6W+aIkeW3suaLjeS4i++8jOW+heS7mOasvl0DoAQaBdoEKnsiY29udGVudFR5cGUiOjI2LCJkeENhcmQiOnsiaXRlbSI6eyJtYWluIjp7ImNsaWNrUGFyYW0iOnsiYXJnMSI6Ik1zZ0NhcmQiLCJhcmdzIjp7InNvdXJjZSI6ImltIiwidGFza19pZCI6IjNleFFKSE9UbVBVMSIsIm1zZ19pZCI6ImNjOGJjMmRmN2M5MzRkZjA4NmUwNTY3Y2I2OWYxNTczIn19LCJleENvbnRlbnQiOnsiYmdDb2xvciI6IiNGRkZGRkYiLCJidXR0b24iOnsiYmdDb2xvciI6IiNGRkU2MEYiLCJib3JkZXJDb2xvciI6IiNGRkU2MEYiLCJjbGlja1BhcmFtIjp7ImFyZzEiOiJNc2dDYXJkQWN0aW9uIiwiYXJncyI6eyJzb3VyY2UiOiJpbSIsInRhc2tfaWQiOiIzZXhRSkhPVG1QVTEiLCJtc2dfaWQiOiJjYzhiYzJkZjdjOTM0ZGYwODZlMDU2N2NiNjlmMTU3MyJ9fSwiZm9udENvbG9yIjoiIzMzMzMzMyIsInRhcmdldFVybCI6ImZsZWFtYXJrZXQ6Ly9hZGp1c3RfcHJpY2U/Zmx1dHRlcj10cnVlJmJpek9yZGVySWQ9MjUwMzY4ODEyNjM1NjYzNjM3MCIsInRleHQiOiLkv67mlLnku7fmoLwifSwiZGVzYyI6Iuivt+WPjOaWueayn+mAmuWPiuaXtuehruiupOS7t+agvCIsImRlc2NDb2xvciI6IiNBM0EzQTMiLCJ0aXRsZSI6IuaIkeW3suaLjeS4i++8jOW+heS7mOasviIsInVwZ3JhZGUiOnsidGFyZ2V0VXJsIjoiaHR0cHM6Ly9oNS5tLmdvb2Zpc2guY29tL2FwcC9pZGxlRmlzaC1GMmUvZm0tZG93bmxhb2QvaG9tZS5odG1sP25vUmVkcmllY3Q9dHJ1ZSZjYW5CYWNrPXRydWUmY2hlY2tWZXJzaW9uPXRydWUiLCJ2ZXJzaW9uIjoiNy43LjkwIn19LCJ0YXJnZXRVcmwiOiJmbGVhbWFya2V0Oi8vb3JkZXJfZGV0YWlsP2lkPTI1MDM2ODgxMjYzNTY2MzYzNzAmcm9sZT1zZWxsZXIifX0sInRlbXBsYXRlIjp7Im5hbWUiOiJpZGxlZmlzaF9tZXNzYWdlX3RyYWRlX2NoYXRfY2FyZCIsInVybCI6Imh0dHBzOi8vZGluYW1pY3guYWxpYmFiYXVzZXJjb250ZW50LmNvbS9wdWIvaWRsZWZpc2hfbWVzc2FnZV90cmFkZV9jaGF0X2NhcmQvMTY2NzIyMjA1Mjc2Ny9pZGxlZmlzaF9tZXNzYWdlX3RyYWRlX2NoYXRfY2FyZC56aXAiLCJ2ZXJzaW9uIjoiMTY2NzIyMjA1Mjc2NyJ9fX0HAQgBCQAK3gAQpmJpelRhZ9oAe3sic291cmNlSWQiOiJDMkM6M2V4UUpIT1RtUFUxIiwidGFza05hbWUiOiLlt7Lmi43kuItf5pyq5LuY5qy+X+WNluWutiIsIm1hdGVyaWFsSWQiOiIzZXhRSkhPVG1QVTEiLCJ0YXNrSWQiOiIzZXhRSkhPVG1QVTEifbFjbG9zZVB1c2hSZWNlaXZlcqVmYWxzZbFjbG9zZVVucmVhZE51bWJlcqVmYWxzZaxkZXRhaWxOb3RpY2W6W+aIkeW3suaLjeS4i++8jOW+heS7mOasvl2nZXh0SnNvbtoBr3sibXNnQXJncyI6eyJ0YXNrX2lkIjoiM2V4UUpIT1RtUFUxIiwic291cmNlIjoiaW0iLCJtc2dfaWQiOiJjYzhiYzJkZjdjOTM0ZGYwODZlMDU2N2NiNjlmMTU3MyJ9LCJxdWlja1JlcGx5IjoiMSIsIm1zZ0FyZzEiOiJNc2dDYXJkIiwidXBkYXRlS2V5IjoiNDc5ODMzODkwOTY6MjUwMzY4ODEyNjM1NjYzNjM3MDoxX25vdF9wYXlfc2VsbGVyIiwibWVzc2FnZUlkIjoiY2M4YmMyZGY3YzkzNGRmMDg2ZTA1NjdjYjY5ZjE1NzMiLCJtdWx0aUNoYW5uZWwiOnsiaHVhd2VpIjoiRVhQUkVTUyIsInhpYW9taSI6IjEwODAwMCIsIm9wcG8iOiJFWFBSRVNTIiwiaG9ub3IiOiJOT1JNQUwiLCJhZ29vIjoicHJvZHVjdCIsInZpdm8iOiJPUkRFUiJ9LCJjb250ZW50VHlwZSI6IjI2IiwiY29ycmVsYXRpb25Hcm91cElkIjoiM2V4UUpIT1RtUFUxX0ZGcjRHT1NuOE9RbyJ9qHJlY2VpdmVyrTIyMDI2NDA5MTgwNzmrcmVkUmVtaW5kZXKy562J5b6F5Lmw5a625LuY5qy+sHJlZFJlbWluZGVyU3R5bGWhMa9yZW1pbmRlckNvbnRlbnS6W+aIkeW3suaLjeS4i++8jOW+heS7mOasvl2ucmVtaW5kZXJOb3RpY2W75Lmw5a625bey5ouN5LiL77yM5b6F5LuY5qy+rXJlbWluZGVyVGl0bGW75Lmw5a625bey5ouN5LiL77yM5b6F5LuY5qy+q3JlbWluZGVyVXJs2gCaZmxlYW1hcmtldDovL21lc3NhZ2VfY2hhdD9pdGVtSWQ9OTAwMDUyNjQ0Mjc3JnBlZXJVc2VySWQ9MzE0OTYzNzA2MyZwZWVyVXNlck5pY2s955S3KioqeSZzaWQ9NDc5ODMzODkwOTYmbWVzc2FnZUlkPWNjOGJjMmRmN2M5MzRkZjA4NmUwNTY3Y2I2OWYxNTczJmFkdj1ub6xzZW5kZXJVc2VySWSqMzE0OTYzNzA2M65zZW5kZXJVc2VyVHlwZaEwq3Nlc3Npb25UeXBloTGqdXBkYXRlSGVhZKR0cnVlDAEDgahuZWVkUHVzaKR0cnVl"
|
||||
|
||||
res = decrypt(msg)
|
||||
print(res)
|
BIN
xianyu_data.db
Normal file
BIN
xianyu_data.db
Normal file
Binary file not shown.
178
使用说明.md
Normal file
178
使用说明.md
Normal file
@ -0,0 +1,178 @@
|
||||
# 闲鱼自动回复管理系统 - 使用说明
|
||||
|
||||
## 🎉 系统功能概述
|
||||
|
||||
本系统已完全实现您要求的所有功能:
|
||||
|
||||
### ✅ 已实现功能
|
||||
1. **多Cookies支持** - 支持同时管理多个闲鱼账号
|
||||
2. **美观前端界面** - 响应式设计,支持Cookies和关键词的CRUD操作
|
||||
3. **SQLite数据库存储** - 持久化存储Cookies和关键词数据
|
||||
4. **关键词管理** - 每个账号独立的关键词回复设置
|
||||
5. **API接口** - 完整实现 `/xianyu/reply` 接口
|
||||
6. **智能回复** - 根据Cookie ID匹配对应关键词进行回复
|
||||
7. **用户认证** - 安全的登录认证系统
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. 启动系统
|
||||
```bash
|
||||
python Start.py
|
||||
```
|
||||
|
||||
### 3. 访问系统
|
||||
- 打开浏览器访问:`http://localhost:8080`
|
||||
- 默认登录账号:
|
||||
- **用户名**:`admin`
|
||||
- **密码**:`admin123`
|
||||
|
||||
## 📋 系统使用流程
|
||||
|
||||
### 步骤1:登录系统
|
||||
1. 访问 `http://localhost:8080`
|
||||
2. 输入用户名和密码登录
|
||||
3. 成功登录后进入管理界面
|
||||
|
||||
### 步骤2:添加闲鱼账号
|
||||
1. 在"添加新账号"区域填写:
|
||||
- **账号ID**:唯一标识(如:account1, 主账号等)
|
||||
- **Cookie值**:完整的闲鱼Cookie字符串
|
||||
2. 点击"添加账号"按钮
|
||||
|
||||
### 步骤3:设置关键词回复
|
||||
1. 在账号列表中点击"关键词"按钮
|
||||
2. 添加关键词和对应的回复内容
|
||||
3. 支持变量替换:
|
||||
- `{send_user_name}` - 发送者昵称
|
||||
- `{send_user_id}` - 发送者ID
|
||||
- `{send_message}` - 发送的消息内容
|
||||
4. 点击"保存更改"
|
||||
|
||||
### 步骤4:系统自动运行
|
||||
- 系统会自动监控闲鱼消息
|
||||
- 根据关键词匹配自动回复
|
||||
- 所有操作都有详细日志记录
|
||||
|
||||
## 🔧 功能详解
|
||||
|
||||
### 多账号管理
|
||||
- **添加账号**:支持添加无限数量的闲鱼账号
|
||||
- **修改Cookie**:可以随时更新账号的Cookie值
|
||||
- **删除账号**:删除不需要的账号及其关键词
|
||||
- **独立运行**:每个账号独立运行,互不干扰
|
||||
|
||||
### 关键词回复系统
|
||||
- **精确匹配**:支持关键词精确匹配
|
||||
- **变量替换**:回复内容支持动态变量
|
||||
- **优先级**:账号级关键词优先于全局关键词
|
||||
- **默认回复**:未匹配关键词时使用默认回复
|
||||
|
||||
### API接口
|
||||
- **接口地址**:`POST http://localhost:8080/xianyu/reply`
|
||||
- **功能**:根据cookie_id和消息内容返回回复内容
|
||||
- **自动调用**:系统收到消息时自动调用此接口
|
||||
|
||||
## 📊 系统架构
|
||||
|
||||
```
|
||||
用户界面 (Web) ←→ FastAPI服务器 ←→ SQLite数据库
|
||||
↓
|
||||
CookieManager
|
||||
↓
|
||||
XianyuLive (多实例)
|
||||
↓
|
||||
闲鱼WebSocket连接
|
||||
```
|
||||
|
||||
## 🔐 安全说明
|
||||
|
||||
### 登录认证
|
||||
- 所有管理功能都需要登录认证
|
||||
- Session token有效期24小时
|
||||
- 自动登录状态检查
|
||||
|
||||
### 数据安全
|
||||
- Cookie数据加密存储在SQLite数据库
|
||||
- 界面上不显示完整Cookie值
|
||||
- 支持安全的Cookie更新机制
|
||||
|
||||
## 📝 日志系统
|
||||
|
||||
### 日志文件位置
|
||||
- 日志目录:`logs/`
|
||||
- 文件格式:`xianyu_YYYY-MM-DD.log`
|
||||
- 自动轮转:每天一个文件,保留7天
|
||||
|
||||
### 日志内容
|
||||
- 系统启动和关闭
|
||||
- 账号添加、修改、删除
|
||||
- 消息接收和发送
|
||||
- 错误和异常信息
|
||||
|
||||
## 🛠️ 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. Cookie过期
|
||||
**现象**:日志显示"Session过期"
|
||||
**解决**:在Web界面更新对应账号的Cookie值
|
||||
|
||||
#### 2. 无法连接闲鱼
|
||||
**现象**:WebSocket连接失败
|
||||
**解决**:检查网络连接和Cookie是否有效
|
||||
|
||||
#### 3. 关键词不匹配
|
||||
**现象**:收到消息但没有自动回复
|
||||
**解决**:检查关键词设置,确保关键词包含在消息中
|
||||
|
||||
#### 4. 登录失败
|
||||
**现象**:无法登录管理界面
|
||||
**解决**:确认用户名密码正确(admin/admin123)
|
||||
|
||||
### 系统要求
|
||||
- Python 3.7+
|
||||
- Windows/Linux/macOS
|
||||
- 网络连接
|
||||
- 有效的闲鱼账号Cookie
|
||||
|
||||
## 🔄 更新和维护
|
||||
|
||||
### 配置文件
|
||||
- 主配置:`global_config.yml`
|
||||
- 数据库:`xianyu_data.db`
|
||||
- 静态文件:`static/` 目录
|
||||
|
||||
### 备份建议
|
||||
- 定期备份 `xianyu_data.db` 文件
|
||||
- 备份 `global_config.yml` 配置文件
|
||||
- 备份自定义的关键词文件
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
### 测试系统
|
||||
运行测试脚本检查系统状态:
|
||||
```bash
|
||||
python test_system.py
|
||||
```
|
||||
|
||||
### 重新创建配置
|
||||
如果配置文件损坏,运行:
|
||||
```bash
|
||||
python create_config.py
|
||||
```
|
||||
|
||||
## 🎯 使用建议
|
||||
|
||||
1. **Cookie获取**:使用浏览器开发者工具获取完整Cookie
|
||||
2. **关键词设置**:设置常用的咨询关键词和回复
|
||||
3. **定期检查**:定期查看日志确保系统正常运行
|
||||
4. **备份数据**:重要数据请及时备份
|
||||
|
||||
---
|
||||
|
||||
**注意**:本系统仅供学习交流使用,请遵守相关法律法规和平台规则。
|
199
商品管理功能说明.md
Normal file
199
商品管理功能说明.md
Normal file
@ -0,0 +1,199 @@
|
||||
# 🛍️ 商品管理功能使用说明
|
||||
|
||||
## 📋 功能概述
|
||||
|
||||
新增的商品管理功能可以自动收集和管理各个闲鱼账号的商品信息,为自动发货提供更准确的商品数据支持。
|
||||
|
||||
## 🎯 核心特性
|
||||
|
||||
### 1. 自动商品信息收集
|
||||
- **消息触发收集**:接收消息时自动提取商品ID并保存到数据库
|
||||
- **API获取详情**:通过闲鱼API获取完整商品信息
|
||||
- **智能去重**:商品ID唯一,避免重复存储
|
||||
- **增量更新**:有详情的商品不会被空数据覆盖
|
||||
|
||||
### 2. 多账号商品管理
|
||||
- **账号隔离**:每个Cookie账号的商品信息独立管理
|
||||
- **批量查看**:支持查看所有账号或指定账号的商品
|
||||
- **筛选功能**:可按账号ID筛选商品列表
|
||||
|
||||
### 3. 商品详情编辑
|
||||
- **可视化编辑**:提供专门的编辑界面修改商品详情
|
||||
- **JSON格式**:支持复杂的商品信息结构
|
||||
- **实时保存**:修改后立即保存到数据库
|
||||
|
||||
### 4. 自动发货增强
|
||||
- **优先级策略**:优先使用API获取商品信息,失败时从数据库获取
|
||||
- **智能匹配**:基于商品标题、描述、分类进行关键词匹配
|
||||
- **容错机制**:API失败时自动降级到数据库数据
|
||||
|
||||
## 🗄️ 数据库结构
|
||||
|
||||
### 商品信息表 (item_info)
|
||||
```sql
|
||||
CREATE TABLE item_info (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cookie_id TEXT NOT NULL, -- 所属账号ID
|
||||
item_id TEXT NOT NULL, -- 商品ID
|
||||
item_title TEXT, -- 商品标题
|
||||
item_description TEXT, -- 商品描述
|
||||
item_category TEXT, -- 商品分类
|
||||
item_price TEXT, -- 商品价格
|
||||
item_detail TEXT, -- 完整商品详情(JSON)
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(cookie_id, item_id) -- 联合唯一索引
|
||||
);
|
||||
```
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 1. 访问商品管理
|
||||
1. 登录系统后,点击侧边栏"商品管理"菜单
|
||||
2. 系统会自动加载所有账号的商品信息
|
||||
|
||||
### 2. 筛选商品
|
||||
1. 使用"筛选账号"下拉框选择特定账号
|
||||
2. 点击"刷新"按钮更新商品列表
|
||||
|
||||
### 3. 编辑商品详情
|
||||
1. 在商品列表中点击"编辑详情"按钮
|
||||
2. 在弹出的编辑框中修改商品详情(JSON格式)
|
||||
3. 点击"保存"按钮保存修改
|
||||
|
||||
### 4. 商品信息自动收集
|
||||
- **无需手动操作**:系统会在以下情况自动收集商品信息:
|
||||
- 接收到闲鱼消息时
|
||||
- 自动发货时获取商品详情
|
||||
- 调用商品信息API时
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### 1. 数据收集流程
|
||||
```
|
||||
接收消息 → 提取商品ID → 调用API获取详情 → 保存到数据库
|
||||
↓
|
||||
如果API失败 → 仅保存商品ID → 后续可手动补充详情
|
||||
```
|
||||
|
||||
### 2. 自动发货增强流程
|
||||
```
|
||||
触发自动发货 → 优先调用API获取商品信息
|
||||
↓
|
||||
API成功 → 使用API数据进行关键词匹配
|
||||
↓
|
||||
API失败 → 从数据库获取商品信息 → 使用数据库数据匹配
|
||||
```
|
||||
|
||||
### 3. 数据更新策略
|
||||
- **新商品**:直接插入数据库
|
||||
- **已存在且无详情**:更新为有详情的数据
|
||||
- **已存在且有详情**:跳过更新,保护现有数据
|
||||
|
||||
## 📊 API接口
|
||||
|
||||
### 获取所有商品
|
||||
```http
|
||||
GET /items
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 获取指定账号商品
|
||||
```http
|
||||
GET /items/cookie/{cookie_id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 获取商品详情
|
||||
```http
|
||||
GET /items/{cookie_id}/{item_id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 更新商品详情
|
||||
```http
|
||||
PUT /items/{cookie_id}/{item_id}
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {token}
|
||||
|
||||
{
|
||||
"item_detail": "{JSON格式的商品详情}"
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 界面功能
|
||||
|
||||
### 商品列表显示
|
||||
- **账号ID**:商品所属的闲鱼账号
|
||||
- **商品ID**:闲鱼商品的唯一标识
|
||||
- **商品标题**:从API获取的商品标题
|
||||
- **商品分类**:商品所属分类
|
||||
- **价格**:商品价格信息
|
||||
- **更新时间**:最后更新时间
|
||||
- **操作按钮**:编辑详情功能
|
||||
|
||||
### 编辑详情界面
|
||||
- **大文本框**:支持编辑大量JSON数据
|
||||
- **格式验证**:保存前验证JSON格式
|
||||
- **实时保存**:修改后立即生效
|
||||
|
||||
## 🔍 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **商品信息为空**
|
||||
- 原因:API获取失败,只保存了商品ID
|
||||
- 解决:手动编辑商品详情,补充信息
|
||||
|
||||
2. **自动发货匹配失败**
|
||||
- 原因:商品信息不完整,关键词匹配失败
|
||||
- 解决:编辑商品详情,添加相关关键词
|
||||
|
||||
3. **商品重复显示**
|
||||
- 原因:不同账号可能有相同商品ID
|
||||
- 说明:这是正常现象,系统按账号隔离
|
||||
|
||||
### 调试方法
|
||||
|
||||
1. **查看日志**
|
||||
```bash
|
||||
# 查看商品信息收集日志
|
||||
grep "商品信息" logs/xianyu_*.log
|
||||
|
||||
# 查看自动发货日志
|
||||
grep "自动发货" logs/xianyu_*.log
|
||||
```
|
||||
|
||||
2. **数据库查询**
|
||||
```python
|
||||
from db_manager import db_manager
|
||||
|
||||
# 查看所有商品
|
||||
items = db_manager.get_all_items()
|
||||
print(f"共有 {len(items)} 个商品")
|
||||
|
||||
# 查看特定账号商品
|
||||
items = db_manager.get_items_by_cookie("account_id")
|
||||
print(f"该账号有 {len(items)} 个商品")
|
||||
```
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
1. **定期检查商品信息**
|
||||
- 定期查看商品管理页面
|
||||
- 补充缺失的商品详情
|
||||
- 优化关键词匹配规则
|
||||
|
||||
2. **合理设置发货规则**
|
||||
- 根据商品信息设置精确的关键词
|
||||
- 利用商品分类进行规则匹配
|
||||
- 测试匹配效果
|
||||
|
||||
3. **监控系统日志**
|
||||
- 关注商品信息收集日志
|
||||
- 监控自动发货匹配情况
|
||||
- 及时处理异常情况
|
||||
|
||||
---
|
||||
|
||||
🎉 **商品管理功能让您的闲鱼自动发货更加智能和准确!**
|
296
日志管理功能说明.md
Normal file
296
日志管理功能说明.md
Normal file
@ -0,0 +1,296 @@
|
||||
# 📋 实时日志管理功能说明
|
||||
|
||||
## 📋 功能概述
|
||||
|
||||
新增了**实时日志管理功能**,可以实时收集和显示系统的控制台日志输出,支持滚动查看、过滤搜索、自动刷新和统计分析。
|
||||
|
||||
## 🔥 实时日志特色
|
||||
|
||||
### 📡 真正的实时日志
|
||||
- **实时收集**:直接从Python logging系统收集日志
|
||||
- **内存缓存**:在内存中保存最新的2000条日志
|
||||
- **零延迟**:无需读取文件,直接从内存获取
|
||||
- **控制台同步**:与控制台输出完全同步
|
||||
|
||||
## ✨ 主要功能
|
||||
|
||||
### 1. 实时日志显示
|
||||
- 📊 **实时收集**:直接从Python logging系统收集日志
|
||||
- 🔄 **自动刷新**:支持每5秒自动刷新日志
|
||||
- 📜 **滚动查看**:支持滚动查看所有历史日志
|
||||
- 🎨 **语法高亮**:不同日志级别使用不同颜色显示
|
||||
- 💾 **内存缓存**:最多保存2000条最新日志
|
||||
|
||||
### 2. 强大的过滤功能
|
||||
- 🏷️ **日志级别过滤**:DEBUG、INFO、WARNING、ERROR、CRITICAL
|
||||
- 📦 **来源模块过滤**:自动化模块、Web服务器、数据库管理、Cookie管理
|
||||
- 🔍 **关键词搜索**:支持实时搜索日志内容
|
||||
- 📏 **行数控制**:可选择显示100/200/500/1000行日志
|
||||
- 🎯 **服务器端过滤**:支持在API层面进行过滤,提高性能
|
||||
|
||||
### 3. 便捷的操作功能
|
||||
- 🔄 **手动刷新**:点击刷新按钮立即获取最新日志
|
||||
- 🗑️ **清空显示**:清空当前显示的日志内容
|
||||
- 💥 **清空服务器日志**:清空服务器内存中的所有日志
|
||||
- ⏯️ **自动刷新开关**:一键开启/关闭自动刷新
|
||||
- 📊 **统计信息**:显示详细的日志统计和分析
|
||||
|
||||
### 4. 统计分析功能
|
||||
- 📈 **总体统计**:总日志数、内存使用率
|
||||
- 📊 **级别分布**:各日志级别的数量和百分比
|
||||
- 🏷️ **来源分布**:各模块的日志数量和百分比
|
||||
- 📋 **实时更新**:统计信息实时更新
|
||||
|
||||
## 🎯 界面设计
|
||||
|
||||
### 菜单位置
|
||||
在左侧导航栏中,位于"商品管理"和"系统设置"之间:
|
||||
```
|
||||
📦 商品管理
|
||||
📋 日志管理 ← 新增
|
||||
⚙️ 系统设置
|
||||
```
|
||||
|
||||
### 页面布局
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 📋 日志管理 [刷新] [清空显示] [清空服务器] [统计] [自动刷新] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ [日志级别▼] [来源模块▼] [搜索框] [显示行数▼] │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ 系统日志 [1250条日志] [刚刚更新] │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 2025-01-23 12:30:45.123 [INFO] reply_server: 服务启动 │ │
|
||||
│ │ 2025-01-23 12:30:46.456 [ERROR] db_manager: 连接失败 │ │
|
||||
│ │ 2025-01-23 12:30:47.789 [WARNING] cookie_manager: 令牌过期 │ │
|
||||
│ │ 2025-01-23 12:30:48.012 [DEBUG] XianyuAutoAsync: 处理消息 │ │
|
||||
│ │ ... │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### 核心架构:实时日志收集器
|
||||
1. **LogCollector类**
|
||||
```python
|
||||
class LogCollector:
|
||||
def __init__(self, max_logs: int = 2000):
|
||||
self.logs = deque(maxlen=max_logs) # 循环缓冲区
|
||||
self.handler = LogHandler(self) # 自定义日志处理器
|
||||
```
|
||||
|
||||
2. **LogHandler处理器**
|
||||
```python
|
||||
class LogHandler(logging.Handler):
|
||||
def emit(self, record: logging.LogRecord):
|
||||
self.collector.add_log(record) # 实时收集日志
|
||||
```
|
||||
|
||||
3. **全局集成**
|
||||
- 在Start.py中初始化日志收集器
|
||||
- 自动注册到Python logging系统
|
||||
- 所有模块的日志都会被自动收集
|
||||
|
||||
### 前端实现
|
||||
1. **HTML结构**
|
||||
- 添加日志管理菜单项
|
||||
- 创建日志显示页面
|
||||
- 设计过滤器和控制按钮
|
||||
- 新增统计信息模态框
|
||||
|
||||
2. **CSS样式**
|
||||
- 深色主题的日志容器
|
||||
- 不同级别的颜色区分
|
||||
- 滚动条美化
|
||||
- 响应式布局
|
||||
|
||||
3. **JavaScript功能**
|
||||
- 实时日志获取和显示
|
||||
- 客户端和服务器端过滤
|
||||
- 自动刷新机制
|
||||
- 统计信息展示
|
||||
- 页面切换处理
|
||||
|
||||
### 后端API接口
|
||||
1. **获取日志**
|
||||
```python
|
||||
@app.get("/logs")
|
||||
async def get_logs(lines: int = 200, level: str = None, source: str = None):
|
||||
```
|
||||
|
||||
2. **日志统计**
|
||||
```python
|
||||
@app.get("/logs/stats")
|
||||
async def get_log_stats():
|
||||
```
|
||||
|
||||
3. **清空日志**
|
||||
```python
|
||||
@app.post("/logs/clear")
|
||||
async def clear_logs():
|
||||
```
|
||||
|
||||
## 📊 实时日志格式
|
||||
|
||||
### 收集的日志信息
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-01-23T12:30:45.123000",
|
||||
"level": "INFO",
|
||||
"source": "XianyuAutoAsync",
|
||||
"function": "process_message",
|
||||
"line": 1234,
|
||||
"message": "处理消息成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 日志来源
|
||||
- **XianyuAutoAsync**: 自动化核心模块
|
||||
- **reply_server**: Web服务器
|
||||
- **db_manager**: 数据库管理
|
||||
- **cookie_manager**: Cookie管理
|
||||
- **log_collector**: 日志收集器
|
||||
- **Start**: 启动模块
|
||||
- **test_***: 测试模块
|
||||
|
||||
### 支持的日志级别
|
||||
- 🔵 **DEBUG**:调试信息(蓝色)
|
||||
- 🟢 **INFO**:一般信息(绿色)
|
||||
- 🟡 **WARNING**:警告信息(黄色)
|
||||
- 🔴 **ERROR**:错误信息(红色)
|
||||
- 🟣 **CRITICAL**:严重错误(紫色,加粗)
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 1. 访问日志管理
|
||||
1. 登录系统后,点击左侧菜单的"日志管理"
|
||||
2. 系统会自动加载最新的200条日志
|
||||
3. 日志以时间倒序显示,最新的在底部
|
||||
|
||||
### 2. 过滤日志
|
||||
- **按级别过滤**:选择特定的日志级别(如只看ERROR)
|
||||
- **按来源过滤**:选择特定的模块(如只看数据库相关)
|
||||
- **关键词搜索**:输入关键词实时搜索日志内容
|
||||
- **调整行数**:选择显示更多或更少的日志行数
|
||||
|
||||
### 3. 自动刷新
|
||||
1. 点击"开启自动刷新"按钮
|
||||
2. 系统每5秒自动获取最新日志
|
||||
3. 再次点击可停止自动刷新
|
||||
|
||||
### 4. 手动操作
|
||||
- **刷新**:立即获取最新日志
|
||||
- **清空显示**:清空当前显示的日志(不删除服务器数据)
|
||||
- **清空服务器日志**:清空服务器内存中的所有日志
|
||||
- **统计信息**:查看详细的日志统计和分析
|
||||
- **滚动查看**:使用鼠标滚轮或滚动条查看历史日志
|
||||
|
||||
### 5. 统计分析
|
||||
- **总体统计**:查看日志总数和内存使用情况
|
||||
- **级别分布**:查看各日志级别的数量和百分比
|
||||
- **来源分布**:查看各模块的日志数量和百分比
|
||||
- **实时更新**:统计信息随日志实时更新
|
||||
|
||||
## 💾 内存日志管理
|
||||
|
||||
### 内存缓存机制
|
||||
- **循环缓冲区**:使用deque实现,最多保存2000条日志
|
||||
- **自动清理**:超出容量时自动删除最旧的日志
|
||||
- **线程安全**:使用锁机制确保多线程安全
|
||||
- **零文件依赖**:完全基于内存,无需读取日志文件
|
||||
|
||||
### 性能优势
|
||||
- **零延迟**:直接从内存获取,无文件I/O
|
||||
- **实时性**:与控制台输出完全同步
|
||||
- **高效过滤**:在内存中进行过滤,速度极快
|
||||
- **低资源消耗**:内存占用可控,不会无限增长
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 1. 性能考虑
|
||||
- 大日志文件可能影响加载速度
|
||||
- 建议设置合理的显示行数
|
||||
- 自动刷新会增加服务器负载
|
||||
|
||||
### 2. 权限要求
|
||||
- 需要管理员权限才能访问
|
||||
- 确保日志文件有读取权限
|
||||
- 网络连接稳定性影响实时性
|
||||
|
||||
### 3. 浏览器兼容性
|
||||
- 建议使用现代浏览器
|
||||
- 支持WebSocket的浏览器效果更佳
|
||||
- 移动端可能显示效果有限
|
||||
|
||||
## 🔮 未来扩展
|
||||
|
||||
### 1. 高级功能
|
||||
- 📊 **日志统计**:错误率、调用频率统计
|
||||
- 📈 **图表展示**:日志趋势图表
|
||||
- 🔔 **实时告警**:错误日志实时通知
|
||||
- 💾 **日志下载**:支持导出日志文件
|
||||
|
||||
### 2. 性能优化
|
||||
- 🚀 **WebSocket**:实时推送新日志
|
||||
- 💾 **缓存机制**:减少文件读取次数
|
||||
- 🔄 **增量更新**:只获取新增日志
|
||||
- 📦 **压缩传输**:减少网络传输量
|
||||
|
||||
### 3. 用户体验
|
||||
- 🎨 **主题切换**:支持亮色/暗色主题
|
||||
- 🔍 **高级搜索**:正则表达式搜索
|
||||
- 📌 **书签功能**:标记重要日志
|
||||
- 📱 **移动适配**:优化移动端显示
|
||||
|
||||
## 🎉 功能特色
|
||||
|
||||
### 1. 实时性
|
||||
- 自动刷新机制确保日志实时性
|
||||
- 页面切换时智能停止刷新
|
||||
- 手动刷新立即获取最新内容
|
||||
|
||||
### 2. 易用性
|
||||
- 直观的界面设计
|
||||
- 丰富的过滤选项
|
||||
- 便捷的操作按钮
|
||||
|
||||
### 3. 专业性
|
||||
- 类似IDE的日志显示效果
|
||||
- 语法高亮和颜色区分
|
||||
- 专业的等宽字体
|
||||
|
||||
### 4. 可扩展性
|
||||
- 模块化的代码结构
|
||||
- 易于添加新功能
|
||||
- 支持多种日志格式
|
||||
|
||||
## 🆚 对比:文件日志 vs 实时日志
|
||||
|
||||
| 特性 | 文件日志 | 实时日志 |
|
||||
|------|---------|---------|
|
||||
| **实时性** | 需要刷新文件 | 完全实时 |
|
||||
| **性能** | 文件I/O开销 | 内存访问,极快 |
|
||||
| **同步性** | 可能有延迟 | 与控制台同步 |
|
||||
| **资源消耗** | 磁盘I/O | 内存占用 |
|
||||
| **历史记录** | 永久保存 | 最新2000条 |
|
||||
| **过滤效率** | 需要解析 | 内存中过滤 |
|
||||
| **部署复杂度** | 需要日志文件 | 无额外依赖 |
|
||||
|
||||
## 🎯 使用建议
|
||||
|
||||
### 适用场景
|
||||
- ✅ **开发调试**:实时查看程序运行状态
|
||||
- ✅ **问题排查**:快速定位错误和异常
|
||||
- ✅ **性能监控**:监控系统运行情况
|
||||
- ✅ **用户支持**:协助用户解决问题
|
||||
|
||||
### 最佳实践
|
||||
1. **开启自动刷新**:实时监控系统状态
|
||||
2. **使用过滤器**:快速找到关注的日志
|
||||
3. **查看统计信息**:了解系统整体状况
|
||||
4. **定期清空**:避免内存占用过多
|
||||
|
||||
---
|
||||
|
||||
🎉 **实时日志管理功能已完成,提供了真正的实时日志查看、过滤和分析能力!**
|
265
自动发货功能说明.md
Normal file
265
自动发货功能说明.md
Normal file
@ -0,0 +1,265 @@
|
||||
# 🚚 自动发货功能使用说明
|
||||
|
||||
## 📋 功能概述
|
||||
|
||||
自动发货功能可以在买家付款成功后,根据商品信息自动匹配发货规则,并发送对应的卡券内容给买家。
|
||||
|
||||
## 🎯 工作流程
|
||||
|
||||
### 1. 触发条件
|
||||
- 系统收到"等待卖家发货"消息时自动触发
|
||||
- 表示买家已完成付款,等待卖家发货
|
||||
|
||||
### 2. 商品信息获取
|
||||
```
|
||||
买家付款成功 → 提取商品ID → 调用闲鱼API → 获取商品详情
|
||||
```
|
||||
|
||||
**获取的信息包括:**
|
||||
- 商品标题
|
||||
- 商品描述
|
||||
- 商品分类
|
||||
- 商品价格
|
||||
- 其他相关信息
|
||||
|
||||
### 3. 智能匹配规则
|
||||
```
|
||||
商品信息 → 组合搜索文本 → 匹配发货规则 → 选择最佳规则
|
||||
```
|
||||
|
||||
**匹配策略:**
|
||||
- 优先匹配关键字长度较长的规则(更精确)
|
||||
- 支持商品标题、描述、分类的综合匹配
|
||||
- 支持模糊匹配和部分匹配
|
||||
|
||||
### 4. 自动发货执行
|
||||
```
|
||||
匹配规则 → 获取卡券内容 → 发送给买家 → 更新统计
|
||||
```
|
||||
|
||||
## 🎫 卡券类型说明
|
||||
|
||||
### 1. API接口类型
|
||||
**适用场景:** 需要动态获取内容的卡券
|
||||
- 调用外部API获取实时数据
|
||||
- 支持GET/POST请求
|
||||
- 可配置请求头和参数
|
||||
- 自动处理API响应
|
||||
|
||||
**配置示例:**
|
||||
```json
|
||||
{
|
||||
"url": "https://api.example.com/get-card",
|
||||
"method": "GET",
|
||||
"timeout": 10,
|
||||
"headers": "{\"Authorization\": \"Bearer token\"}",
|
||||
"params": "{\"type\": \"recharge\", \"amount\": 100}"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 固定文字类型
|
||||
**适用场景:** 标准化的文字内容
|
||||
- 发送预设的固定文字
|
||||
- 适合使用说明、激活码等
|
||||
- 内容固定不变
|
||||
|
||||
**内容示例:**
|
||||
```
|
||||
🎉 恭喜您获得VIP会员卡!
|
||||
|
||||
会员卡号:VIP2024001
|
||||
有效期:2024年12月31日
|
||||
|
||||
专属权益:
|
||||
✅ 免费配送
|
||||
✅ 专属客服
|
||||
✅ 会员折扣
|
||||
|
||||
感谢您的支持!
|
||||
```
|
||||
|
||||
### 3. 批量数据类型
|
||||
**适用场景:** 唯一性数据,如卡密
|
||||
- 逐条消费预设数据
|
||||
- 线程安全,防止重复发送
|
||||
- 自动管理库存
|
||||
|
||||
**数据格式:**
|
||||
```
|
||||
CARD001:PASS001
|
||||
CARD002:PASS002
|
||||
CARD003:PASS003
|
||||
```
|
||||
|
||||
## ⚙️ 配置步骤
|
||||
|
||||
### 1. 创建卡券
|
||||
1. 登录管理界面
|
||||
2. 点击"卡券管理"菜单
|
||||
3. 点击"添加卡券"按钮
|
||||
4. 选择卡券类型并配置相应参数
|
||||
5. 启用卡券
|
||||
|
||||
### 2. 配置发货规则
|
||||
1. 点击"自动发货"菜单
|
||||
2. 点击"添加规则"按钮
|
||||
3. 设置商品关键字
|
||||
4. 选择对应的卡券
|
||||
5. 设置发货数量
|
||||
6. 启用规则
|
||||
|
||||
### 3. 关键字配置建议
|
||||
|
||||
**商品分类关键字:**
|
||||
- `手机` - 匹配手机类商品
|
||||
- `数码` - 匹配数码产品
|
||||
- `服装` - 匹配服装类商品
|
||||
- `鞋子` - 匹配鞋类商品
|
||||
- `家居` - 匹配家居用品
|
||||
|
||||
**品牌关键字:**
|
||||
- `iPhone` - 匹配iPhone商品
|
||||
- `小米` - 匹配小米商品
|
||||
- `华为` - 匹配华为商品
|
||||
|
||||
**功能关键字:**
|
||||
- `充值` - 匹配充值类商品
|
||||
- `会员` - 匹配会员服务
|
||||
- `游戏` - 匹配游戏相关商品
|
||||
|
||||
## 📊 统计功能
|
||||
|
||||
### 发货统计
|
||||
- 总发货次数
|
||||
- 各规则发货次数
|
||||
- 发货成功率
|
||||
- 卡券消耗情况
|
||||
|
||||
### 库存管理
|
||||
- 批量数据剩余数量
|
||||
- 卡券启用状态
|
||||
- 规则启用状态
|
||||
|
||||
## 🔧 高级配置
|
||||
|
||||
### 环境变量
|
||||
```bash
|
||||
# 启用自动发货
|
||||
AUTO_DELIVERY_ENABLED=true
|
||||
|
||||
# 自动发货超时时间
|
||||
AUTO_DELIVERY_TIMEOUT=30
|
||||
|
||||
# API卡券请求超时
|
||||
API_CARD_TIMEOUT=10
|
||||
|
||||
# 批量数据并发保护
|
||||
BATCH_DATA_LOCK_TIMEOUT=5
|
||||
```
|
||||
|
||||
### 日志配置
|
||||
系统会记录详细的发货日志:
|
||||
- 商品信息获取日志
|
||||
- 规则匹配日志
|
||||
- 发货执行日志
|
||||
- 错误处理日志
|
||||
|
||||
## 🚨 注意事项
|
||||
|
||||
### 1. 商品ID提取
|
||||
- 系统会尝试从多个字段提取商品ID
|
||||
- 如果提取失败,会使用默认ID
|
||||
- 建议检查日志确认提取是否成功
|
||||
|
||||
### 2. API卡券配置
|
||||
- 确保API地址可访问
|
||||
- 检查请求头和参数格式
|
||||
- 设置合适的超时时间
|
||||
- 处理API异常情况
|
||||
|
||||
### 3. 批量数据管理
|
||||
- 定期检查数据库存
|
||||
- 及时补充批量数据
|
||||
- 监控消耗速度
|
||||
- 设置库存预警
|
||||
|
||||
### 4. 规则优先级
|
||||
- 关键字越长,优先级越高
|
||||
- 避免关键字冲突
|
||||
- 定期优化匹配规则
|
||||
- 测试匹配效果
|
||||
|
||||
## 🧪 测试方法
|
||||
|
||||
### 1. 功能测试
|
||||
```bash
|
||||
python test-item-info-delivery.py
|
||||
```
|
||||
|
||||
### 2. 手动测试
|
||||
1. 创建测试卡券和规则
|
||||
2. 模拟"等待卖家发货"消息
|
||||
3. 检查发货是否正常
|
||||
4. 验证统计数据更新
|
||||
|
||||
### 3. 日志检查
|
||||
查看日志文件确认:
|
||||
- 商品信息获取是否成功
|
||||
- 规则匹配是否正确
|
||||
- 发货内容是否正确发送
|
||||
|
||||
## 🔍 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
**1. 未触发自动发货**
|
||||
- 检查发货规则是否启用
|
||||
- 检查卡券是否启用
|
||||
- 检查关键字是否匹配
|
||||
- 查看错误日志
|
||||
|
||||
**2. 商品信息获取失败**
|
||||
- 检查Cookie是否有效
|
||||
- 检查网络连接
|
||||
- 检查商品ID是否正确
|
||||
- 查看API调用日志
|
||||
|
||||
**3. 批量数据用完**
|
||||
- 检查数据库存
|
||||
- 及时补充数据
|
||||
- 考虑使用其他类型卡券
|
||||
|
||||
**4. API卡券调用失败**
|
||||
- 检查API地址和参数
|
||||
- 检查网络连接
|
||||
- 增加超时时间
|
||||
- 查看API响应日志
|
||||
|
||||
## 📈 性能优化
|
||||
|
||||
### 1. 数据库优化
|
||||
- 定期清理过期数据
|
||||
- 优化查询索引
|
||||
- 监控数据库性能
|
||||
|
||||
### 2. 网络优化
|
||||
- 设置合适的超时时间
|
||||
- 使用连接池
|
||||
- 实现重试机制
|
||||
|
||||
### 3. 并发控制
|
||||
- 批量数据消费使用锁机制
|
||||
- 避免重复发货
|
||||
- 控制并发数量
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
1. **规则设计**:关键字要具体明确,避免过于宽泛
|
||||
2. **库存管理**:定期检查批量数据库存,及时补充
|
||||
3. **监控告警**:设置发货失败告警,及时处理异常
|
||||
4. **测试验证**:新规则上线前充分测试
|
||||
5. **日志分析**:定期分析发货日志,优化匹配规则
|
||||
|
||||
---
|
||||
|
||||
🎉 **自动发货功能让您的闲鱼店铺实现真正的自动化运营!**
|
203
获取所有商品功能说明.md
Normal file
203
获取所有商品功能说明.md
Normal file
@ -0,0 +1,203 @@
|
||||
# 📦 获取所有商品功能说明
|
||||
|
||||
## 📋 功能概述
|
||||
|
||||
在商品管理界面新增了"获取所有商品"功能,支持选择指定账号,一键获取该账号下的所有商品信息,并将详细结果打印到控制台。
|
||||
|
||||
## ✨ 主要功能
|
||||
|
||||
### 1. 界面功能
|
||||
- 🎯 **账号选择**:使用现有的账号筛选下拉框选择目标账号
|
||||
- 🔄 **一键获取**:点击"获取所有商品"按钮开始获取
|
||||
- 📊 **状态显示**:按钮显示获取进度状态
|
||||
- 🔔 **结果通知**:获取完成后显示成功消息和商品数量
|
||||
|
||||
### 2. 后端功能
|
||||
- 🔐 **账号验证**:验证选中账号的有效性
|
||||
- 🔄 **自动重试**:支持token失效时自动刷新重试(最多3次)
|
||||
- 📝 **详细日志**:完整的获取过程日志记录
|
||||
- 🖨️ **控制台输出**:格式化的商品信息打印到控制台
|
||||
|
||||
## 🎯 使用方法
|
||||
|
||||
### 1. 访问商品管理
|
||||
1. 登录系统后,点击左侧菜单的"商品管理"
|
||||
2. 进入商品管理页面
|
||||
|
||||
### 2. 选择账号
|
||||
1. 在页面上方的"筛选账号"下拉框中选择目标账号
|
||||
2. 确保选择了有效的账号(不是"全部账号")
|
||||
|
||||
### 3. 获取商品
|
||||
1. 点击"获取所有商品"按钮
|
||||
2. 按钮会显示"获取中..."状态
|
||||
3. 等待获取完成
|
||||
|
||||
### 4. 查看结果
|
||||
1. 获取完成后会显示成功消息
|
||||
2. 控制台会打印详细的商品信息
|
||||
3. 商品列表会自动刷新
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### 前端实现
|
||||
```javascript
|
||||
// 获取所有商品信息
|
||||
async function getAllItemsFromAccount() {
|
||||
const cookieSelect = document.getElementById('itemCookieFilter');
|
||||
const selectedCookieId = cookieSelect.value;
|
||||
|
||||
// 调用后端API
|
||||
const response = await fetch(`${apiBase}/items/get-all-from-account`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
cookie_id: selectedCookieId
|
||||
})
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 后端API
|
||||
```python
|
||||
@app.post("/items/get-all-from-account")
|
||||
async def get_all_items_from_account(request: dict, _: None = Depends(require_auth)):
|
||||
"""从指定账号获取所有商品信息"""
|
||||
# 1. 验证参数
|
||||
# 2. 获取账号信息
|
||||
# 3. 创建XianyuLive实例
|
||||
# 4. 调用get_item_list_info方法
|
||||
# 5. 返回结果
|
||||
```
|
||||
|
||||
### 核心方法
|
||||
```python
|
||||
async def get_item_list_info(self, retry_count=0):
|
||||
"""获取商品信息,自动处理token失效的情况"""
|
||||
# 1. 检查重试次数
|
||||
# 2. 刷新token(如果需要)
|
||||
# 3. 构造请求参数
|
||||
# 4. 发送API请求
|
||||
# 5. 处理响应结果
|
||||
# 6. 打印商品信息到控制台
|
||||
```
|
||||
|
||||
## 📊 API参数说明
|
||||
|
||||
### 请求参数
|
||||
```json
|
||||
{
|
||||
"cookie_id": "账号ID"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "成功获取 5 个商品,详细信息已打印到控制台",
|
||||
"total_count": 5
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
## 🖨️ 控制台输出格式
|
||||
|
||||
```
|
||||
================================================================================
|
||||
📦 账号 12345678 的商品列表 (5 个商品)
|
||||
================================================================================
|
||||
|
||||
🔸 商品 1:
|
||||
商品ID: 123456789
|
||||
商品标题: iPhone 13 Pro Max 256G
|
||||
价格: 6999
|
||||
状态: 在售
|
||||
创建时间: 2025-07-23 10:30:00
|
||||
更新时间: 2025-07-23 16:45:00
|
||||
图片数量: 8
|
||||
详细信息: {
|
||||
"id": "123456789",
|
||||
"title": "iPhone 13 Pro Max 256G",
|
||||
"price": "6999",
|
||||
"status": "在售",
|
||||
...
|
||||
}
|
||||
|
||||
🔸 商品 2:
|
||||
...
|
||||
|
||||
================================================================================
|
||||
✅ 商品列表获取完成
|
||||
================================================================================
|
||||
```
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 1. 使用限制
|
||||
- 需要选择有效的账号(不能是"全部账号")
|
||||
- 需要账号的cookie信息有效
|
||||
- 需要网络连接正常
|
||||
|
||||
### 2. 错误处理
|
||||
- Token失效时会自动刷新重试
|
||||
- 最多重试3次,避免无限循环
|
||||
- 网络异常时会显示错误信息
|
||||
|
||||
### 3. 性能考虑
|
||||
- 获取过程可能需要几秒钟时间
|
||||
- 大量商品时控制台输出较多
|
||||
- 建议在网络良好时使用
|
||||
|
||||
## 🔮 功能特色
|
||||
|
||||
### 1. 智能重试机制
|
||||
- 自动检测token失效
|
||||
- 智能刷新token后重试
|
||||
- 避免因token问题导致的失败
|
||||
|
||||
### 2. 详细信息输出
|
||||
- 完整的商品信息展示
|
||||
- 格式化的控制台输出
|
||||
- 便于调试和分析
|
||||
|
||||
### 3. 用户友好界面
|
||||
- 直观的操作按钮
|
||||
- 实时状态反馈
|
||||
- 清晰的成功/失败提示
|
||||
|
||||
### 4. 完整的日志记录
|
||||
- 详细的操作日志
|
||||
- 错误信息记录
|
||||
- 便于问题排查
|
||||
|
||||
## 🚀 扩展可能
|
||||
|
||||
### 1. 批量操作
|
||||
- 支持多个账号批量获取
|
||||
- 导出商品信息到文件
|
||||
- 商品信息对比分析
|
||||
|
||||
### 2. 数据处理
|
||||
- 商品信息入库存储
|
||||
- 商品状态监控
|
||||
- 价格变化追踪
|
||||
|
||||
### 3. 界面优化
|
||||
- 商品信息表格显示
|
||||
- 商品图片预览
|
||||
- 筛选和搜索功能
|
||||
|
||||
---
|
||||
|
||||
🎉 **获取所有商品功能已完成,支持一键获取指定账号的所有商品信息!**
|
Loading…
x
Reference in New Issue
Block a user