首次提交

This commit is contained in:
zhinianboke 2025-07-24 12:05:21 +08:00
commit 73b274cce8
45 changed files with 18012 additions and 0 deletions

91
.dockerignore Normal file
View 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
View 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
View 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
View 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
View 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
View 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
- 带Nginxhttp://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
View 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
View File

@ -0,0 +1,173 @@
# 🐟 XianYuAutoDeliveryX - 闲鱼虚拟商品商自动发货&聊天对接大模型
[![Python Version](https://img.shields.io/badge/python-3.7%2B-blue)](https://www.python.org/)
[![License](https://img.shields.io/badge/license-MIT-green)](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 调用频率和成本控制
## 📝 效果
![image-20250611004531745](https://typeropic.oss-cn-beijing.aliyuncs.com/cp/image-20250611004531745.png)
![image-20250611004549662](https://typeropic.oss-cn-beijing.aliyuncs.com/cp/image-20250611004549662.png)
## 🧸特别鸣谢
本项目参考了以下开源项目: https://github.com/cv-cat/XianYuApis
感谢[@CVcat](https://github.com/cv-cat)的技术支持
## 📞 联系方式
如有问题或建议,欢迎提交 Issue 或 Pull Request。
## 技术交流
![image-20250611004141387](https://typeropic.oss-cn-beijing.aliyuncs.com/cp/image-20250611004141387.png)

125
Start.py Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

121
config.py Normal file
View 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
View 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

Binary file not shown.

1823
db_manager.py Normal file

File diff suppressed because it is too large Load Diff

298
deploy.bat Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

29
requirements.txt Normal file
View 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

File diff suppressed because it is too large Load Diff

281
static/login.html Normal file
View 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>

View 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
View 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
View 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
View 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
View 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

Binary file not shown.

178
使用说明.md Normal file
View 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
View 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
View 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
View 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. **日志分析**:定期分析发货日志,优化匹配规则
---
🎉 **自动发货功能让您的闲鱼店铺实现真正的自动化运营!**

View 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. 界面优化
- 商品信息表格显示
- 商品图片预览
- 筛选和搜索功能
---
🎉 **获取所有商品功能已完成,支持一键获取指定账号的所有商品信息!**