mirror of
https://github.com/zhinianboke/xianyu-auto-reply.git
synced 2025-08-03 04:57:35 +08:00
新增扫码登录
This commit is contained in:
parent
215b5dad49
commit
1243b7c17e
59
README.md
59
README.md
@ -75,45 +75,48 @@
|
|||||||
```
|
```
|
||||||
xianyu-auto-reply/
|
xianyu-auto-reply/
|
||||||
├── 📄 核心文件
|
├── 📄 核心文件
|
||||||
│ ├── Start.py # 项目启动入口
|
│ ├── Start.py # 项目启动入口,初始化所有服务
|
||||||
│ ├── XianyuAutoAsync.py # 闲鱼WebSocket连接和消息处理
|
│ ├── XianyuAutoAsync.py # 闲鱼WebSocket连接和消息处理核心
|
||||||
│ ├── reply_server.py # FastAPI Web服务器和API接口
|
│ ├── reply_server.py # FastAPI Web服务器和完整API接口
|
||||||
│ ├── db_manager.py # SQLite数据库管理
|
│ ├── db_manager.py # SQLite数据库管理,支持多用户数据隔离
|
||||||
│ ├── cookie_manager.py # Cookie和账号管理
|
│ ├── cookie_manager.py # 多账号Cookie管理和任务调度
|
||||||
│ ├── ai_reply_engine.py # AI智能回复引擎
|
│ ├── ai_reply_engine.py # AI智能回复引擎,支持多种AI模型
|
||||||
│ ├── file_log_collector.py # 日志收集和管理
|
│ ├── file_log_collector.py # 实时日志收集和管理系统
|
||||||
│ ├── config.py # 配置文件管理
|
│ ├── config.py # 全局配置文件管理器
|
||||||
│ └── secure_confirm_ultra.py # 自动确认发货模块(超级加密保护)
|
│ └── secure_confirm_ultra.py # 自动确认发货模块(多层加密保护)
|
||||||
├── 🛠️ 工具模块
|
├── 🛠️ 工具模块
|
||||||
│ └── utils/
|
│ └── utils/
|
||||||
│ ├── xianyu_utils.py # 闲鱼API工具函数
|
│ ├── xianyu_utils.py # 闲鱼API工具函数(加密、签名、解析)
|
||||||
│ ├── message_utils.py # 消息格式化工具
|
│ ├── message_utils.py # 消息格式化和处理工具
|
||||||
│ ├── ws_utils.py # WebSocket客户端工具
|
│ ├── ws_utils.py # WebSocket客户端封装
|
||||||
│ └── item_search.py # 商品搜索功能
|
│ ├── qr_login.py # 二维码登录功能
|
||||||
|
│ ├── item_search.py # 商品搜索功能(基于Playwright)
|
||||||
|
│ └── order_detail_fetcher.py # 订单详情获取工具
|
||||||
├── 🌐 前端界面
|
├── 🌐 前端界面
|
||||||
│ └── static/
|
│ └── static/
|
||||||
│ ├── index.html # 主管理界面
|
│ ├── index.html # 主管理界面(账号管理、系统监控)
|
||||||
│ ├── login.html # 用户登录页面
|
│ ├── login.html # 用户登录页面
|
||||||
│ ├── register.html # 用户注册页面
|
│ ├── register.html # 用户注册页面(邮箱验证)
|
||||||
│ ├── user_management.html # 用户管理页面
|
│ ├── user_management.html # 用户管理页面(管理员功能)
|
||||||
│ ├── data_management.html # 数据管理页面
|
│ ├── data_management.html # 数据管理页面(导入导出)
|
||||||
│ ├── log_management.html # 日志管理页面
|
│ ├── log_management.html # 日志管理页面(实时日志查看)
|
||||||
│ ├── item_search.html # 商品搜索页面
|
│ ├── item_search.html # 商品搜索页面(真实数据获取)
|
||||||
│ └── lib/ # 前端依赖库
|
│ ├── xianyu_js_version_2.js # 闲鱼JavaScript工具库
|
||||||
|
│ └── lib/ # 前端依赖库(Bootstrap等)
|
||||||
├── 🐳 Docker部署
|
├── 🐳 Docker部署
|
||||||
│ ├── Dockerfile # Docker镜像构建文件
|
│ ├── Dockerfile # Docker镜像构建文件
|
||||||
│ ├── docker-compose.yml # Docker Compose配置
|
│ ├── docker-compose.yml # Docker Compose一键部署配置
|
||||||
│ ├── docker-deploy.sh # Docker部署脚本
|
│ ├── docker-deploy.sh # Docker部署管理脚本
|
||||||
│ ├── .env # 环境变量配置文件
|
|
||||||
│ └── nginx/ # Nginx反向代理配置
|
│ └── nginx/ # Nginx反向代理配置
|
||||||
├── 📋 配置文件
|
├── 📋 配置文件
|
||||||
│ ├── global_config.yml # 全局配置文件
|
│ ├── global_config.yml # 全局配置文件(WebSocket、API等)
|
||||||
│ ├── requirements.txt # Python依赖包
|
│ ├── requirements.txt # Python依赖包列表
|
||||||
│ └── README.md # 项目说明文档
|
│ └── README.md # 项目说明文档
|
||||||
└── 📊 数据目录
|
└── 📊 数据目录
|
||||||
├── data/ # 数据库和数据文件
|
├── xianyu_data.db # SQLite数据库文件
|
||||||
├── logs/ # 日志文件
|
├── logs/ # 按日期分割的日志文件
|
||||||
└── backups/ # 备份文件
|
├── backups/ # 数据备份文件
|
||||||
|
└── realtime.log # 实时日志文件
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
|
@ -257,12 +257,13 @@ class XianyuLive:
|
|||||||
# 发送Cookie更新失败通知
|
# 发送Cookie更新失败通知
|
||||||
await self.send_token_refresh_notification(f"Cookie更新失败: {str(e)}", "cookie_update_failed")
|
await self.send_token_refresh_notification(f"Cookie更新失败: {str(e)}", "cookie_update_failed")
|
||||||
|
|
||||||
async def save_item_info_to_db(self, item_id: str, item_detail: str = None):
|
async def save_item_info_to_db(self, item_id: str, item_detail: str = None, item_title: str = None):
|
||||||
"""保存商品信息到数据库
|
"""保存商品信息到数据库
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
item_id: 商品ID
|
item_id: 商品ID
|
||||||
item_detail: 商品详情内容(可以是任意格式的文本)
|
item_detail: 商品详情内容(可以是任意格式的文本)
|
||||||
|
item_title: 商品标题
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 跳过以 auto_ 开头的商品ID
|
# 跳过以 auto_ 开头的商品ID
|
||||||
@ -270,6 +271,16 @@ class XianyuLive:
|
|||||||
logger.debug(f"跳过保存自动生成的商品ID: {item_id}")
|
logger.debug(f"跳过保存自动生成的商品ID: {item_id}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 验证:如果只有商品ID,没有商品标题和商品详情,则不插入数据库
|
||||||
|
if not item_title and not item_detail:
|
||||||
|
logger.debug(f"跳过保存商品信息:缺少商品标题和详情 - {item_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 如果有商品标题但没有详情,也跳过(根据需求,需要同时有标题和详情)
|
||||||
|
if not item_title or not item_detail:
|
||||||
|
logger.debug(f"跳过保存商品信息:商品标题或详情不完整 - {item_id}")
|
||||||
|
return
|
||||||
|
|
||||||
from db_manager import db_manager
|
from db_manager import db_manager
|
||||||
|
|
||||||
# 直接使用传入的详情内容
|
# 直接使用传入的详情内容
|
||||||
@ -1288,8 +1299,22 @@ class XianyuLive:
|
|||||||
# 使用第一个匹配的规则(按关键字长度降序排列,优先匹配更精确的规则)
|
# 使用第一个匹配的规则(按关键字长度降序排列,优先匹配更精确的规则)
|
||||||
rule = delivery_rules[0]
|
rule = delivery_rules[0]
|
||||||
|
|
||||||
# 保存商品信息到数据库
|
# 保存商品信息到数据库(需要有商品标题才保存)
|
||||||
await self.save_item_info_to_db(item_id, search_text)
|
# 尝试获取商品标题
|
||||||
|
item_title_for_save = None
|
||||||
|
try:
|
||||||
|
from db_manager import db_manager
|
||||||
|
db_item_info = db_manager.get_item_info(self.cookie_id, item_id)
|
||||||
|
if db_item_info:
|
||||||
|
item_title_for_save = db_item_info.get('item_title', '').strip()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 如果有商品标题,则保存商品信息
|
||||||
|
if item_title_for_save:
|
||||||
|
await self.save_item_info_to_db(item_id, search_text, item_title_for_save)
|
||||||
|
else:
|
||||||
|
logger.debug(f"跳过保存商品信息:缺少商品标题 - {item_id}")
|
||||||
|
|
||||||
# 详细的匹配结果日志
|
# 详细的匹配结果日志
|
||||||
if rule.get('is_multi_spec'):
|
if rule.get('is_multi_spec'):
|
||||||
@ -2457,9 +2482,8 @@ class XianyuLive:
|
|||||||
reply = await self.get_default_reply(send_user_name, send_user_id, send_message)
|
reply = await self.get_default_reply(send_user_name, send_user_id, send_message)
|
||||||
reply_source = '默认' # 标记为默认回复
|
reply_source = '默认' # 标记为默认回复
|
||||||
|
|
||||||
# 保存商品信息到数据库(记录通知时传递的item_id)
|
# 注意:这里只有商品ID,没有标题和详情,根据新的规则不保存到数据库
|
||||||
if item_id:
|
# 商品信息会在其他有完整信息的地方保存(如发货规则匹配时)
|
||||||
await self.save_item_info_to_db(item_id, None)
|
|
||||||
# 发送通知
|
# 发送通知
|
||||||
await self.send_notification(send_user_name, send_user_id, send_message, item_id)
|
await self.send_notification(send_user_name, send_user_id, send_message, item_id)
|
||||||
|
|
||||||
|
101
db_manager.py
101
db_manager.py
@ -125,7 +125,6 @@ class DBManager:
|
|||||||
keyword TEXT,
|
keyword TEXT,
|
||||||
reply TEXT,
|
reply TEXT,
|
||||||
item_id TEXT,
|
item_id TEXT,
|
||||||
PRIMARY KEY (cookie_id, keyword),
|
|
||||||
FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE
|
FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
@ -434,6 +433,10 @@ class DBManager:
|
|||||||
self._execute_sql(cursor, "ALTER TABLE item_info ADD COLUMN is_multi_spec BOOLEAN DEFAULT FALSE")
|
self._execute_sql(cursor, "ALTER TABLE item_info ADD COLUMN is_multi_spec BOOLEAN DEFAULT FALSE")
|
||||||
logger.info("为item_info表添加多规格字段")
|
logger.info("为item_info表添加多规格字段")
|
||||||
|
|
||||||
|
# 处理keywords表的唯一约束问题
|
||||||
|
# 由于SQLite不支持直接修改约束,我们需要重建表
|
||||||
|
self._migrate_keywords_table_constraints(cursor)
|
||||||
|
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
logger.info(f"数据库初始化成功: {self.db_path}")
|
logger.info(f"数据库初始化成功: {self.db_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -442,6 +445,66 @@ class DBManager:
|
|||||||
self.conn.close()
|
self.conn.close()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def _migrate_keywords_table_constraints(self, cursor):
|
||||||
|
"""迁移keywords表的约束,支持基于商品ID的唯一性校验"""
|
||||||
|
try:
|
||||||
|
# 检查是否已经迁移过(通过检查是否存在新的唯一索引)
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_keywords_unique_with_item'")
|
||||||
|
if cursor.fetchone():
|
||||||
|
logger.info("keywords表约束已经迁移过,跳过")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("开始迁移keywords表约束...")
|
||||||
|
|
||||||
|
# 1. 创建临时表,不设置主键约束
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS keywords_temp (
|
||||||
|
cookie_id TEXT,
|
||||||
|
keyword TEXT,
|
||||||
|
reply TEXT,
|
||||||
|
item_id TEXT,
|
||||||
|
FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 2. 复制现有数据到临时表
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO keywords_temp (cookie_id, keyword, reply, item_id)
|
||||||
|
SELECT cookie_id, keyword, reply, item_id FROM keywords
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 3. 删除原表
|
||||||
|
cursor.execute('DROP TABLE keywords')
|
||||||
|
|
||||||
|
# 4. 重命名临时表
|
||||||
|
cursor.execute('ALTER TABLE keywords_temp RENAME TO keywords')
|
||||||
|
|
||||||
|
# 5. 创建复合唯一索引来实现我们需要的约束逻辑
|
||||||
|
# 对于item_id为空的情况:(cookie_id, keyword)必须唯一
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE UNIQUE INDEX idx_keywords_unique_no_item
|
||||||
|
ON keywords(cookie_id, keyword)
|
||||||
|
WHERE item_id IS NULL OR item_id = ''
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 对于item_id不为空的情况:(cookie_id, keyword, item_id)必须唯一
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE UNIQUE INDEX idx_keywords_unique_with_item
|
||||||
|
ON keywords(cookie_id, keyword, item_id)
|
||||||
|
WHERE item_id IS NOT NULL AND item_id != ''
|
||||||
|
''')
|
||||||
|
|
||||||
|
logger.info("keywords表约束迁移完成")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"迁移keywords表约束失败: {e}")
|
||||||
|
# 如果迁移失败,尝试回滚
|
||||||
|
try:
|
||||||
|
cursor.execute('DROP TABLE IF EXISTS keywords_temp')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""关闭数据库连接"""
|
"""关闭数据库连接"""
|
||||||
if self.conn:
|
if self.conn:
|
||||||
@ -672,11 +735,20 @@ class DBManager:
|
|||||||
# 先删除该cookie_id的所有关键字
|
# 先删除该cookie_id的所有关键字
|
||||||
self._execute_sql(cursor, "DELETE FROM keywords WHERE cookie_id = ?", (cookie_id,))
|
self._execute_sql(cursor, "DELETE FROM keywords WHERE cookie_id = ?", (cookie_id,))
|
||||||
|
|
||||||
# 插入新关键字
|
# 插入新关键字,使用INSERT OR REPLACE来处理可能的唯一约束冲突
|
||||||
for keyword, reply, item_id in keywords:
|
for keyword, reply, item_id in keywords:
|
||||||
|
# 标准化item_id:空字符串转为NULL
|
||||||
|
normalized_item_id = item_id if item_id and item_id.strip() else None
|
||||||
|
|
||||||
|
try:
|
||||||
self._execute_sql(cursor,
|
self._execute_sql(cursor,
|
||||||
"INSERT INTO keywords (cookie_id, keyword, reply, item_id) VALUES (?, ?, ?, ?)",
|
"INSERT INTO keywords (cookie_id, keyword, reply, item_id) VALUES (?, ?, ?, ?)",
|
||||||
(cookie_id, keyword, reply, item_id))
|
(cookie_id, keyword, reply, normalized_item_id))
|
||||||
|
except sqlite3.IntegrityError as ie:
|
||||||
|
# 如果遇到唯一约束冲突,记录详细错误信息
|
||||||
|
item_desc = f"商品ID: {normalized_item_id}" if normalized_item_id else "通用关键词"
|
||||||
|
logger.error(f"关键词唯一约束冲突: Cookie={cookie_id}, 关键词='{keyword}', {item_desc}")
|
||||||
|
raise ie
|
||||||
|
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
logger.info(f"关键字保存成功: {cookie_id}, {len(keywords)}条")
|
logger.info(f"关键字保存成功: {cookie_id}, {len(keywords)}条")
|
||||||
@ -2563,6 +2635,23 @@ class DBManager:
|
|||||||
bool: 操作是否成功
|
bool: 操作是否成功
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# 验证:如果只有商品ID,没有商品详情数据,则不插入数据库
|
||||||
|
if not item_data:
|
||||||
|
logger.debug(f"跳过保存商品信息:缺少商品详情数据 - {item_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 如果是字典类型,检查是否有标题信息
|
||||||
|
if isinstance(item_data, dict):
|
||||||
|
title = item_data.get('title', '').strip()
|
||||||
|
if not title:
|
||||||
|
logger.debug(f"跳过保存商品信息:缺少商品标题 - {item_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 如果是字符串类型,检查是否为空
|
||||||
|
if isinstance(item_data, str) and not item_data.strip():
|
||||||
|
logger.debug(f"跳过保存商品信息:商品详情为空 - {item_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
cursor = self.conn.cursor()
|
cursor = self.conn.cursor()
|
||||||
|
|
||||||
@ -2633,6 +2722,7 @@ class DBManager:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"保存商品信息失败: {e}")
|
logger.error(f"保存商品信息失败: {e}")
|
||||||
|
self.conn.rollback()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_item_info(self, cookie_id: str, item_id: str) -> Optional[Dict]:
|
def get_item_info(self, cookie_id: str, item_id: str) -> Optional[Dict]:
|
||||||
@ -2897,6 +2987,11 @@ class DBManager:
|
|||||||
if not cookie_id or not item_id:
|
if not cookie_id or not item_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# 验证:如果没有商品标题,则跳过保存
|
||||||
|
if not item_title or not item_title.strip():
|
||||||
|
logger.debug(f"跳过批量保存商品信息:缺少商品标题 - {item_id}")
|
||||||
|
continue
|
||||||
|
|
||||||
# 使用 INSERT OR IGNORE + UPDATE 模式
|
# 使用 INSERT OR IGNORE + UPDATE 模式
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT OR IGNORE INTO item_info (cookie_id, item_id, item_title, item_description,
|
INSERT OR IGNORE INTO item_info (cookie_id, item_id, item_title, item_description,
|
||||||
|
132
reply_server.py
132
reply_server.py
@ -19,6 +19,8 @@ import cookie_manager
|
|||||||
from db_manager import db_manager
|
from db_manager import db_manager
|
||||||
from file_log_collector import setup_file_logging, get_file_log_collector
|
from file_log_collector import setup_file_logging, get_file_log_collector
|
||||||
from ai_reply_engine import ai_reply_engine
|
from ai_reply_engine import ai_reply_engine
|
||||||
|
from utils.qr_login import qr_login_manager
|
||||||
|
from utils.xianyu_utils import trans_cookies
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
# 关键字文件路径
|
# 关键字文件路径
|
||||||
@ -966,6 +968,121 @@ def update_cookie(cid: str, item: CookieIn, current_user: Dict[str, Any] = Depen
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# ========================= 扫码登录相关接口 =========================
|
||||||
|
|
||||||
|
@app.post("/qr-login/generate")
|
||||||
|
async def generate_qr_code(current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||||
|
"""生成扫码登录二维码"""
|
||||||
|
try:
|
||||||
|
log_with_user('info', "请求生成扫码登录二维码", current_user)
|
||||||
|
|
||||||
|
result = await qr_login_manager.generate_qr_code()
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
log_with_user('info', f"扫码登录二维码生成成功: {result['session_id']}", current_user)
|
||||||
|
else:
|
||||||
|
log_with_user('warning', f"扫码登录二维码生成失败: {result.get('message', '未知错误')}", current_user)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log_with_user('error', f"生成扫码登录二维码异常: {str(e)}", current_user)
|
||||||
|
return {'success': False, 'message': f'生成二维码失败: {str(e)}'}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/qr-login/check/{session_id}")
|
||||||
|
async def check_qr_code_status(session_id: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||||
|
"""检查扫码登录状态"""
|
||||||
|
try:
|
||||||
|
# 清理过期会话
|
||||||
|
qr_login_manager.cleanup_expired_sessions()
|
||||||
|
|
||||||
|
# 获取会话状态
|
||||||
|
status_info = qr_login_manager.get_session_status(session_id)
|
||||||
|
|
||||||
|
if status_info['status'] == 'success':
|
||||||
|
# 登录成功,处理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)
|
||||||
|
|
||||||
|
return status_info
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log_with_user('error', f"检查扫码登录状态异常: {str(e)}", current_user)
|
||||||
|
return {'status': 'error', 'message': str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""处理扫码登录获取的Cookie"""
|
||||||
|
try:
|
||||||
|
user_id = current_user['user_id']
|
||||||
|
|
||||||
|
# 检查是否已存在相同unb的账号
|
||||||
|
existing_cookies = db_manager.get_all_cookies(user_id)
|
||||||
|
existing_account_id = None
|
||||||
|
|
||||||
|
for account_id, cookie_value in existing_cookies.items():
|
||||||
|
try:
|
||||||
|
# 解析现有Cookie中的unb
|
||||||
|
existing_cookie_dict = trans_cookies(cookie_value)
|
||||||
|
if existing_cookie_dict.get('unb') == unb:
|
||||||
|
existing_account_id = account_id
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if existing_account_id:
|
||||||
|
# 更新现有账号的Cookie
|
||||||
|
db_manager.save_cookie(existing_account_id, cookies, user_id)
|
||||||
|
|
||||||
|
# 更新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:
|
||||||
|
# 创建新账号,使用unb作为账号ID
|
||||||
|
account_id = unb
|
||||||
|
|
||||||
|
# 确保账号ID唯一
|
||||||
|
counter = 1
|
||||||
|
original_account_id = account_id
|
||||||
|
while account_id in existing_cookies:
|
||||||
|
account_id = f"{original_account_id}_{counter}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
# 保存新账号
|
||||||
|
db_manager.save_cookie(account_id, cookies, user_id)
|
||||||
|
|
||||||
|
# 添加到cookie_manager
|
||||||
|
if cookie_manager.manager:
|
||||||
|
cookie_manager.manager.add_cookie(account_id, cookies)
|
||||||
|
|
||||||
|
log_with_user('info', f"扫码登录创建新账号: {account_id}, UNB: {unb}", current_user)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'account_id': account_id,
|
||||||
|
'is_new_account': True
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log_with_user('error', f"处理扫码登录Cookie失败: {str(e)}", current_user)
|
||||||
|
raise 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)):
|
||||||
"""更新账号的启用/禁用状态"""
|
"""更新账号的启用/禁用状态"""
|
||||||
@ -1403,7 +1520,20 @@ def get_keywords(cid: str, current_user: Dict[str, Any] = Depends(get_current_us
|
|||||||
if cid not in user_cookies:
|
if cid not in user_cookies:
|
||||||
raise HTTPException(status_code=403, detail="无权限访问该Cookie")
|
raise HTTPException(status_code=403, detail="无权限访问该Cookie")
|
||||||
|
|
||||||
return cookie_manager.manager.get_keywords(cid)
|
# 直接从数据库获取所有关键词(避免重复计算)
|
||||||
|
item_keywords = db_manager.get_keywords_with_item_id(cid)
|
||||||
|
|
||||||
|
# 转换为统一格式
|
||||||
|
all_keywords = []
|
||||||
|
for keyword, reply, item_id in item_keywords:
|
||||||
|
all_keywords.append({
|
||||||
|
"keyword": keyword,
|
||||||
|
"reply": reply,
|
||||||
|
"item_id": item_id,
|
||||||
|
"type": "item" if item_id else "normal"
|
||||||
|
})
|
||||||
|
|
||||||
|
return all_keywords
|
||||||
|
|
||||||
|
|
||||||
@app.get("/keywords-with-item-id/{cid}")
|
@app.get("/keywords-with-item-id/{cid}")
|
||||||
|
@ -14,6 +14,7 @@ loguru>=0.7.0
|
|||||||
websockets>=10.0,<13.0
|
websockets>=10.0,<13.0
|
||||||
aiohttp>=3.9.0
|
aiohttp>=3.9.0
|
||||||
requests>=2.31.0
|
requests>=2.31.0
|
||||||
|
httpx>=0.25.0
|
||||||
|
|
||||||
# 配置文件处理
|
# 配置文件处理
|
||||||
PyYAML>=6.0.0
|
PyYAML>=6.0.0
|
||||||
@ -34,8 +35,9 @@ python-multipart>=0.0.6
|
|||||||
# AI回复引擎
|
# AI回复引擎
|
||||||
openai>=1.65.5
|
openai>=1.65.5
|
||||||
|
|
||||||
# 图像处理(验证码生成)
|
# 图像处理(验证码生成、二维码生成)
|
||||||
Pillow>=10.0.0
|
Pillow>=10.0.0
|
||||||
|
qrcode[pil]>=7.4.2
|
||||||
|
|
||||||
# 浏览器自动化(商品搜索、订单详情获取)
|
# 浏览器自动化(商品搜索、订单详情获取)
|
||||||
playwright>=1.40.0
|
playwright>=1.40.0
|
||||||
@ -55,7 +57,13 @@ regex>=2023.10.3
|
|||||||
pandas>=2.0.0
|
pandas>=2.0.0
|
||||||
openpyxl>=3.1.0
|
openpyxl>=3.1.0
|
||||||
|
|
||||||
|
# 邮件发送(用户注册验证)
|
||||||
|
email-validator>=2.0.0
|
||||||
|
|
||||||
# 其他工具库
|
# 其他工具库
|
||||||
typing-extensions>=4.7.0
|
typing-extensions>=4.7.0
|
||||||
|
|
||||||
# 注意:sqlite3 是Python内置模块,无需安装
|
# 注意:
|
||||||
|
# - sqlite3 是Python内置模块,无需安装
|
||||||
|
# - smtplib 是Python内置模块,无需安装
|
||||||
|
# - email 是Python内置模块,无需安装
|
@ -1100,6 +1100,112 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 扫码登录按钮特殊样式 */
|
||||||
|
.qr-login-btn {
|
||||||
|
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-login-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4);
|
||||||
|
background: linear-gradient(135deg, #218838 0%, #1ea085 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-login-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-login-btn::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||||
|
transition: left 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-login-btn:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 二维码容器样式 */
|
||||||
|
.qr-code-wrapper {
|
||||||
|
border: 3px solid #28a745;
|
||||||
|
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-wrapper:hover {
|
||||||
|
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 步骤指引样式 */
|
||||||
|
.step-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手动输入按钮样式 */
|
||||||
|
.manual-input-btn {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-input-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 等待提示样式 */
|
||||||
|
.bg-light-warning {
|
||||||
|
background-color: #fff3cd !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-loading-tip {
|
||||||
|
animation: pulse-warning 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-warning {
|
||||||
|
0% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.qr-login-btn, .manual-input-btn {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-item {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
width: 25px !important;
|
||||||
|
height: 25px !important;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -1300,10 +1406,36 @@
|
|||||||
<div class="content-body">
|
<div class="content-body">
|
||||||
<!-- 添加Cookie卡片 -->
|
<!-- 添加Cookie卡片 -->
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header">
|
||||||
<span><i class="bi bi-plus-circle me-2"></i>添加新账号</span>
|
<span><i class="bi bi-plus-circle me-2"></i>添加新账号</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<!-- 添加方式选择 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<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;">
|
||||||
|
<i class="bi bi-qr-code me-2"></i>
|
||||||
|
<span class="fw-bold">扫码登录</span>
|
||||||
|
<br>
|
||||||
|
<small class="opacity-75">推荐方式,安全便捷</small>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-lg flex-fill manual-input-btn" onclick="toggleManualInput()" style="max-width: 300px;">
|
||||||
|
<i class="bi bi-keyboard me-2"></i>
|
||||||
|
<span class="fw-bold">手动输入</span>
|
||||||
|
<br>
|
||||||
|
<small class="opacity-75">输入Cookie信息</small>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 手动输入表单(默认隐藏) -->
|
||||||
|
<div id="manualInputForm" style="display: none;">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
<strong>提示:</strong>推荐使用扫码登录,更加安全便捷。如需手动输入,请确保Cookie信息的准确性。
|
||||||
|
</div>
|
||||||
<form id="addForm" class="row g-3">
|
<form id="addForm" class="row g-3">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label for="cookieId" class="form-label">账号ID</label>
|
<label for="cookieId" class="form-label">账号ID</label>
|
||||||
@ -1317,10 +1449,14 @@
|
|||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<i class="bi bi-plus-lg me-1"></i>添加账号
|
<i class="bi bi-plus-lg me-1"></i>添加账号
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary ms-2" onclick="toggleManualInput()">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>取消
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Cookie列表卡片 -->
|
<!-- Cookie列表卡片 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@ -2064,6 +2200,85 @@
|
|||||||
|
|
||||||
</div> <!-- 结束 main-content -->
|
</div> <!-- 结束 main-content -->
|
||||||
|
|
||||||
|
<!-- 扫码登录模态框 -->
|
||||||
|
<div class="modal fade" id="qrCodeLoginModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-success text-white">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="bi bi-qr-code me-2"></i>扫码登录闲鱼账号
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center py-4">
|
||||||
|
<!-- 步骤指引 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-center align-items-center">
|
||||||
|
<div class="step-item me-3">
|
||||||
|
<div class="step-number bg-success text-white rounded-circle d-inline-flex align-items-center justify-content-center" style="width: 30px; height: 30px;">1</div>
|
||||||
|
<small class="d-block mt-1">打开闲鱼APP</small>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-arrow-right text-muted me-3"></i>
|
||||||
|
<div class="step-item me-3">
|
||||||
|
<div class="step-number bg-success text-white rounded-circle d-inline-flex align-items-center justify-content-center" style="width: 30px; height: 30px;">2</div>
|
||||||
|
<small class="d-block mt-1">扫描二维码</small>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-arrow-right text-muted me-3"></i>
|
||||||
|
<div class="step-item">
|
||||||
|
<div class="step-number bg-success text-white rounded-circle d-inline-flex align-items-center justify-content-center" style="width: 30px; height: 30px;">3</div>
|
||||||
|
<small class="d-block mt-1">自动添加账号</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 二维码区域 -->
|
||||||
|
<div id="qrCodeContainer">
|
||||||
|
<div class="spinner-border text-success mb-3" role="status" style="width: 3rem; height: 3rem;">
|
||||||
|
<span class="visually-hidden">生成二维码中...</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted fs-5 mb-2">正在生成二维码...</p>
|
||||||
|
<div class="alert alert-warning border-0 bg-light-warning d-inline-block qr-loading-tip">
|
||||||
|
<i class="bi bi-clock me-2 text-warning"></i>
|
||||||
|
<small class="text-warning fw-bold">二维码生成较慢,请耐心等待</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="qrCodeImage" style="display: none;">
|
||||||
|
<div class="qr-code-wrapper p-3 bg-light rounded-3 d-inline-block mb-3">
|
||||||
|
<img id="qrCodeImg" src="" alt="登录二维码" class="img-fluid" style="max-width: 280px;">
|
||||||
|
</div>
|
||||||
|
<h6 class="text-success mb-2">
|
||||||
|
<i class="bi bi-phone me-2"></i>请使用闲鱼APP扫描二维码
|
||||||
|
</h6>
|
||||||
|
<div class="alert alert-info border-0 bg-light">
|
||||||
|
<i class="bi bi-info-circle me-2 text-info"></i>
|
||||||
|
<small>扫码后请等待页面提示,系统会自动获取并保存您的账号信息</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态显示 -->
|
||||||
|
<div id="qrCodeStatus" class="mt-3">
|
||||||
|
<div class="d-flex align-items-center justify-content-center">
|
||||||
|
<div class="spinner-border spinner-border-sm text-success me-2" role="status" style="display: none;" id="statusSpinner">
|
||||||
|
<span class="visually-hidden">检查中...</span>
|
||||||
|
</div>
|
||||||
|
<span id="statusText" class="text-muted">等待扫码...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer bg-light">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>关闭
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-success" onclick="refreshQRCode()" id="refreshQRBtn">
|
||||||
|
<i class="bi bi-arrow-clockwise me-1"></i>重新生成二维码
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 编辑通知渠道模态框 -->
|
<!-- 编辑通知渠道模态框 -->
|
||||||
<div class="modal fade" id="editChannelModal" tabindex="-1">
|
<div class="modal fade" id="editChannelModal" tabindex="-1">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
@ -2718,19 +2933,22 @@
|
|||||||
const keywordsData = await keywordsResponse.json();
|
const keywordsData = await keywordsResponse.json();
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
keywords: keywordsData
|
keywords: keywordsData,
|
||||||
|
keywordCount: keywordsData.length
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
keywords: []
|
keywords: [],
|
||||||
|
keywordCount: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`获取账号 ${account.id} 关键词失败:`, error);
|
console.error(`获取账号 ${account.id} 关键词失败:`, error);
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
keywords: []
|
keywords: [],
|
||||||
|
keywordCount: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -2744,7 +2962,7 @@
|
|||||||
let enabledAccounts = 0;
|
let enabledAccounts = 0;
|
||||||
|
|
||||||
accountsWithKeywords.forEach(account => {
|
accountsWithKeywords.forEach(account => {
|
||||||
const keywordCount = account.keywords ? account.keywords.length : 0;
|
const keywordCount = account.keywordCount || 0;
|
||||||
const isEnabled = account.enabled === undefined ? true : account.enabled;
|
const isEnabled = account.enabled === undefined ? true : account.enabled;
|
||||||
|
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
@ -2795,7 +3013,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
accounts.forEach(account => {
|
accounts.forEach(account => {
|
||||||
const keywordCount = account.keywords ? account.keywords.length : 0;
|
const keywordCount = account.keywordCount || 0;
|
||||||
const isEnabled = account.enabled === undefined ? true : account.enabled;
|
const isEnabled = account.enabled === undefined ? true : account.enabled;
|
||||||
|
|
||||||
let status = '';
|
let status = '';
|
||||||
@ -2826,7 +3044,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取账号关键词数量(带缓存)
|
// 获取账号关键词数量(带缓存)- 包含普通关键词和商品关键词
|
||||||
async function getAccountKeywordCount(accountId) {
|
async function getAccountKeywordCount(accountId) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
@ -2844,6 +3062,7 @@
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const keywordsData = await response.json();
|
const keywordsData = await response.json();
|
||||||
|
// 现在API返回的是包含普通关键词和商品关键词的完整列表
|
||||||
const count = keywordsData.length;
|
const count = keywordsData.length;
|
||||||
|
|
||||||
// 更新缓存
|
// 更新缓存
|
||||||
@ -2897,6 +3116,7 @@
|
|||||||
const keywordsData = await keywordsResponse.json();
|
const keywordsData = await keywordsResponse.json();
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
|
keywords: keywordsData,
|
||||||
keywordCount: keywordsData.length
|
keywordCount: keywordsData.length
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@ -7586,6 +7806,201 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================= 账号添加相关函数 =========================
|
||||||
|
|
||||||
|
// 切换手动输入表单显示/隐藏
|
||||||
|
function toggleManualInput() {
|
||||||
|
const manualForm = document.getElementById('manualInputForm');
|
||||||
|
if (manualForm.style.display === 'none') {
|
||||||
|
manualForm.style.display = 'block';
|
||||||
|
// 清空表单
|
||||||
|
document.getElementById('addForm').reset();
|
||||||
|
} else {
|
||||||
|
manualForm.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================= 扫码登录相关函数 =========================
|
||||||
|
|
||||||
|
let qrCodeCheckInterval = null;
|
||||||
|
let qrCodeSessionId = null;
|
||||||
|
|
||||||
|
// 显示扫码登录模态框
|
||||||
|
function showQRCodeLogin() {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('qrCodeLoginModal'));
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// 模态框显示后生成二维码
|
||||||
|
modal._element.addEventListener('shown.bs.modal', function () {
|
||||||
|
generateQRCode();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模态框关闭时清理定时器
|
||||||
|
modal._element.addEventListener('hidden.bs.modal', function () {
|
||||||
|
clearQRCodeCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新二维码(兼容旧函数名)
|
||||||
|
async function refreshQRCode() {
|
||||||
|
await generateQRCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成二维码
|
||||||
|
async function generateQRCode() {
|
||||||
|
try {
|
||||||
|
showQRCodeLoading();
|
||||||
|
|
||||||
|
const response = await fetch(`${apiBase}/qr-login/generate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${authToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
qrCodeSessionId = data.session_id;
|
||||||
|
showQRCodeImage(data.qr_code_url);
|
||||||
|
startQRCodeCheck();
|
||||||
|
} else {
|
||||||
|
showQRCodeError(data.message || '生成二维码失败');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showQRCodeError('生成二维码失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成二维码失败:', error);
|
||||||
|
showQRCodeError('网络错误,请重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示二维码加载状态
|
||||||
|
function showQRCodeLoading() {
|
||||||
|
document.getElementById('qrCodeContainer').style.display = 'block';
|
||||||
|
document.getElementById('qrCodeImage').style.display = 'none';
|
||||||
|
document.getElementById('statusText').textContent = '正在生成二维码,请耐心等待...';
|
||||||
|
document.getElementById('statusSpinner').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示二维码图片
|
||||||
|
function showQRCodeImage(qrCodeUrl) {
|
||||||
|
document.getElementById('qrCodeContainer').style.display = 'none';
|
||||||
|
document.getElementById('qrCodeImage').style.display = 'block';
|
||||||
|
document.getElementById('qrCodeImg').src = qrCodeUrl;
|
||||||
|
document.getElementById('statusText').textContent = '等待扫码...';
|
||||||
|
document.getElementById('statusSpinner').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示二维码错误
|
||||||
|
function showQRCodeError(message) {
|
||||||
|
document.getElementById('qrCodeContainer').innerHTML = `
|
||||||
|
<div class="text-danger">
|
||||||
|
<i class="bi bi-exclamation-triangle fs-1 mb-3"></i>
|
||||||
|
<p>${message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById('qrCodeImage').style.display = 'none';
|
||||||
|
document.getElementById('statusText').textContent = '生成失败';
|
||||||
|
document.getElementById('statusSpinner').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始检查二维码状态
|
||||||
|
function startQRCodeCheck() {
|
||||||
|
if (qrCodeCheckInterval) {
|
||||||
|
clearInterval(qrCodeCheckInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('statusSpinner').style.display = 'inline-block';
|
||||||
|
document.getElementById('statusText').textContent = '等待扫码...';
|
||||||
|
|
||||||
|
qrCodeCheckInterval = setInterval(checkQRCodeStatus, 2000); // 每2秒检查一次
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查二维码状态
|
||||||
|
async function checkQRCodeStatus() {
|
||||||
|
if (!qrCodeSessionId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiBase}/qr-login/check/${qrCodeSessionId}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${authToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
switch (data.status) {
|
||||||
|
case 'waiting':
|
||||||
|
document.getElementById('statusText').textContent = '等待扫码...';
|
||||||
|
break;
|
||||||
|
case 'scanned':
|
||||||
|
document.getElementById('statusText').textContent = '已扫码,请在手机上确认...';
|
||||||
|
break;
|
||||||
|
case 'success':
|
||||||
|
document.getElementById('statusText').textContent = '登录成功!';
|
||||||
|
document.getElementById('statusSpinner').style.display = 'none';
|
||||||
|
clearQRCodeCheck();
|
||||||
|
handleQRCodeSuccess(data);
|
||||||
|
break;
|
||||||
|
case 'expired':
|
||||||
|
document.getElementById('statusText').textContent = '二维码已过期';
|
||||||
|
document.getElementById('statusSpinner').style.display = 'none';
|
||||||
|
clearQRCodeCheck();
|
||||||
|
showQRCodeError('二维码已过期,请刷新重试');
|
||||||
|
break;
|
||||||
|
case 'cancelled':
|
||||||
|
document.getElementById('statusText').textContent = '用户取消登录';
|
||||||
|
document.getElementById('statusSpinner').style.display = 'none';
|
||||||
|
clearQRCodeCheck();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查二维码状态失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理扫码成功
|
||||||
|
function handleQRCodeSuccess(data) {
|
||||||
|
if (data.account_info) {
|
||||||
|
const { account_id, is_new_account } = data.account_info;
|
||||||
|
|
||||||
|
if (is_new_account) {
|
||||||
|
showToast(`新账号添加成功!账号ID: ${account_id}`, 'success');
|
||||||
|
} else {
|
||||||
|
showToast(`账号Cookie已更新!账号ID: ${account_id}`, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭模态框
|
||||||
|
setTimeout(() => {
|
||||||
|
const modal = bootstrap.Modal.getInstance(document.getElementById('qrCodeLoginModal'));
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
// 刷新账号列表
|
||||||
|
loadCookies();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理二维码检查
|
||||||
|
function clearQRCodeCheck() {
|
||||||
|
if (qrCodeCheckInterval) {
|
||||||
|
clearInterval(qrCodeCheckInterval);
|
||||||
|
qrCodeCheckInterval = null;
|
||||||
|
}
|
||||||
|
qrCodeSessionId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新二维码
|
||||||
|
function refreshQRCode() {
|
||||||
|
clearQRCodeCheck();
|
||||||
|
generateQRCode();
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- AI回复配置模态框 -->
|
<!-- AI回复配置模态框 -->
|
||||||
|
387
utils/qr_login.py
Normal file
387
utils/qr_login.py
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
闲鱼扫码登录工具
|
||||||
|
基于API接口实现二维码生成和Cookie获取(参照myfish-main项目)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from random import random
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
import httpx
|
||||||
|
import qrcode
|
||||||
|
import qrcode.constants
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
def generate_headers():
|
||||||
|
"""生成请求头"""
|
||||||
|
return {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'application/json, text/plain, */*',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||||
|
'Accept-Encoding': 'gzip, deflate, br',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
'Sec-Fetch-Dest': 'empty',
|
||||||
|
'Sec-Fetch-Mode': 'cors',
|
||||||
|
'Sec-Fetch-Site': 'same-origin',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class GetLoginParamsError(Exception):
|
||||||
|
"""获取登录参数错误"""
|
||||||
|
|
||||||
|
|
||||||
|
class GetLoginQRCodeError(Exception):
|
||||||
|
"""获取登录二维码失败"""
|
||||||
|
|
||||||
|
|
||||||
|
class NotLoginError(Exception):
|
||||||
|
"""未登录错误"""
|
||||||
|
|
||||||
|
|
||||||
|
class QRLoginSession:
|
||||||
|
"""二维码登录会话"""
|
||||||
|
|
||||||
|
def __init__(self, session_id: str):
|
||||||
|
self.session_id = session_id
|
||||||
|
self.status = 'waiting' # waiting, scanned, success, expired, cancelled
|
||||||
|
self.qr_code_url = None
|
||||||
|
self.qr_content = None
|
||||||
|
self.cookies = {}
|
||||||
|
self.unb = None
|
||||||
|
self.created_time = time.time()
|
||||||
|
self.expire_time = 300 # 5分钟过期
|
||||||
|
self.params = {} # 存储登录参数
|
||||||
|
|
||||||
|
def is_expired(self) -> bool:
|
||||||
|
"""检查是否过期"""
|
||||||
|
return time.time() - self.created_time > self.expire_time
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""转换为字典"""
|
||||||
|
return {
|
||||||
|
'session_id': self.session_id,
|
||||||
|
'status': self.status,
|
||||||
|
'qr_code_url': self.qr_code_url,
|
||||||
|
'created_time': self.created_time,
|
||||||
|
'is_expired': self.is_expired()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class QRLoginManager:
|
||||||
|
"""二维码登录管理器"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.sessions: Dict[str, QRLoginSession] = {}
|
||||||
|
self.headers = generate_headers()
|
||||||
|
self.host = "https://passport.goofish.com"
|
||||||
|
self.api_mini_login = f"{self.host}/mini_login.htm"
|
||||||
|
self.api_generate_qr = f"{self.host}/newlogin/qrcode/generate.do"
|
||||||
|
self.api_scan_status = f"{self.host}/newlogin/qrcode/query.do"
|
||||||
|
self.api_h5_tk = "https://h5api.m.goofish.com/h5/mtop.gaia.nodejs.gaia.idle.data.gw.v2.index.get/1.0/"
|
||||||
|
|
||||||
|
def _cookie_marshal(self, cookies: dict) -> str:
|
||||||
|
"""将Cookie字典转换为字符串"""
|
||||||
|
return "; ".join([f"{k}={v}" for k, v in cookies.items()])
|
||||||
|
|
||||||
|
async def _get_mh5tk(self, session: QRLoginSession) -> dict:
|
||||||
|
"""获取m_h5_tk和m_h5_tk_enc"""
|
||||||
|
params = {
|
||||||
|
"jsv": "2.7.2",
|
||||||
|
"appKey": "34839810",
|
||||||
|
"t": int(time.time()),
|
||||||
|
"sign": "",
|
||||||
|
"v": "1.0",
|
||||||
|
"type": "originaljson",
|
||||||
|
"accountSite": "xianyu",
|
||||||
|
"dataType": "json",
|
||||||
|
"timeout": 20000,
|
||||||
|
"api": "mtop.gaia.nodejs.gaia.idle.data.gw.v2.index.get",
|
||||||
|
"sessionOption": "AutoLoginOnly",
|
||||||
|
"spm_cnt": "a21ybx.home.0.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(follow_redirects=True) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
self.api_h5_tk, params=params, headers=self.headers
|
||||||
|
)
|
||||||
|
cookies = {}
|
||||||
|
for k, v in resp.cookies.items():
|
||||||
|
cookies[k] = v
|
||||||
|
session.cookies[k] = v
|
||||||
|
return cookies
|
||||||
|
|
||||||
|
async def _get_login_params(self, session: QRLoginSession) -> dict:
|
||||||
|
"""获取二维码登录时需要的表单参数"""
|
||||||
|
params = {
|
||||||
|
"lang": "zh_cn",
|
||||||
|
"appName": "xianyu",
|
||||||
|
"appEntrance": "web",
|
||||||
|
"styleType": "vertical",
|
||||||
|
"bizParams": "",
|
||||||
|
"notLoadSsoView": False,
|
||||||
|
"notKeepLogin": False,
|
||||||
|
"isMobile": False,
|
||||||
|
"qrCodeFirst": False,
|
||||||
|
"stie": 77,
|
||||||
|
"rnd": random(),
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(follow_redirects=True) as client:
|
||||||
|
resp = await client.get(
|
||||||
|
self.api_mini_login,
|
||||||
|
params=params,
|
||||||
|
cookies=session.cookies,
|
||||||
|
headers=self.headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 正则匹配需要的json数据
|
||||||
|
pattern = r"window\.viewData\s*=\s*(\{.*?\});"
|
||||||
|
match = re.search(pattern, resp.text)
|
||||||
|
if match:
|
||||||
|
json_string = match.group(1)
|
||||||
|
view_data = json.loads(json_string)
|
||||||
|
data = view_data.get("loginFormData")
|
||||||
|
if data:
|
||||||
|
data["umidTag"] = "SERVER"
|
||||||
|
session.params.update(data)
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
raise GetLoginParamsError("未找到loginFormData")
|
||||||
|
else:
|
||||||
|
raise GetLoginParamsError("获取登录参数失败")
|
||||||
|
|
||||||
|
async def generate_qr_code(self) -> Dict[str, Any]:
|
||||||
|
"""生成二维码"""
|
||||||
|
try:
|
||||||
|
# 创建新的会话
|
||||||
|
session_id = str(uuid.uuid4())
|
||||||
|
session = QRLoginSession(session_id)
|
||||||
|
|
||||||
|
# 1. 获取m_h5_tk
|
||||||
|
await self._get_mh5tk(session)
|
||||||
|
logger.info(f"获取m_h5_tk成功: {session_id}")
|
||||||
|
|
||||||
|
# 2. 获取登录参数
|
||||||
|
login_params = await self._get_login_params(session)
|
||||||
|
logger.info(f"获取登录参数成功: {session_id}")
|
||||||
|
|
||||||
|
# 3. 生成二维码
|
||||||
|
async with httpx.AsyncClient(follow_redirects=True) as client:
|
||||||
|
resp = await client.get(
|
||||||
|
self.api_generate_qr,
|
||||||
|
params=login_params,
|
||||||
|
headers=self.headers
|
||||||
|
)
|
||||||
|
results = resp.json()
|
||||||
|
|
||||||
|
if results.get("content", {}).get("success") == True:
|
||||||
|
# 更新会话参数
|
||||||
|
session.params.update({
|
||||||
|
"t": results["content"]["data"]["t"],
|
||||||
|
"ck": results["content"]["data"]["ck"],
|
||||||
|
})
|
||||||
|
|
||||||
|
# 获取二维码内容
|
||||||
|
qr_content = results["content"]["data"]["codeContent"]
|
||||||
|
session.qr_content = qr_content
|
||||||
|
|
||||||
|
# 生成二维码图片(base64格式)
|
||||||
|
qr = qrcode.QRCode(
|
||||||
|
version=5,
|
||||||
|
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||||
|
box_size=10,
|
||||||
|
border=2,
|
||||||
|
)
|
||||||
|
qr.add_data(qr_content)
|
||||||
|
qr.make()
|
||||||
|
|
||||||
|
# 将二维码转换为base64
|
||||||
|
from io import BytesIO
|
||||||
|
import base64
|
||||||
|
|
||||||
|
qr_img = qr.make_image()
|
||||||
|
buffer = BytesIO()
|
||||||
|
qr_img.save(buffer, format='PNG')
|
||||||
|
qr_base64 = base64.b64encode(buffer.getvalue()).decode()
|
||||||
|
qr_data_url = f"data:image/png;base64,{qr_base64}"
|
||||||
|
|
||||||
|
session.qr_code_url = qr_data_url
|
||||||
|
session.status = 'waiting'
|
||||||
|
|
||||||
|
# 保存会话
|
||||||
|
self.sessions[session_id] = session
|
||||||
|
|
||||||
|
# 启动状态检查任务
|
||||||
|
asyncio.create_task(self._monitor_qr_status(session_id))
|
||||||
|
|
||||||
|
logger.info(f"二维码生成成功: {session_id}")
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'session_id': session_id,
|
||||||
|
'qr_code_url': qr_data_url
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise GetLoginQRCodeError("获取登录二维码失败")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"生成二维码失败: {e}")
|
||||||
|
return {'success': False, 'message': f'生成二维码失败: {str(e)}'}
|
||||||
|
|
||||||
|
async def _poll_qrcode_status(self, session: QRLoginSession) -> httpx.Response:
|
||||||
|
"""获取二维码扫描状态"""
|
||||||
|
async with httpx.AsyncClient(follow_redirects=True) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
self.api_scan_status,
|
||||||
|
data=session.params,
|
||||||
|
cookies=session.cookies,
|
||||||
|
headers=self.headers,
|
||||||
|
)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
async def _monitor_qr_status(self, session_id: str):
|
||||||
|
"""监控二维码状态"""
|
||||||
|
try:
|
||||||
|
session = self.sessions.get(session_id)
|
||||||
|
if not session:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"开始监控二维码状态: {session_id}")
|
||||||
|
|
||||||
|
# 监控登录状态
|
||||||
|
max_wait_time = 300 # 5分钟
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while time.time() - start_time < max_wait_time:
|
||||||
|
try:
|
||||||
|
# 检查会话是否还存在
|
||||||
|
if session_id not in self.sessions:
|
||||||
|
break
|
||||||
|
|
||||||
|
# 轮询二维码状态
|
||||||
|
resp = await self._poll_qrcode_status(session)
|
||||||
|
qrcode_status = (
|
||||||
|
resp.json()
|
||||||
|
.get("content", {})
|
||||||
|
.get("data", {})
|
||||||
|
.get("qrCodeStatus")
|
||||||
|
)
|
||||||
|
|
||||||
|
if qrcode_status == "CONFIRMED":
|
||||||
|
# 登录确认
|
||||||
|
if (
|
||||||
|
resp.json()
|
||||||
|
.get("content", {})
|
||||||
|
.get("data", {})
|
||||||
|
.get("iframeRedirect")
|
||||||
|
is True
|
||||||
|
):
|
||||||
|
# 账号被风控,需要手机验证
|
||||||
|
session.status = 'cancelled'
|
||||||
|
iframe_url = (
|
||||||
|
resp.json()
|
||||||
|
.get("content", {})
|
||||||
|
.get("data", {})
|
||||||
|
.get("iframeRedirectUrl")
|
||||||
|
)
|
||||||
|
logger.warning(f"账号被风控,需要手机验证: {session_id}, URL: {iframe_url}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# 登录成功
|
||||||
|
session.status = 'success'
|
||||||
|
|
||||||
|
# 保存Cookie
|
||||||
|
for k, v in resp.cookies.items():
|
||||||
|
session.cookies[k] = v
|
||||||
|
if k == 'unb':
|
||||||
|
session.unb = v
|
||||||
|
|
||||||
|
logger.info(f"扫码登录成功: {session_id}, UNB: {session.unb}")
|
||||||
|
break
|
||||||
|
|
||||||
|
elif qrcode_status == "NEW":
|
||||||
|
# 二维码未被扫描,继续轮询
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif qrcode_status == "EXPIRED":
|
||||||
|
# 二维码已过期
|
||||||
|
session.status = 'expired'
|
||||||
|
logger.info(f"二维码已过期: {session_id}")
|
||||||
|
break
|
||||||
|
|
||||||
|
elif qrcode_status == "SCANED":
|
||||||
|
# 二维码已被扫描,等待确认
|
||||||
|
if session.status == 'waiting':
|
||||||
|
session.status = 'scanned'
|
||||||
|
logger.info(f"二维码已扫描,等待确认: {session_id}")
|
||||||
|
else:
|
||||||
|
# 用户取消确认
|
||||||
|
session.status = 'cancelled'
|
||||||
|
logger.info(f"用户取消登录: {session_id}")
|
||||||
|
break
|
||||||
|
|
||||||
|
await asyncio.sleep(0.8) # 每0.8秒检查一次
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"监控二维码状态异常: {e}")
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
# 超时处理
|
||||||
|
if session.status not in ['success', 'expired', 'cancelled']:
|
||||||
|
session.status = 'expired'
|
||||||
|
logger.info(f"二维码监控超时,标记为过期: {session_id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"监控二维码状态失败: {e}")
|
||||||
|
if session_id in self.sessions:
|
||||||
|
self.sessions[session_id].status = 'expired'
|
||||||
|
|
||||||
|
def get_session_status(self, session_id: str) -> Dict[str, Any]:
|
||||||
|
"""获取会话状态"""
|
||||||
|
session = self.sessions.get(session_id)
|
||||||
|
if not session:
|
||||||
|
return {'status': 'not_found'}
|
||||||
|
|
||||||
|
if session.is_expired() and session.status != 'success':
|
||||||
|
session.status = 'expired'
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'status': session.status,
|
||||||
|
'session_id': session_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# 如果登录成功,返回Cookie信息
|
||||||
|
if session.status == 'success' and session.cookies and session.unb:
|
||||||
|
result['cookies'] = self._cookie_marshal(session.cookies)
|
||||||
|
result['unb'] = session.unb
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def cleanup_expired_sessions(self):
|
||||||
|
"""清理过期会话"""
|
||||||
|
expired_sessions = []
|
||||||
|
for session_id, session in self.sessions.items():
|
||||||
|
if session.is_expired():
|
||||||
|
expired_sessions.append(session_id)
|
||||||
|
|
||||||
|
for session_id in expired_sessions:
|
||||||
|
del self.sessions[session_id]
|
||||||
|
logger.info(f"清理过期会话: {session_id}")
|
||||||
|
|
||||||
|
def get_session_cookies(self, session_id: str) -> Optional[Dict[str, str]]:
|
||||||
|
"""获取会话Cookie"""
|
||||||
|
session = self.sessions.get(session_id)
|
||||||
|
if session and session.status == 'success':
|
||||||
|
return {
|
||||||
|
'cookies': self._cookie_marshal(session.cookies),
|
||||||
|
'unb': session.unb
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# 全局二维码登录管理器实例
|
||||||
|
qr_login_manager = QRLoginManager()
|
Loading…
x
Reference in New Issue
Block a user