mirror of
https://github.com/zhinianboke/xianyu-auto-reply.git
synced 2025-08-29 17:17:38 +08:00
合并优化
合并优化
This commit is contained in:
commit
12d044362c
3
.gitignore
vendored
3
.gitignore
vendored
@ -294,6 +294,8 @@ example_*.py
|
|||||||
*_example.py
|
*_example.py
|
||||||
demo_*.py
|
demo_*.py
|
||||||
*_demo.py
|
*_demo.py
|
||||||
|
fix_*.py
|
||||||
|
*_fix.py
|
||||||
|
|
||||||
# 文档文件(除了README.md)
|
# 文档文件(除了README.md)
|
||||||
*.md
|
*.md
|
||||||
@ -301,6 +303,7 @@ demo_*.py
|
|||||||
!CHANGELOG.md
|
!CHANGELOG.md
|
||||||
!CONTRIBUTING.md
|
!CONTRIBUTING.md
|
||||||
!LICENSE.md
|
!LICENSE.md
|
||||||
|
!docs/*.md
|
||||||
|
|
||||||
# 临时配置文件
|
# 临时配置文件
|
||||||
*.local.yml
|
*.local.yml
|
||||||
|
13
Dockerfile
13
Dockerfile
@ -3,11 +3,13 @@ FROM python:3.11-slim-bookworm
|
|||||||
|
|
||||||
# 设置标签信息
|
# 设置标签信息
|
||||||
LABEL maintainer="zhinianboke"
|
LABEL maintainer="zhinianboke"
|
||||||
LABEL version="2.1.0"
|
LABEL version="2.2.0"
|
||||||
LABEL description="闲鱼自动回复系统 - 企业级多用户版本,支持自动发货和免拼发货"
|
LABEL description="闲鱼自动回复系统 - 企业级多用户版本,支持自动发货和免拼发货"
|
||||||
LABEL repository="https://github.com/zhinianboke/xianyu-auto-reply"
|
LABEL repository="https://github.com/zhinianboke/xianyu-auto-reply"
|
||||||
LABEL license="仅供学习使用,禁止商业用途"
|
LABEL license="仅供学习使用,禁止商业用途"
|
||||||
LABEL author="zhinianboke"
|
LABEL author="zhinianboke"
|
||||||
|
LABEL build-date=""
|
||||||
|
LABEL vcs-ref=""
|
||||||
|
|
||||||
# 设置工作目录
|
# 设置工作目录
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@ -99,9 +101,10 @@ EXPOSE 8080
|
|||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD curl -f http://localhost:8080/health || exit 1
|
CMD curl -f http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
# 复制启动脚本
|
# 复制启动脚本并设置权限
|
||||||
COPY entrypoint.sh /app/entrypoint.sh
|
COPY entrypoint.sh /app/entrypoint.sh
|
||||||
RUN chmod +x /app/entrypoint.sh
|
RUN chmod +x /app/entrypoint.sh && \
|
||||||
|
dos2unix /app/entrypoint.sh 2>/dev/null || true
|
||||||
|
|
||||||
# 启动命令
|
# 启动命令(使用ENTRYPOINT确保脚本被执行)
|
||||||
CMD ["/app/entrypoint.sh"]
|
ENTRYPOINT ["/bin/bash", "/app/entrypoint.sh"]
|
64
README.md
64
README.md
@ -113,13 +113,8 @@ xianyu-auto-reply/
|
|||||||
│ ├── index.html # 主管理界面(集成所有功能模块)
|
│ ├── index.html # 主管理界面(集成所有功能模块)
|
||||||
│ ├── login.html # 用户登录页面
|
│ ├── login.html # 用户登录页面
|
||||||
│ ├── register.html # 用户注册页面(邮箱验证)
|
│ ├── register.html # 用户注册页面(邮箱验证)
|
||||||
│ ├── user_management.html # 用户管理页面(管理员功能)
|
|
||||||
│ ├── data_management.html # 数据管理页面(导入导出)
|
|
||||||
│ ├── log_management.html # 日志管理页面(实时日志查看)
|
|
||||||
│ ├── item_search.html # 商品搜索页面(独立版本)
|
|
||||||
│ ├── js/
|
│ ├── js/
|
||||||
│ │ ├── app.js # 主要JavaScript逻辑
|
│ │ └── app.js # 主要JavaScript逻辑和所有功能模块
|
||||||
│ │ └── modules/ # 模块化JavaScript文件
|
|
||||||
│ ├── css/
|
│ ├── css/
|
||||||
│ │ ├── variables.css # CSS变量定义
|
│ │ ├── variables.css # CSS变量定义
|
||||||
│ │ ├── layout.css # 布局样式
|
│ │ ├── layout.css # 布局样式
|
||||||
@ -141,16 +136,23 @@ xianyu-auto-reply/
|
|||||||
│ ├── wechat-group.png # 微信群二维码
|
│ ├── wechat-group.png # 微信群二维码
|
||||||
│ └── qq-group.png # QQ群二维码
|
│ └── qq-group.png # QQ群二维码
|
||||||
├── 🐳 Docker部署
|
├── 🐳 Docker部署
|
||||||
│ ├── Dockerfile # Docker镜像构建文件
|
│ ├── Dockerfile # Docker镜像构建文件(优化版)
|
||||||
|
│ ├── Dockerfile-cn # 国内优化版Docker镜像构建文件
|
||||||
│ ├── docker-compose.yml # Docker Compose一键部署配置
|
│ ├── docker-compose.yml # Docker Compose一键部署配置
|
||||||
|
│ ├── docker-compose-cn.yml # 国内优化版Docker Compose配置
|
||||||
│ ├── docker-deploy.sh # Docker部署管理脚本(Linux/macOS)
|
│ ├── docker-deploy.sh # Docker部署管理脚本(Linux/macOS)
|
||||||
│ ├── docker-deploy.bat # Docker部署管理脚本(Windows)
|
│ ├── docker-deploy.bat # Docker部署管理脚本(Windows)
|
||||||
│ └── entrypoint.sh # Docker容器启动脚本
|
│ ├── entrypoint.sh # Docker容器启动脚本
|
||||||
|
│ └── .dockerignore # Docker构建忽略文件
|
||||||
|
├── 🌐 Nginx配置
|
||||||
|
│ └── nginx/
|
||||||
|
│ ├── nginx.conf # Nginx反向代理配置
|
||||||
|
│ └── ssl/ # SSL证书目录
|
||||||
├── 📋 配置文件
|
├── 📋 配置文件
|
||||||
│ ├── global_config.yml # 全局配置文件(WebSocket、API等)
|
│ ├── global_config.yml # 全局配置文件(WebSocket、API等)
|
||||||
│ ├── requirements.txt # Python依赖包列表(精简版)
|
│ ├── requirements.txt # Python依赖包列表(精简版,无内置模块)
|
||||||
│ ├── .gitignore # Git忽略文件配置
|
│ ├── .gitignore # Git忽略文件配置(完整版)
|
||||||
│ └── README.md # 项目说明文档
|
│ └── README.md # 项目说明文档(本文件)
|
||||||
└── 📊 数据目录(运行时创建)
|
└── 📊 数据目录(运行时创建)
|
||||||
├── data/ # 数据目录(Docker挂载)
|
├── data/ # 数据目录(Docker挂载)
|
||||||
│ └── xianyu_data.db # SQLite数据库文件
|
│ └── xianyu_data.db # SQLite数据库文件
|
||||||
@ -340,7 +342,7 @@ python Start.py
|
|||||||
- **多重安全验证** - 超级加密保护,防止误操作和数据泄露
|
- **多重安全验证** - 超级加密保护,防止误操作和数据泄露
|
||||||
- **批量处理能力** - 支持批量确认发货,提高处理效率
|
- **批量处理能力** - 支持批量确认发货,提高处理效率
|
||||||
- **异常处理机制** - 完善的错误处理和重试机制,确保发货成功
|
- **异常处理机制** - 完善的错误处理和重试机制,确保发货成功
|
||||||
- **多渠道通知** - 支持QQ、钉钉、邮件等多种发货通知方式
|
- **多渠道通知** - 支持QQ、钉钉、飞书、Bark、邮件等多种发货通知方式
|
||||||
|
|
||||||
### 👥 多用户系统
|
### 👥 多用户系统
|
||||||
- **用户注册登录** - 支持邮箱验证和图形验证码,安全可靠
|
- **用户注册登录** - 支持邮箱验证和图形验证码,安全可靠
|
||||||
@ -364,10 +366,11 @@ python Start.py
|
|||||||
- **账号状态验证** - 自动检查cookies启用状态,确保搜索功能正常
|
- **账号状态验证** - 自动检查cookies启用状态,确保搜索功能正常
|
||||||
|
|
||||||
### 📱 通知系统
|
### 📱 通知系统
|
||||||
- **多渠道支持** - QQ、钉钉、邮件、微信、Telegram等6种通知方式
|
- **多渠道支持** - QQ、钉钉、飞书、Bark、邮件、微信、Telegram等8种通知方式
|
||||||
- **智能配置** - 可视化配置界面,支持复杂参数和加密设置
|
- **智能配置** - 可视化配置界面,支持复杂参数和加密设置
|
||||||
- **实时推送** - 重要事件实时通知,及时了解系统状态
|
- **实时推送** - 重要事件实时通知,及时了解系统状态
|
||||||
- **通知模板** - 自定义通知内容和格式,个性化消息推送
|
- **通知模板** - 自定义通知内容和格式,个性化消息推送
|
||||||
|
- **移动端支持** - Bark iOS推送,随时随地接收通知
|
||||||
|
|
||||||
### 🔐 安全特性
|
### 🔐 安全特性
|
||||||
- **Cookie安全管理** - 加密存储用户凭证,定期自动刷新
|
- **Cookie安全管理** - 加密存储用户凭证,定期自动刷新
|
||||||
@ -411,30 +414,27 @@ python Start.py
|
|||||||
- **`image_uploader.py`** - 图片上传工具,支持多种CDN服务商、自动压缩、格式优化、批量上传
|
- **`image_uploader.py`** - 图片上传工具,支持多种CDN服务商、自动压缩、格式优化、批量上传
|
||||||
|
|
||||||
### 🌐 前端界面 (`static/`)
|
### 🌐 前端界面 (`static/`)
|
||||||
- **`index.html`** - 主管理界面,包含账号管理、关键词管理、系统监控、实时状态显示
|
- **`index.html`** - 主管理界面,集成所有功能模块:账号管理、关键词管理、商品管理、发货管理、系统监控、用户管理等
|
||||||
- **`login.html`** - 用户登录页面,支持图形验证码、记住登录状态、多重安全验证
|
- **`login.html`** - 用户登录页面,支持图形验证码、记住登录状态、多重安全验证
|
||||||
- **`register.html`** - 用户注册页面,支持邮箱验证码、实时验证、密码强度检测
|
- **`register.html`** - 用户注册页面,支持邮箱验证码、实时验证、密码强度检测
|
||||||
- **`user_management.html`** - 用户管理页面,管理员专用,用户增删改查、权限管理
|
- **`js/app.js`** - 主要JavaScript逻辑,包含所有功能模块:前端交互、API调用、实时更新、数据管理、用户界面控制
|
||||||
- **`data_management.html`** - 数据管理页面,支持Excel导入导出、数据备份、批量操作
|
- **`css/`** - 模块化样式文件,包含布局、组件、主题等分类样式,响应式设计,支持明暗主题切换
|
||||||
- **`log_management.html`** - 日志管理页面,实时日志查看、日志搜索过滤、日志下载
|
|
||||||
- **`item_search.html`** - 商品搜索页面,获取真实闲鱼商品数据,支持多条件搜索
|
|
||||||
- **`js/app.js`** - 主要JavaScript逻辑,处理前端交互、API调用、实时更新
|
|
||||||
- **`css/`** - 模块化样式文件,包含布局、组件、主题等分类样式,响应式设计
|
|
||||||
- **`xianyu_js_version_2.js`** - 闲鱼JavaScript工具库,加密解密、数据处理、API封装
|
- **`xianyu_js_version_2.js`** - 闲鱼JavaScript工具库,加密解密、数据处理、API封装
|
||||||
- **`lib/`** - 前端依赖库,包含Bootstrap、jQuery、Chart.js等第三方库
|
- **`lib/`** - 前端依赖库,包含Bootstrap 5、Bootstrap Icons等第三方库
|
||||||
|
- **`uploads/images/`** - 图片上传目录,支持发货图片和其他媒体文件存储
|
||||||
|
|
||||||
### 🐳 部署配置
|
### 🐳 部署配置
|
||||||
- **`Dockerfile`** - Docker镜像构建文件,包含Python环境、Playwright浏览器、系统依赖,支持无头模式运行
|
- **`Dockerfile`** - Docker镜像构建文件,基于Python 3.11-slim,包含Playwright浏览器、系统依赖,支持无头模式运行,优化构建层级
|
||||||
- **`docker-compose.yml`** - Docker Compose配置,支持一键部署、环境变量配置、资源限制、健康检查
|
- **`Dockerfile-cn`** - 国内优化版Docker镜像构建文件,使用国内镜像源加速构建,适合国内网络环境
|
||||||
- **`docker-deploy.sh`** - Docker部署管理脚本,提供构建、启动、监控、日志查看等功能(Linux/macOS)
|
- **`docker-compose.yml`** - Docker Compose配置,支持一键部署、完整环境变量配置、资源限制、健康检查、可选Nginx代理
|
||||||
- **`docker-deploy.bat`** - Windows版本部署脚本,支持Windows环境一键部署
|
- **`docker-compose-cn.yml`** - 国内优化版Docker Compose配置文件,使用国内镜像源
|
||||||
- **`entrypoint.sh`** - Docker容器启动脚本,处理环境初始化和服务启动
|
- **`docker-deploy.sh`** - Docker部署管理脚本,提供构建、启动、停止、重启、监控、日志查看等功能(Linux/macOS)
|
||||||
- **`nginx/nginx.conf`** - Nginx反向代理配置,支持负载均衡、SSL终端、WebSocket代理
|
- **`docker-deploy.bat`** - Windows版本部署脚本,支持Windows环境一键部署和管理
|
||||||
- **`requirements.txt`** - Python依赖包列表,精简版本无冗余依赖,按功能分类组织,包含详细说明
|
- **`entrypoint.sh`** - Docker容器启动脚本,处理环境初始化、目录创建、权限设置和服务启动
|
||||||
- **`.gitignore`** - Git忽略文件配置,完整覆盖Python、Docker、前端等开发文件
|
- **`nginx/nginx.conf`** - Nginx反向代理配置,支持负载均衡、SSL终端、WebSocket代理、静态文件服务
|
||||||
- **`.dockerignore`** - Docker构建忽略文件,优化构建上下文大小和构建速度
|
- **`requirements.txt`** - Python依赖包列表,精简版本无内置模块,按功能分类组织,包含详细版本说明和安装指南
|
||||||
- **`Dockerfile-cn`** - 国内优化版Docker镜像构建文件,使用国内镜像源加速构建
|
- **`.gitignore`** - Git忽略文件配置,完整覆盖Python、Docker、前端、测试、临时文件等,支持项目特定文件类型
|
||||||
- **`docker-compose-cn.yml`** - 国内优化版Docker Compose配置文件
|
- **`.dockerignore`** - Docker构建忽略文件,优化构建上下文大小和构建速度,排除不必要的文件和目录
|
||||||
|
|
||||||
## 🏗️ 详细技术架构
|
## 🏗️ 详细技术架构
|
||||||
|
|
||||||
|
10
Start.py
10
Start.py
@ -6,6 +6,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import asyncio
|
import asyncio
|
||||||
import threading
|
import threading
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@ -13,6 +14,15 @@ from urllib.parse import urlparse
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
# 修复Linux环境下的asyncio子进程问题
|
||||||
|
if sys.platform.startswith('linux'):
|
||||||
|
try:
|
||||||
|
# 在程序启动时就设置正确的事件循环策略
|
||||||
|
asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy())
|
||||||
|
logger.debug("已设置事件循环策略以支持子进程")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"设置事件循环策略失败: {e}")
|
||||||
|
|
||||||
from config import AUTO_REPLY, COOKIES_LIST
|
from config import AUTO_REPLY, COOKIES_LIST
|
||||||
import cookie_manager as cm
|
import cookie_manager as cm
|
||||||
from db_manager import db_manager
|
from db_manager import db_manager
|
||||||
|
1061
XianyuAutoAsync.py
1061
XianyuAutoAsync.py
File diff suppressed because it is too large
Load Diff
@ -354,7 +354,7 @@ class DBManager:
|
|||||||
CREATE TABLE IF NOT EXISTS notification_channels (
|
CREATE TABLE IF NOT EXISTS notification_channels (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
type TEXT NOT NULL CHECK (type IN ('qq')),
|
type TEXT NOT NULL CHECK (type IN ('qq','ding_talk','dingtalk','feishu','lark','bark','email','webhook','wechat','telegram')),
|
||||||
config TEXT NOT NULL,
|
config TEXT NOT NULL,
|
||||||
enabled BOOLEAN DEFAULT TRUE,
|
enabled BOOLEAN DEFAULT TRUE,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
@ -568,6 +568,14 @@ class DBManager:
|
|||||||
self.set_system_setting("db_version", "1.3", "数据库版本号")
|
self.set_system_setting("db_version", "1.3", "数据库版本号")
|
||||||
logger.info("数据库升级到版本1.3完成")
|
logger.info("数据库升级到版本1.3完成")
|
||||||
|
|
||||||
|
|
||||||
|
# 升级到版本1.4 - 添加关键词类型和图片URL字段
|
||||||
|
if current_version < "1.4":
|
||||||
|
logger.info("开始升级数据库到版本1.4...")
|
||||||
|
self.upgrade_notification_channels_types(cursor)
|
||||||
|
self.set_system_setting("db_version", "1.4", "数据库版本号")
|
||||||
|
logger.info("数据库升级到版本1.4完成")
|
||||||
|
|
||||||
# 迁移遗留数据(在所有版本升级完成后执行)
|
# 迁移遗留数据(在所有版本升级完成后执行)
|
||||||
self.migrate_legacy_data(cursor)
|
self.migrate_legacy_data(cursor)
|
||||||
|
|
||||||
@ -803,13 +811,13 @@ class DBManager:
|
|||||||
existing_data = cursor.fetchall()
|
existing_data = cursor.fetchall()
|
||||||
logger.info(f"备份 {count} 条通知渠道数据")
|
logger.info(f"备份 {count} 条通知渠道数据")
|
||||||
|
|
||||||
# 创建新表,支持更多渠道类型
|
# 创建新表,支持所有通知渠道类型
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
CREATE TABLE notification_channels_new (
|
CREATE TABLE notification_channels_new (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
type TEXT NOT NULL CHECK (type IN ('qq','ding_talk','dingtalk','email','webhook','wechat','telegram')),
|
type TEXT NOT NULL CHECK (type IN ('qq','ding_talk','dingtalk','feishu','lark','bark','email','webhook','wechat','telegram')),
|
||||||
config TEXT NOT NULL,
|
config TEXT NOT NULL,
|
||||||
enabled BOOLEAN DEFAULT TRUE,
|
enabled BOOLEAN DEFAULT TRUE,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
@ -824,15 +832,18 @@ class DBManager:
|
|||||||
# 处理类型映射,支持更多渠道类型
|
# 处理类型映射,支持更多渠道类型
|
||||||
old_type = row[3] if len(row) > 3 else 'qq' # type字段
|
old_type = row[3] if len(row) > 3 else 'qq' # type字段
|
||||||
|
|
||||||
# 扩展的类型映射规则
|
# 完整的类型映射规则,支持所有通知渠道
|
||||||
type_mapping = {
|
type_mapping = {
|
||||||
'ding_talk': 'dingtalk', # 统一为dingtalk
|
'ding_talk': 'dingtalk', # 统一为dingtalk
|
||||||
'dingtalk': 'dingtalk',
|
'dingtalk': 'dingtalk',
|
||||||
'qq': 'qq',
|
'qq': 'qq',
|
||||||
'email': 'email', # 现在支持email
|
'feishu': 'feishu', # 飞书通知
|
||||||
'webhook': 'webhook', # 现在支持webhook
|
'lark': 'lark', # 飞书通知(英文名)
|
||||||
'wechat': 'wechat', # 现在支持wechat
|
'bark': 'bark', # Bark通知
|
||||||
'telegram': 'telegram' # 现在支持telegram
|
'email': 'email', # 邮件通知
|
||||||
|
'webhook': 'webhook', # Webhook通知
|
||||||
|
'wechat': 'wechat', # 微信通知
|
||||||
|
'telegram': 'telegram' # Telegram通知
|
||||||
}
|
}
|
||||||
|
|
||||||
new_type = type_mapping.get(old_type, 'qq') # 默认为qq
|
new_type = type_mapping.get(old_type, 'qq') # 默认为qq
|
||||||
@ -863,6 +874,15 @@ class DBManager:
|
|||||||
cursor.execute("ALTER TABLE notification_channels_new RENAME TO notification_channels")
|
cursor.execute("ALTER TABLE notification_channels_new RENAME TO notification_channels")
|
||||||
|
|
||||||
logger.info("notification_channels表类型升级完成")
|
logger.info("notification_channels表类型升级完成")
|
||||||
|
logger.info("✅ 现在支持以下所有通知渠道类型:")
|
||||||
|
logger.info(" - qq (QQ通知)")
|
||||||
|
logger.info(" - ding_talk/dingtalk (钉钉通知)")
|
||||||
|
logger.info(" - feishu/lark (飞书通知)")
|
||||||
|
logger.info(" - bark (Bark通知)")
|
||||||
|
logger.info(" - email (邮件通知)")
|
||||||
|
logger.info(" - webhook (Webhook通知)")
|
||||||
|
logger.info(" - wechat (微信通知)")
|
||||||
|
logger.info(" - telegram (Telegram通知)")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"升级notification_channels表类型失败: {e}")
|
logger.error(f"升级notification_channels表类型失败: {e}")
|
||||||
@ -1190,7 +1210,7 @@ class DBManager:
|
|||||||
'user_id': result[2],
|
'user_id': result[2],
|
||||||
'auto_confirm': bool(result[3]),
|
'auto_confirm': bool(result[3]),
|
||||||
'remark': result[4] or '',
|
'remark': result[4] or '',
|
||||||
'pause_duration': result[5] or 10,
|
'pause_duration': result[5] if result[5] is not None else 10,
|
||||||
'created_at': result[6]
|
'created_at': result[6]
|
||||||
}
|
}
|
||||||
return None
|
return None
|
||||||
@ -1245,11 +1265,19 @@ class DBManager:
|
|||||||
self._execute_sql(cursor, "SELECT pause_duration FROM cookies WHERE id = ?", (cookie_id,))
|
self._execute_sql(cursor, "SELECT pause_duration FROM cookies WHERE id = ?", (cookie_id,))
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
if result:
|
if result:
|
||||||
return result[0] or 10 # 默认10分钟
|
if result[0] is None:
|
||||||
return 10 # 如果没有找到记录,返回默认值
|
logger.warning(f"账号 {cookie_id} 的pause_duration为NULL,使用默认值10分钟并修复数据库")
|
||||||
|
# 修复数据库中的NULL值
|
||||||
|
self._execute_sql(cursor, "UPDATE cookies SET pause_duration = 10 WHERE id = ?", (cookie_id,))
|
||||||
|
self.conn.commit()
|
||||||
|
return 10
|
||||||
|
return result[0] # 返回实际值,不使用or操作符
|
||||||
|
else:
|
||||||
|
logger.warning(f"账号 {cookie_id} 未找到记录,使用默认值10分钟")
|
||||||
|
return 10
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取账号自动回复暂停时间失败: {e}")
|
logger.error(f"获取账号自动回复暂停时间失败: {e}")
|
||||||
return 10 # 出错时返回默认值
|
return 10
|
||||||
|
|
||||||
def get_auto_confirm(self, cookie_id: str) -> bool:
|
def get_auto_confirm(self, cookie_id: str) -> bool:
|
||||||
"""获取Cookie的自动确认发货设置"""
|
"""获取Cookie的自动确认发货设置"""
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "🚀 启动闲鱼自动回复系统..."
|
echo "Starting xianyu-auto-reply system..."
|
||||||
echo "📊 数据库将在应用启动时自动初始化..."
|
|
||||||
echo "🎯 启动主应用..."
|
|
||||||
|
|
||||||
# 确保数据目录存在
|
# Create necessary directories
|
||||||
mkdir -p /app/data /app/logs /app/backups /app/static/uploads/images
|
mkdir -p /app/data /app/logs /app/backups /app/static/uploads/images
|
||||||
|
|
||||||
# 设置目录权限
|
# Set permissions
|
||||||
chmod 777 /app/data /app/logs /app/backups /app/static/uploads /app/static/uploads/images
|
chmod 777 /app/data /app/logs /app/backups /app/static/uploads /app/static/uploads/images
|
||||||
|
|
||||||
# 启动主应用
|
# Start the application
|
||||||
exec python Start.py
|
exec python Start.py
|
||||||
|
@ -45,7 +45,7 @@ DEFAULT_HEADERS:
|
|||||||
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
|
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
|
like Gecko) Chrome/133.0.0.0 Safari/537.36
|
||||||
HEARTBEAT_INTERVAL: 15
|
HEARTBEAT_INTERVAL: 15
|
||||||
HEARTBEAT_TIMEOUT: 5
|
HEARTBEAT_TIMEOUT: 30
|
||||||
LOG_CONFIG:
|
LOG_CONFIG:
|
||||||
compression: zip
|
compression: zip
|
||||||
format: '<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level>
|
format: '<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level>
|
||||||
@ -58,7 +58,7 @@ MANUAL_MODE:
|
|||||||
timeout: 3600
|
timeout: 3600
|
||||||
toggle_keywords: []
|
toggle_keywords: []
|
||||||
MESSAGE_EXPIRE_TIME: 300000
|
MESSAGE_EXPIRE_TIME: 300000
|
||||||
TOKEN_REFRESH_INTERVAL: 72000 # 从3600秒(1小时)增加到18000秒(20小时)
|
TOKEN_REFRESH_INTERVAL: 72000 # 从3600秒(1小时)增加到72000秒(20小时)
|
||||||
TOKEN_RETRY_INTERVAL: 7200 # 从300秒(5分钟)增加到7200秒(2小时)
|
TOKEN_RETRY_INTERVAL: 7200 # 从300秒(5分钟)增加到7200秒(2小时)
|
||||||
WEBSOCKET_HEADERS:
|
WEBSOCKET_HEADERS:
|
||||||
Accept-Encoding: gzip, deflate, br, zstd
|
Accept-Encoding: gzip, deflate, br, zstd
|
||||||
|
355
reply_server.py
355
reply_server.py
@ -14,6 +14,8 @@ import os
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import io
|
import io
|
||||||
|
import asyncio
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import cookie_manager
|
import cookie_manager
|
||||||
from db_manager import db_manager
|
from db_manager import db_manager
|
||||||
@ -36,9 +38,30 @@ TOKEN_EXPIRE_TIME = 24 * 60 * 60 # token过期时间:24小时
|
|||||||
# HTTP Bearer认证
|
# HTTP Bearer认证
|
||||||
security = HTTPBearer(auto_error=False)
|
security = HTTPBearer(auto_error=False)
|
||||||
|
|
||||||
|
# 扫码登录检查锁 - 防止并发处理同一个session
|
||||||
|
qr_check_locks = defaultdict(lambda: asyncio.Lock())
|
||||||
|
qr_check_processed = {} # 记录已处理的session: {session_id: {'processed': bool, 'timestamp': float}}
|
||||||
|
|
||||||
# 不再需要单独的密码初始化,由数据库初始化时处理
|
# 不再需要单独的密码初始化,由数据库初始化时处理
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_qr_check_records():
|
||||||
|
"""清理过期的扫码检查记录"""
|
||||||
|
current_time = time.time()
|
||||||
|
expired_sessions = []
|
||||||
|
|
||||||
|
for session_id, record in qr_check_processed.items():
|
||||||
|
# 清理超过1小时的记录
|
||||||
|
if current_time - record['timestamp'] > 3600:
|
||||||
|
expired_sessions.append(session_id)
|
||||||
|
|
||||||
|
for session_id in expired_sessions:
|
||||||
|
if session_id in qr_check_processed:
|
||||||
|
del qr_check_processed[session_id]
|
||||||
|
if session_id in qr_check_locks:
|
||||||
|
del qr_check_locks[session_id]
|
||||||
|
|
||||||
|
|
||||||
def load_keywords() -> List[Tuple[str, str]]:
|
def load_keywords() -> List[Tuple[str, str]]:
|
||||||
"""读取关键字→回复映射表
|
"""读取关键字→回复映射表
|
||||||
|
|
||||||
@ -1038,26 +1061,57 @@ async def generate_qr_code(current_user: Dict[str, Any] = Depends(get_current_us
|
|||||||
async def check_qr_code_status(session_id: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
async def check_qr_code_status(session_id: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||||
"""检查扫码登录状态"""
|
"""检查扫码登录状态"""
|
||||||
try:
|
try:
|
||||||
# 清理过期会话
|
# 清理过期记录
|
||||||
qr_login_manager.cleanup_expired_sessions()
|
cleanup_qr_check_records()
|
||||||
|
|
||||||
# 获取会话状态
|
# 检查是否已经处理过
|
||||||
status_info = qr_login_manager.get_session_status(session_id)
|
if session_id in qr_check_processed:
|
||||||
|
record = qr_check_processed[session_id]
|
||||||
|
if record['processed']:
|
||||||
|
log_with_user('debug', f"扫码登录session {session_id} 已处理过,直接返回", current_user)
|
||||||
|
# 返回简单的成功状态,避免重复处理
|
||||||
|
return {'status': 'already_processed', 'message': '该会话已处理完成'}
|
||||||
|
|
||||||
if status_info['status'] == 'success':
|
# 获取该session的锁
|
||||||
# 登录成功,处理Cookie
|
session_lock = qr_check_locks[session_id]
|
||||||
cookies_info = qr_login_manager.get_session_cookies(session_id)
|
|
||||||
if cookies_info:
|
|
||||||
account_info = await process_qr_login_cookies(
|
|
||||||
cookies_info['cookies'],
|
|
||||||
cookies_info['unb'],
|
|
||||||
current_user
|
|
||||||
)
|
|
||||||
status_info['account_info'] = account_info
|
|
||||||
|
|
||||||
log_with_user('info', f"扫码登录成功处理完成: {session_id}, 账号: {account_info.get('account_id', 'unknown')}", current_user)
|
# 使用非阻塞方式尝试获取锁
|
||||||
|
if session_lock.locked():
|
||||||
|
log_with_user('debug', f"扫码登录session {session_id} 正在被其他请求处理,跳过", current_user)
|
||||||
|
return {'status': 'processing', 'message': '正在处理中,请稍候...'}
|
||||||
|
|
||||||
return status_info
|
async with session_lock:
|
||||||
|
# 再次检查是否已处理(双重检查)
|
||||||
|
if session_id in qr_check_processed and qr_check_processed[session_id]['processed']:
|
||||||
|
log_with_user('debug', f"扫码登录session {session_id} 在获取锁后发现已处理,直接返回", current_user)
|
||||||
|
return {'status': 'already_processed', 'message': '该会话已处理完成'}
|
||||||
|
|
||||||
|
# 清理过期会话
|
||||||
|
qr_login_manager.cleanup_expired_sessions()
|
||||||
|
|
||||||
|
# 获取会话状态
|
||||||
|
status_info = qr_login_manager.get_session_status(session_id)
|
||||||
|
|
||||||
|
if status_info['status'] == 'success':
|
||||||
|
# 登录成功,处理Cookie(现在包含获取真实cookie的逻辑)
|
||||||
|
cookies_info = qr_login_manager.get_session_cookies(session_id)
|
||||||
|
if cookies_info:
|
||||||
|
account_info = await process_qr_login_cookies(
|
||||||
|
cookies_info['cookies'],
|
||||||
|
cookies_info['unb'],
|
||||||
|
current_user
|
||||||
|
)
|
||||||
|
status_info['account_info'] = account_info
|
||||||
|
|
||||||
|
log_with_user('info', f"扫码登录处理完成: {session_id}, 账号: {account_info.get('account_id', 'unknown')}", current_user)
|
||||||
|
|
||||||
|
# 标记该session已处理
|
||||||
|
qr_check_processed[session_id] = {
|
||||||
|
'processed': True,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
return status_info
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_with_user('error', f"检查扫码登录状态异常: {str(e)}", current_user)
|
log_with_user('error', f"检查扫码登录状态异常: {str(e)}", current_user)
|
||||||
@ -1065,7 +1119,7 @@ async def check_qr_code_status(session_id: str, current_user: Dict[str, Any] = D
|
|||||||
|
|
||||||
|
|
||||||
async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[str, Any]) -> Dict[str, Any]:
|
async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""处理扫码登录获取的Cookie"""
|
"""处理扫码登录获取的Cookie - 先获取真实cookie再保存到数据库"""
|
||||||
try:
|
try:
|
||||||
user_id = current_user['user_id']
|
user_id = current_user['user_id']
|
||||||
|
|
||||||
@ -1083,20 +1137,11 @@ async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[st
|
|||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# 确定账号ID
|
||||||
if existing_account_id:
|
if existing_account_id:
|
||||||
# 更新现有账号的Cookie
|
account_id = existing_account_id
|
||||||
db_manager.save_cookie(existing_account_id, cookies, user_id)
|
is_new_account = False
|
||||||
|
log_with_user('info', f"扫码登录找到现有账号: {account_id}, UNB: {unb}", current_user)
|
||||||
# 更新cookie_manager中的Cookie
|
|
||||||
if cookie_manager.manager:
|
|
||||||
cookie_manager.manager.update_cookie(existing_account_id, cookies)
|
|
||||||
|
|
||||||
log_with_user('info', f"扫码登录更新现有账号Cookie: {existing_account_id}, UNB: {unb}", current_user)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'account_id': existing_account_id,
|
|
||||||
'is_new_account': False
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
# 创建新账号,使用unb作为账号ID
|
# 创建新账号,使用unb作为账号ID
|
||||||
account_id = unb
|
account_id = unb
|
||||||
@ -1108,25 +1153,255 @@ async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[st
|
|||||||
account_id = f"{original_account_id}_{counter}"
|
account_id = f"{original_account_id}_{counter}"
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
# 保存新账号
|
is_new_account = True
|
||||||
db_manager.save_cookie(account_id, cookies, user_id)
|
log_with_user('info', f"扫码登录准备创建新账号: {account_id}, UNB: {unb}", current_user)
|
||||||
|
|
||||||
# 添加到cookie_manager
|
# 第一步:使用扫码cookie获取真实cookie
|
||||||
if cookie_manager.manager:
|
log_with_user('info', f"开始使用扫码cookie获取真实cookie: {account_id}", current_user)
|
||||||
cookie_manager.manager.add_cookie(account_id, cookies)
|
|
||||||
|
|
||||||
log_with_user('info', f"扫码登录创建新账号: {account_id}, UNB: {unb}", current_user)
|
try:
|
||||||
|
# 创建一个临时的XianyuLive实例来执行cookie刷新
|
||||||
|
from XianyuAutoAsync import XianyuLive
|
||||||
|
|
||||||
return {
|
# 使用扫码登录的cookie创建临时实例
|
||||||
'account_id': account_id,
|
temp_instance = XianyuLive(
|
||||||
'is_new_account': True
|
cookies_str=cookies,
|
||||||
}
|
cookie_id=account_id,
|
||||||
|
user_id=user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# 执行cookie刷新获取真实cookie
|
||||||
|
refresh_success = await temp_instance.refresh_cookies_from_qr_login(
|
||||||
|
qr_cookies_str=cookies,
|
||||||
|
cookie_id=account_id,
|
||||||
|
user_id=user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if refresh_success:
|
||||||
|
log_with_user('info', f"扫码登录真实cookie获取成功: {account_id}", current_user)
|
||||||
|
|
||||||
|
# 从数据库获取刚刚保存的真实cookie
|
||||||
|
updated_cookie_info = db_manager.get_cookie_by_id(account_id)
|
||||||
|
if updated_cookie_info:
|
||||||
|
real_cookies = updated_cookie_info['cookies_str']
|
||||||
|
log_with_user('info', f"已获取真实cookie,长度: {len(real_cookies)}", current_user)
|
||||||
|
|
||||||
|
# 第二步:将真实cookie添加到cookie_manager(如果是新账号)或更新现有账号
|
||||||
|
if cookie_manager.manager:
|
||||||
|
if is_new_account:
|
||||||
|
cookie_manager.manager.add_cookie(account_id, real_cookies)
|
||||||
|
log_with_user('info', f"已将真实cookie添加到cookie_manager: {account_id}", current_user)
|
||||||
|
else:
|
||||||
|
cookie_manager.manager.update_cookie(account_id, real_cookies)
|
||||||
|
log_with_user('info', f"已更新cookie_manager中的真实cookie: {account_id}", current_user)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'account_id': account_id,
|
||||||
|
'is_new_account': is_new_account,
|
||||||
|
'real_cookie_refreshed': True,
|
||||||
|
'cookie_length': len(real_cookies)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
log_with_user('error', f"无法从数据库获取真实cookie: {account_id}", current_user)
|
||||||
|
# 降级处理:使用原始扫码cookie
|
||||||
|
return await _fallback_save_qr_cookie(account_id, cookies, user_id, is_new_account, current_user, "无法从数据库获取真实cookie")
|
||||||
|
else:
|
||||||
|
log_with_user('warning', f"扫码登录真实cookie获取失败: {account_id}", current_user)
|
||||||
|
# 降级处理:使用原始扫码cookie
|
||||||
|
return await _fallback_save_qr_cookie(account_id, cookies, user_id, is_new_account, current_user, "真实cookie获取失败")
|
||||||
|
|
||||||
|
except Exception as refresh_e:
|
||||||
|
log_with_user('error', f"扫码登录真实cookie获取异常: {str(refresh_e)}", current_user)
|
||||||
|
# 降级处理:使用原始扫码cookie
|
||||||
|
return await _fallback_save_qr_cookie(account_id, cookies, user_id, is_new_account, current_user, f"获取真实cookie异常: {str(refresh_e)}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_with_user('error', f"处理扫码登录Cookie失败: {str(e)}", current_user)
|
log_with_user('error', f"处理扫码登录Cookie失败: {str(e)}", current_user)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
async def _fallback_save_qr_cookie(account_id: str, cookies: str, user_id: int, is_new_account: bool, current_user: Dict[str, Any], error_reason: str) -> Dict[str, Any]:
|
||||||
|
"""降级处理:当无法获取真实cookie时,保存原始扫码cookie"""
|
||||||
|
try:
|
||||||
|
log_with_user('warning', f"降级处理 - 保存原始扫码cookie: {account_id}, 原因: {error_reason}", current_user)
|
||||||
|
|
||||||
|
# 保存原始扫码cookie到数据库
|
||||||
|
if is_new_account:
|
||||||
|
db_manager.save_cookie(account_id, cookies, user_id)
|
||||||
|
log_with_user('info', f"降级处理 - 新账号原始cookie已保存: {account_id}", current_user)
|
||||||
|
else:
|
||||||
|
db_manager.save_cookie(account_id, cookies, user_id)
|
||||||
|
log_with_user('info', f"降级处理 - 现有账号原始cookie已更新: {account_id}", current_user)
|
||||||
|
|
||||||
|
# 添加到或更新cookie_manager
|
||||||
|
if cookie_manager.manager:
|
||||||
|
if is_new_account:
|
||||||
|
cookie_manager.manager.add_cookie(account_id, cookies)
|
||||||
|
log_with_user('info', f"降级处理 - 已将原始cookie添加到cookie_manager: {account_id}", current_user)
|
||||||
|
else:
|
||||||
|
cookie_manager.manager.update_cookie(account_id, cookies)
|
||||||
|
log_with_user('info', f"降级处理 - 已更新cookie_manager中的原始cookie: {account_id}", current_user)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'account_id': account_id,
|
||||||
|
'is_new_account': is_new_account,
|
||||||
|
'real_cookie_refreshed': False,
|
||||||
|
'fallback_reason': error_reason,
|
||||||
|
'cookie_length': len(cookies)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as fallback_e:
|
||||||
|
log_with_user('error', f"降级处理失败: {str(fallback_e)}", current_user)
|
||||||
|
raise fallback_e
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/qr-login/refresh-cookies")
|
||||||
|
async def refresh_cookies_from_qr_login(
|
||||||
|
request: Dict[str, Any],
|
||||||
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""使用扫码登录获取的cookie访问指定界面获取真实cookie并存入数据库"""
|
||||||
|
try:
|
||||||
|
qr_cookies = request.get('qr_cookies')
|
||||||
|
cookie_id = request.get('cookie_id')
|
||||||
|
|
||||||
|
if not qr_cookies:
|
||||||
|
return {'success': False, 'message': '缺少扫码登录cookie'}
|
||||||
|
|
||||||
|
if not cookie_id:
|
||||||
|
return {'success': False, 'message': '缺少cookie_id'}
|
||||||
|
|
||||||
|
log_with_user('info', f"开始使用扫码cookie刷新真实cookie: {cookie_id}", current_user)
|
||||||
|
|
||||||
|
# 创建一个临时的XianyuLive实例来执行cookie刷新
|
||||||
|
from XianyuAutoAsync import XianyuLive
|
||||||
|
|
||||||
|
# 使用扫码登录的cookie创建临时实例
|
||||||
|
temp_instance = XianyuLive(
|
||||||
|
cookies_str=qr_cookies,
|
||||||
|
cookie_id=cookie_id,
|
||||||
|
user_id=current_user['user_id']
|
||||||
|
)
|
||||||
|
|
||||||
|
# 执行cookie刷新
|
||||||
|
success = await temp_instance.refresh_cookies_from_qr_login(
|
||||||
|
qr_cookies_str=qr_cookies,
|
||||||
|
cookie_id=cookie_id,
|
||||||
|
user_id=current_user['user_id']
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
log_with_user('info', f"扫码cookie刷新成功: {cookie_id}", current_user)
|
||||||
|
|
||||||
|
# 如果cookie_manager存在,更新其中的cookie
|
||||||
|
if cookie_manager.manager:
|
||||||
|
# 从数据库获取更新后的cookie
|
||||||
|
updated_cookie_info = db_manager.get_cookie_by_id(cookie_id)
|
||||||
|
if updated_cookie_info:
|
||||||
|
cookie_manager.manager.update_cookie(cookie_id, updated_cookie_info['cookies_str'])
|
||||||
|
log_with_user('info', f"已更新cookie_manager中的cookie: {cookie_id}", current_user)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': '真实cookie获取并保存成功',
|
||||||
|
'cookie_id': cookie_id
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
log_with_user('error', f"扫码cookie刷新失败: {cookie_id}", current_user)
|
||||||
|
return {'success': False, 'message': '获取真实cookie失败'}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log_with_user('error', f"扫码cookie刷新异常: {str(e)}", current_user)
|
||||||
|
return {'success': False, 'message': f'刷新cookie失败: {str(e)}'}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/qr-login/reset-cooldown/{cookie_id}")
|
||||||
|
async def reset_qr_cookie_refresh_cooldown(
|
||||||
|
cookie_id: str,
|
||||||
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""重置指定账号的扫码登录Cookie刷新冷却时间"""
|
||||||
|
try:
|
||||||
|
log_with_user('info', f"重置扫码登录Cookie刷新冷却时间: {cookie_id}", current_user)
|
||||||
|
|
||||||
|
# 检查cookie是否存在
|
||||||
|
cookie_info = db_manager.get_cookie_by_id(cookie_id)
|
||||||
|
if not cookie_info:
|
||||||
|
return {'success': False, 'message': '账号不存在'}
|
||||||
|
|
||||||
|
# 如果cookie_manager中有对应的实例,直接重置
|
||||||
|
if cookie_manager.manager and cookie_id in cookie_manager.manager.instances:
|
||||||
|
instance = cookie_manager.manager.instances[cookie_id]
|
||||||
|
remaining_time_before = instance.get_qr_cookie_refresh_remaining_time()
|
||||||
|
instance.reset_qr_cookie_refresh_flag()
|
||||||
|
|
||||||
|
log_with_user('info', f"已重置账号 {cookie_id} 的扫码登录冷却时间,原剩余时间: {remaining_time_before}秒", current_user)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': '扫码登录Cookie刷新冷却时间已重置',
|
||||||
|
'cookie_id': cookie_id,
|
||||||
|
'previous_remaining_time': remaining_time_before
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# 如果没有活跃实例,返回成功(因为没有冷却时间需要重置)
|
||||||
|
log_with_user('info', f"账号 {cookie_id} 没有活跃实例,无需重置冷却时间", current_user)
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': '账号没有活跃实例,无需重置冷却时间',
|
||||||
|
'cookie_id': cookie_id
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log_with_user('error', f"重置扫码登录冷却时间异常: {str(e)}", current_user)
|
||||||
|
return {'success': False, 'message': f'重置冷却时间失败: {str(e)}'}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/qr-login/cooldown-status/{cookie_id}")
|
||||||
|
async def get_qr_cookie_refresh_cooldown_status(
|
||||||
|
cookie_id: str,
|
||||||
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""获取指定账号的扫码登录Cookie刷新冷却状态"""
|
||||||
|
try:
|
||||||
|
# 检查cookie是否存在
|
||||||
|
cookie_info = db_manager.get_cookie_by_id(cookie_id)
|
||||||
|
if not cookie_info:
|
||||||
|
return {'success': False, 'message': '账号不存在'}
|
||||||
|
|
||||||
|
# 如果cookie_manager中有对应的实例,获取冷却状态
|
||||||
|
if cookie_manager.manager and cookie_id in cookie_manager.manager.instances:
|
||||||
|
instance = cookie_manager.manager.instances[cookie_id]
|
||||||
|
remaining_time = instance.get_qr_cookie_refresh_remaining_time()
|
||||||
|
cooldown_duration = instance.qr_cookie_refresh_cooldown
|
||||||
|
last_refresh_time = instance.last_qr_cookie_refresh_time
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'cookie_id': cookie_id,
|
||||||
|
'remaining_time': remaining_time,
|
||||||
|
'cooldown_duration': cooldown_duration,
|
||||||
|
'last_refresh_time': last_refresh_time,
|
||||||
|
'is_in_cooldown': remaining_time > 0,
|
||||||
|
'remaining_minutes': remaining_time // 60,
|
||||||
|
'remaining_seconds': remaining_time % 60
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'cookie_id': cookie_id,
|
||||||
|
'remaining_time': 0,
|
||||||
|
'cooldown_duration': 600, # 默认10分钟
|
||||||
|
'last_refresh_time': 0,
|
||||||
|
'is_in_cooldown': False,
|
||||||
|
'message': '账号没有活跃实例'
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log_with_user('error', f"获取扫码登录冷却状态异常: {str(e)}", current_user)
|
||||||
|
return {'success': False, 'message': f'获取冷却状态失败: {str(e)}'}
|
||||||
|
|
||||||
|
|
||||||
@app.put('/cookies/{cid}/status')
|
@app.put('/cookies/{cid}/status')
|
||||||
def update_cookie_status(cid: str, status_data: CookieStatusIn, current_user: Dict[str, Any] = Depends(get_current_user)):
|
def update_cookie_status(cid: str, status_data: CookieStatusIn, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||||
"""更新账号的启用/禁用状态"""
|
"""更新账号的启用/禁用状态"""
|
||||||
|
@ -78,4 +78,5 @@ xlsxwriter>=3.1.0
|
|||||||
# collections, itertools, functools, copy, pickle, gzip, zipfile, shutil
|
# collections, itertools, functools, copy, pickle, gzip, zipfile, shutil
|
||||||
# tempfile, io, csv, xml, html, http, socket, ssl, subprocess, signal
|
# tempfile, io, csv, xml, html, http, socket, ssl, subprocess, signal
|
||||||
# inspect, ast, enum, math, decimal, array, queue, contextlib, warnings
|
# inspect, ast, enum, math, decimal, array, queue, contextlib, warnings
|
||||||
# typing, dataclasses, weakref, gc, platform, stat, glob, fnmatch
|
# typing, dataclasses, weakref, gc, platform, stat, glob, fnmatch, mimetypes
|
||||||
|
# email, smtplib, imaplib, poplib, ftplib, telnetlib, configparser, argparse
|
@ -1,6 +1,16 @@
|
|||||||
/* ================================
|
/* ================================
|
||||||
通用卡片样式 - 适用于所有菜单的卡片
|
通用卡片样式 - 适用于所有菜单的卡片
|
||||||
================================ */
|
================================ */
|
||||||
|
|
||||||
|
/* 旋转动画 */
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
.card {
|
.card {
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
|
@ -147,11 +147,21 @@
|
|||||||
<!-- 仪表盘内容 -->
|
<!-- 仪表盘内容 -->
|
||||||
<div id="dashboard-section" class="content-section active">
|
<div id="dashboard-section" class="content-section active">
|
||||||
<div class="content-header">
|
<div class="content-header">
|
||||||
<h2 class="mb-0">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<i class="bi bi-speedometer2 me-2"></i>
|
<div>
|
||||||
仪表盘
|
<h2 class="mb-0">
|
||||||
</h2>
|
<i class="bi bi-speedometer2 me-2"></i>
|
||||||
<p class="text-muted mb-0">系统概览和统计信息</p>
|
仪表盘
|
||||||
|
</h2>
|
||||||
|
<p class="text-muted mb-0">系统概览和统计信息</p>
|
||||||
|
</div>
|
||||||
|
<div class="version-info">
|
||||||
|
<span class="badge bg-secondary" id="systemVersion">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
|
版本: <span id="versionNumber">加载中...</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-body">
|
<div class="content-body">
|
||||||
<div class="dashboard-stats">
|
<div class="dashboard-stats">
|
||||||
@ -234,17 +244,17 @@
|
|||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||||
<button type="button" class="btn btn-success btn-lg me-md-2 flex-fill qr-login-btn" onclick="showQRCodeLogin()" style="max-width: 300px;">
|
<button type="button" class="btn btn-lg me-md-2 flex-fill qr-login-btn" onclick="showQRCodeLogin()" style="max-width: 300px;">
|
||||||
<i class="bi bi-qr-code me-2"></i>
|
<i class="bi bi-qr-code me-2"></i>
|
||||||
<span class="fw-bold">扫码登录</span>
|
<span class="fw-bold">扫码登录</span>
|
||||||
<br>
|
<br>
|
||||||
<small class="opacity-75">推荐方式,安全便捷</small>
|
<small class="opacity-75">不推荐,一般都不成功</small>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary btn-lg flex-fill manual-input-btn" onclick="toggleManualInput()" style="max-width: 300px;">
|
<button type="button" class="btn btn-outline-secondary btn-lg flex-fill manual-input-btn" onclick="toggleManualInput()" style="max-width: 300px;">
|
||||||
<i class="bi bi-keyboard me-2"></i>
|
<i class="bi bi-keyboard me-2"></i>
|
||||||
<span class="fw-bold">手动输入</span>
|
<span class="fw-bold">手动输入</span>
|
||||||
<br>
|
<br>
|
||||||
<small class="opacity-75">输入Cookie信息</small>
|
<small class="opacity-75">推荐方式,使用消息界面的cookie</small>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1064,6 +1074,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-6 col-md-4 col-lg-3 col-xl-2">
|
||||||
|
<div class="card h-100 channel-type-card" onclick="showAddChannelModal('feishu')">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="channel-icon">
|
||||||
|
<i class="bi bi-chat-square-text-fill text-warning"></i>
|
||||||
|
</div>
|
||||||
|
<h6 class="card-title">飞书通知</h6>
|
||||||
|
<p class="card-text text-muted">飞书机器人消息</p>
|
||||||
|
<div class="mt-auto">
|
||||||
|
<button class="btn btn-outline-warning btn-sm">
|
||||||
|
<i class="bi bi-plus-circle me-1"></i>配置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-6 col-md-4 col-lg-3 col-xl-2">
|
||||||
|
<div class="card h-100 channel-type-card" onclick="showAddChannelModal('bark')">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="channel-icon">
|
||||||
|
<i class="bi bi-phone-fill text-dark"></i>
|
||||||
|
</div>
|
||||||
|
<h6 class="card-title">Bark通知</h6>
|
||||||
|
<p class="card-text text-muted">iOS推送通知</p>
|
||||||
|
<div class="mt-auto">
|
||||||
|
<button class="btn btn-outline-dark btn-sm">
|
||||||
|
<i class="bi bi-plus-circle me-1"></i>配置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-6 col-md-4 col-lg-3 col-xl-2">
|
<div class="col-sm-6 col-md-4 col-lg-3 col-xl-2">
|
||||||
<div class="card h-100 channel-type-card" onclick="showAddChannelModal('email')">
|
<div class="card h-100 channel-type-card" onclick="showAddChannelModal('email')">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
@ -2753,7 +2797,7 @@
|
|||||||
|
|
||||||
<!-- JS依赖 -->
|
<!-- JS依赖 -->
|
||||||
<script src="/static/lib/bootstrap/bootstrap.bundle.min.js"></script>
|
<script src="/static/lib/bootstrap/bootstrap.bundle.min.js"></script>
|
||||||
<script src="/static/js/app.js"></script>
|
<script src="/static/js/app.js?v=2.2.0"></script>
|
||||||
|
|
||||||
<!-- 默认回复管理模态框 -->
|
<!-- 默认回复管理模态框 -->
|
||||||
<div class="modal fade" id="defaultReplyModal" tabindex="-1" aria-labelledby="defaultReplyModalLabel" aria-hidden="true">
|
<div class="modal fade" id="defaultReplyModal" tabindex="-1" aria-labelledby="defaultReplyModalLabel" aria-hidden="true">
|
||||||
|
463
static/js/app.js
463
static/js/app.js
@ -1347,6 +1347,157 @@ function copyCookie(id, value) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 刷新真实Cookie
|
||||||
|
async function refreshRealCookie(cookieId) {
|
||||||
|
if (!cookieId) {
|
||||||
|
showToast('缺少账号ID', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前cookie值
|
||||||
|
try {
|
||||||
|
const cookieDetails = await fetchJSON(`${apiBase}/cookies/details`);
|
||||||
|
const currentCookie = cookieDetails.find(c => c.id === cookieId);
|
||||||
|
|
||||||
|
if (!currentCookie || !currentCookie.value) {
|
||||||
|
showToast('未找到有效的Cookie信息', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认操作
|
||||||
|
if (!confirm(`确定要刷新账号 "${cookieId}" 的真实Cookie吗?\n\n此操作将使用当前Cookie访问闲鱼IM界面获取最新的真实Cookie。`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
const button = event.target.closest('button');
|
||||||
|
const originalContent = button.innerHTML;
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerHTML = '<i class="bi bi-arrow-clockwise spin"></i>';
|
||||||
|
|
||||||
|
// 调用刷新API
|
||||||
|
const response = await fetch(`${apiBase}/qr-login/refresh-cookies`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${authToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
qr_cookies: currentCookie.value,
|
||||||
|
cookie_id: cookieId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showToast(`账号 "${cookieId}" 真实Cookie刷新成功`, 'success');
|
||||||
|
// 刷新账号列表以显示更新后的cookie
|
||||||
|
loadCookies();
|
||||||
|
} else {
|
||||||
|
showToast(`真实Cookie刷新失败: ${result.message}`, 'danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新真实Cookie失败:', error);
|
||||||
|
showToast(`刷新真实Cookie失败: ${error.message || '未知错误'}`, 'danger');
|
||||||
|
} finally {
|
||||||
|
// 恢复按钮状态
|
||||||
|
const button = event.target.closest('button');
|
||||||
|
if (button) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = '<i class="bi bi-arrow-clockwise"></i>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示冷却状态
|
||||||
|
async function showCooldownStatus(cookieId) {
|
||||||
|
if (!cookieId) {
|
||||||
|
showToast('缺少账号ID', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiBase}/qr-login/cooldown-status/${cookieId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${authToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const { remaining_time, cooldown_duration, is_in_cooldown, remaining_minutes, remaining_seconds } = result;
|
||||||
|
|
||||||
|
let statusMessage = `账号: ${cookieId}\n`;
|
||||||
|
statusMessage += `冷却时长: ${cooldown_duration / 60}分钟\n`;
|
||||||
|
|
||||||
|
if (is_in_cooldown) {
|
||||||
|
statusMessage += `冷却状态: 进行中\n`;
|
||||||
|
statusMessage += `剩余时间: ${remaining_minutes}分${remaining_seconds}秒\n\n`;
|
||||||
|
statusMessage += `在冷却期间,_refresh_cookies_via_browser 方法将被跳过。\n\n`;
|
||||||
|
statusMessage += `是否要重置冷却时间?`;
|
||||||
|
|
||||||
|
if (confirm(statusMessage)) {
|
||||||
|
await resetCooldownTime(cookieId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
statusMessage += `冷却状态: 无冷却\n`;
|
||||||
|
statusMessage += `可以正常执行 _refresh_cookies_via_browser 方法`;
|
||||||
|
alert(statusMessage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showToast(`获取冷却状态失败: ${result.message}`, 'danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取冷却状态失败:', error);
|
||||||
|
showToast(`获取冷却状态失败: ${error.message || '未知错误'}`, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置冷却时间
|
||||||
|
async function resetCooldownTime(cookieId) {
|
||||||
|
if (!cookieId) {
|
||||||
|
showToast('缺少账号ID', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiBase}/qr-login/reset-cooldown/${cookieId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${authToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const previousTime = result.previous_remaining_time || 0;
|
||||||
|
const previousMinutes = Math.floor(previousTime / 60);
|
||||||
|
const previousSeconds = previousTime % 60;
|
||||||
|
|
||||||
|
let message = `账号 "${cookieId}" 的扫码登录冷却时间已重置`;
|
||||||
|
if (previousTime > 0) {
|
||||||
|
message += `\n原剩余时间: ${previousMinutes}分${previousSeconds}秒`;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(message, 'success');
|
||||||
|
} else {
|
||||||
|
showToast(`重置冷却时间失败: ${result.message}`, 'danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重置冷却时间失败:', error);
|
||||||
|
showToast(`重置冷却时间失败: ${error.message || '未知错误'}`, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 删除Cookie
|
// 删除Cookie
|
||||||
async function delCookie(id) {
|
async function delCookie(id) {
|
||||||
if (!confirm(`确定要删除账号 "${id}" 吗?此操作不可恢复。`)) return;
|
if (!confirm(`确定要删除账号 "${id}" 吗?此操作不可恢复。`)) return;
|
||||||
@ -1752,6 +1903,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// 首先检查认证状态
|
// 首先检查认证状态
|
||||||
const isAuthenticated = await checkAuth();
|
const isAuthenticated = await checkAuth();
|
||||||
if (!isAuthenticated) return;
|
if (!isAuthenticated) return;
|
||||||
|
|
||||||
|
// 加载系统版本号
|
||||||
|
loadSystemVersion();
|
||||||
// 添加Cookie表单提交
|
// 添加Cookie表单提交
|
||||||
document.getElementById('addForm').addEventListener('submit', async (e) => {
|
document.getElementById('addForm').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -2450,6 +2604,78 @@ const channelTypeConfigs = {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
feishu: {
|
||||||
|
title: '飞书通知',
|
||||||
|
description: '请设置飞书机器人Webhook URL,支持自定义机器人和群机器人',
|
||||||
|
icon: 'bi-chat-square-text-fill',
|
||||||
|
color: 'warning',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
id: 'webhook_url',
|
||||||
|
label: '飞书机器人Webhook URL',
|
||||||
|
type: 'url',
|
||||||
|
placeholder: 'https://open.feishu.cn/open-apis/bot/v2/hook/...',
|
||||||
|
required: true,
|
||||||
|
help: '飞书机器人的Webhook地址'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'secret',
|
||||||
|
label: '签名密钥(可选)',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: '输入签名密钥',
|
||||||
|
required: false,
|
||||||
|
help: '如果机器人开启了签名验证,请填写密钥'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
bark: {
|
||||||
|
title: 'Bark通知',
|
||||||
|
description: 'iOS推送通知服务,支持自建服务器和官方服务器',
|
||||||
|
icon: 'bi-phone-fill',
|
||||||
|
color: 'dark',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
id: 'device_key',
|
||||||
|
label: '设备密钥',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: '输入Bark设备密钥',
|
||||||
|
required: true,
|
||||||
|
help: 'Bark应用中显示的设备密钥'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'server_url',
|
||||||
|
label: '服务器地址(可选)',
|
||||||
|
type: 'url',
|
||||||
|
placeholder: 'https://api.day.app',
|
||||||
|
required: false,
|
||||||
|
help: '自建Bark服务器地址,留空使用官方服务器'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'title',
|
||||||
|
label: '通知标题(可选)',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: '闲鱼自动回复通知',
|
||||||
|
required: false,
|
||||||
|
help: '推送通知的标题'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sound',
|
||||||
|
label: '提示音(可选)',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'default',
|
||||||
|
required: false,
|
||||||
|
help: '通知提示音,如:alarm, anticipate, bell等'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group',
|
||||||
|
label: '分组(可选)',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'xianyu',
|
||||||
|
required: false,
|
||||||
|
help: '通知分组名称,用于归类消息'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
email: {
|
email: {
|
||||||
title: '邮件通知',
|
title: '邮件通知',
|
||||||
description: '通过SMTP服务器发送邮件通知,支持各种邮箱服务商',
|
description: '通过SMTP服务器发送邮件通知,支持各种邮箱服务商',
|
||||||
@ -2753,6 +2979,8 @@ function renderNotificationChannels(channels) {
|
|||||||
let channelType = channel.type;
|
let channelType = channel.type;
|
||||||
if (channelType === 'ding_talk') {
|
if (channelType === 'ding_talk') {
|
||||||
channelType = 'dingtalk'; // 兼容旧的类型名
|
channelType = 'dingtalk'; // 兼容旧的类型名
|
||||||
|
} else if (channelType === 'lark') {
|
||||||
|
channelType = 'feishu'; // 兼容lark类型名
|
||||||
}
|
}
|
||||||
const typeConfig = channelTypeConfigs[channelType];
|
const typeConfig = channelTypeConfigs[channelType];
|
||||||
const typeDisplay = typeConfig ? typeConfig.title : channel.type;
|
const typeDisplay = typeConfig ? typeConfig.title : channel.type;
|
||||||
@ -2867,6 +3095,8 @@ async function editNotificationChannel(channelId) {
|
|||||||
let channelType = channel.type;
|
let channelType = channel.type;
|
||||||
if (channelType === 'ding_talk') {
|
if (channelType === 'ding_talk') {
|
||||||
channelType = 'dingtalk'; // 兼容旧的类型名
|
channelType = 'dingtalk'; // 兼容旧的类型名
|
||||||
|
} else if (channelType === 'lark') {
|
||||||
|
channelType = 'feishu'; // 兼容lark类型名
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = channelTypeConfigs[channelType];
|
const config = channelTypeConfigs[channelType];
|
||||||
@ -2891,6 +3121,10 @@ async function editNotificationChannel(channelId) {
|
|||||||
configData = { qq_number: channel.config };
|
configData = { qq_number: channel.config };
|
||||||
} else if (channel.type === 'dingtalk' || channel.type === 'ding_talk') {
|
} else if (channel.type === 'dingtalk' || channel.type === 'ding_talk') {
|
||||||
configData = { webhook_url: channel.config };
|
configData = { webhook_url: channel.config };
|
||||||
|
} else if (channel.type === 'feishu' || channel.type === 'lark') {
|
||||||
|
configData = { webhook_url: channel.config };
|
||||||
|
} else if (channel.type === 'bark') {
|
||||||
|
configData = { device_key: channel.config };
|
||||||
} else {
|
} else {
|
||||||
configData = { config: channel.config };
|
configData = { config: channel.config };
|
||||||
}
|
}
|
||||||
@ -5883,7 +6117,12 @@ function updateBatchDeleteButton() {
|
|||||||
// 格式化日期时间
|
// 格式化日期时间
|
||||||
function formatDateTime(dateString) {
|
function formatDateTime(dateString) {
|
||||||
if (!dateString) return '未知';
|
if (!dateString) return '未知';
|
||||||
const date = new Date(dateString);
|
// 如果是ISO格式,直接new Date
|
||||||
|
if (dateString.includes('T') && dateString.endsWith('Z')) {
|
||||||
|
return new Date(dateString).toLocaleString('zh-CN');
|
||||||
|
}
|
||||||
|
// 否则按原有逻辑(可选:补偿8小时)
|
||||||
|
const date = new Date(dateString.replace(' ', 'T') + 'Z');
|
||||||
return date.toLocaleString('zh-CN');
|
return date.toLocaleString('zh-CN');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6927,6 +7166,16 @@ async function checkQRCodeStatus() {
|
|||||||
clearQRCodeCheck();
|
clearQRCodeCheck();
|
||||||
showVerificationRequired(data);
|
showVerificationRequired(data);
|
||||||
break;
|
break;
|
||||||
|
case 'processing':
|
||||||
|
document.getElementById('statusText').textContent = '正在处理中...';
|
||||||
|
// 继续轮询,不清理检查
|
||||||
|
break;
|
||||||
|
case 'already_processed':
|
||||||
|
document.getElementById('statusText').textContent = '登录已完成';
|
||||||
|
document.getElementById('statusSpinner').style.display = 'none';
|
||||||
|
clearQRCodeCheck();
|
||||||
|
showToast('该扫码会话已处理完成', 'info');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -6990,12 +7239,37 @@ function showVerificationRequired(data) {
|
|||||||
// 处理扫码成功
|
// 处理扫码成功
|
||||||
function handleQRCodeSuccess(data) {
|
function handleQRCodeSuccess(data) {
|
||||||
if (data.account_info) {
|
if (data.account_info) {
|
||||||
const { account_id, is_new_account } = data.account_info;
|
const { account_id, is_new_account, real_cookie_refreshed, fallback_reason, cookie_length } = data.account_info;
|
||||||
|
|
||||||
|
// 构建成功消息
|
||||||
|
let successMessage = '';
|
||||||
if (is_new_account) {
|
if (is_new_account) {
|
||||||
showToast(`新账号添加成功!账号ID: ${account_id}`, 'success');
|
successMessage = `新账号添加成功!账号ID: ${account_id}`;
|
||||||
} else {
|
} else {
|
||||||
showToast(`账号Cookie已更新!账号ID: ${account_id}`, 'success');
|
successMessage = `账号Cookie已更新!账号ID: ${account_id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加cookie长度信息
|
||||||
|
if (cookie_length) {
|
||||||
|
successMessage += `\nCookie长度: ${cookie_length}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加真实cookie获取状态信息
|
||||||
|
if (real_cookie_refreshed === true) {
|
||||||
|
successMessage += '\n✅ 真实Cookie获取并保存成功';
|
||||||
|
document.getElementById('statusText').textContent = '登录成功!真实Cookie已获取并保存';
|
||||||
|
showToast(successMessage, 'success');
|
||||||
|
} else if (real_cookie_refreshed === false) {
|
||||||
|
successMessage += '\n⚠️ 真实Cookie获取失败,已保存原始扫码Cookie';
|
||||||
|
if (fallback_reason) {
|
||||||
|
successMessage += `\n原因: ${fallback_reason}`;
|
||||||
|
}
|
||||||
|
document.getElementById('statusText').textContent = '登录成功,但使用原始Cookie';
|
||||||
|
showToast(successMessage, 'warning');
|
||||||
|
} else {
|
||||||
|
// 兼容旧版本,没有真实cookie刷新信息
|
||||||
|
document.getElementById('statusText').textContent = '登录成功!';
|
||||||
|
showToast(successMessage, 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭模态框
|
// 关闭模态框
|
||||||
@ -7005,7 +7279,7 @@ function handleQRCodeSuccess(data) {
|
|||||||
|
|
||||||
// 刷新账号列表
|
// 刷新账号列表
|
||||||
loadCookies();
|
loadCookies();
|
||||||
}, 2000);
|
}, 3000); // 延长显示时间以便用户看到详细信息
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -8625,23 +8899,6 @@ function updateBatchDeleteOrdersButton() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化日期时间
|
|
||||||
function formatDateTime(dateString) {
|
|
||||||
if (!dateString) return '未知时间';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleString('zh-CN', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return dateString;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载完成后初始化订单搜索功能
|
// 页面加载完成后初始化订单搜索功能
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
@ -9747,3 +10004,165 @@ function exportSearchResults() {
|
|||||||
showToast('导出搜索结果失败', 'danger');
|
showToast('导出搜索结果失败', 'danger');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// 版本管理功能
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载系统版本号并检查更新
|
||||||
|
*/
|
||||||
|
async function loadSystemVersion() {
|
||||||
|
try {
|
||||||
|
// 从 version.txt 文件读取当前系统版本
|
||||||
|
let currentSystemVersion = 'v1.0.0'; // 默认版本
|
||||||
|
|
||||||
|
try {
|
||||||
|
const versionResponse = await fetch('/static/version.txt');
|
||||||
|
if (versionResponse.ok) {
|
||||||
|
currentSystemVersion = (await versionResponse.text()).trim();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('无法读取本地版本文件,使用默认版本');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示当前版本
|
||||||
|
document.getElementById('versionNumber').textContent = currentSystemVersion;
|
||||||
|
|
||||||
|
// 获取远程版本并检查更新
|
||||||
|
const response = await fetch('http://xianyu.zhinianblog.cn/index.php?action=getVersion');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.error('获取版本号失败:', result.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteVersion = result.data;
|
||||||
|
|
||||||
|
// 检查是否有更新
|
||||||
|
if (remoteVersion !== currentSystemVersion) {
|
||||||
|
showUpdateAvailable(remoteVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取版本号失败:', error);
|
||||||
|
document.getElementById('versionNumber').textContent = '未知';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示有更新标签
|
||||||
|
*/
|
||||||
|
function showUpdateAvailable(newVersion) {
|
||||||
|
const versionContainer = document.querySelector('.version-info');
|
||||||
|
|
||||||
|
if (!versionContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经有更新标签
|
||||||
|
if (versionContainer.querySelector('.update-badge')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建更新标签
|
||||||
|
const updateBadge = document.createElement('span');
|
||||||
|
updateBadge.className = 'badge bg-warning ms-2 update-badge';
|
||||||
|
updateBadge.style.cursor = 'pointer';
|
||||||
|
updateBadge.innerHTML = '<i class="bi bi-arrow-up-circle me-1"></i>有更新';
|
||||||
|
updateBadge.title = `新版本 ${newVersion} 可用,点击查看更新内容`;
|
||||||
|
|
||||||
|
// 点击事件
|
||||||
|
updateBadge.onclick = () => showUpdateInfo(newVersion);
|
||||||
|
|
||||||
|
// 添加到版本信息容器
|
||||||
|
versionContainer.appendChild(updateBadge);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取更新信息
|
||||||
|
*/
|
||||||
|
async function getUpdateInfo() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://xianyu.zhinianblog.cn/index.php?action=getUpdateInfo');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
showToast('获取更新信息失败: ' + result.message, 'danger');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取更新信息失败:', error);
|
||||||
|
showToast('获取更新信息失败', 'danger');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示更新信息(点击"有更新"标签时调用)
|
||||||
|
*/
|
||||||
|
async function showUpdateInfo(newVersion) {
|
||||||
|
const updateInfo = await getUpdateInfo();
|
||||||
|
if (!updateInfo) return;
|
||||||
|
|
||||||
|
let updateList = '';
|
||||||
|
if (updateInfo.updates && updateInfo.updates.length > 0) {
|
||||||
|
updateList = updateInfo.updates.map(item => `<li class="mb-2">${item}</li>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalHtml = `
|
||||||
|
<div class="modal fade" id="updateModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-warning text-dark">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="bi bi-arrow-up-circle me-2"></i>版本更新内容
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
<strong>发现新版本!</strong>以下是最新版本的更新内容。
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6><i class="bi bi-tag me-1"></i>最新版本</h6>
|
||||||
|
<p class="fs-4 text-success fw-bold">${updateInfo.version}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6><i class="bi bi-calendar me-1"></i>发布日期</h6>
|
||||||
|
<p class="text-muted">${updateInfo.releaseDate || '未知'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<h6><i class="bi bi-list-ul me-1"></i>更新内容</h6>
|
||||||
|
${updateList ? `<ul class="list-unstyled ps-3">${updateList}</ul>` : '<p class="text-muted">暂无更新内容</p>'}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 移除已存在的模态框
|
||||||
|
const existingModal = document.getElementById('updateModal');
|
||||||
|
if (existingModal) {
|
||||||
|
existingModal.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新的模态框
|
||||||
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||||
|
|
||||||
|
// 显示模态框
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('updateModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
1
static/version.txt
Normal file
1
static/version.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
v1.0.0
|
BIN
static/wechat.png
Normal file
BIN
static/wechat.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
Loading…
x
Reference in New Issue
Block a user