重构Cookie监控系统:添加Web管理后台、Vue3前端界面、Cookie Cloud参数支持、5秒倒计时自动刷新、专业UI配色
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Cookie监控管理系统 - 后台服务
|
||||
"""
|
||||
from flask import Flask, jsonify
|
||||
from flask_cors import CORS
|
||||
import os
|
||||
|
||||
# 导入配置和数据库
|
||||
from config.settings import Config
|
||||
from models.database import db
|
||||
from routes.users import user_bp
|
||||
from routes.websites import website_bp
|
||||
from routes.cookies import cookie_bp
|
||||
from routes.detections import detection_bp
|
||||
from routes.notifications import notification_bp
|
||||
from routes.admin import admin_bp
|
||||
|
||||
|
||||
def create_app(config_class=Config):
|
||||
"""创建Flask应用"""
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config_class)
|
||||
|
||||
# 配置CORS(允许session cookie)
|
||||
CORS(app, supports_credentials=True)
|
||||
|
||||
# 初始化数据库
|
||||
db.init_app(app)
|
||||
|
||||
# 注册蓝图
|
||||
app.register_blueprint(admin_bp)
|
||||
app.register_blueprint(user_bp, url_prefix='/api/users')
|
||||
app.register_blueprint(website_bp, url_prefix='/api/websites')
|
||||
app.register_blueprint(cookie_bp, url_prefix='/api/cookies')
|
||||
app.register_blueprint(detection_bp, url_prefix='/api/detections')
|
||||
app.register_blueprint(notification_bp, url_prefix='/api/notifications')
|
||||
|
||||
# 健康检查接口
|
||||
@app.route('/api/health', methods=['GET'])
|
||||
def health_check():
|
||||
return jsonify({'status': 'ok', 'message': '服务运行中'})
|
||||
|
||||
# 创建数据库表
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
配置文件
|
||||
"""
|
||||
import os
|
||||
|
||||
class Config:
|
||||
"""基础配置"""
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'cookie-monitor-secret-key'
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///cookie_monitor.db'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
JSON_AS_ASCII = False
|
||||
ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD') or 'admin'
|
||||
Binary file not shown.
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
数据库模型定义
|
||||
"""
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from datetime import datetime
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
"""用户表"""
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
iyuu_token = db.Column(db.String(255))
|
||||
max_fail_count = db.Column(db.Integer, default=3)
|
||||
cookie_cloud_uuid = db.Column(db.String(255))
|
||||
cookie_cloud_password = db.Column(db.String(255))
|
||||
cookie_cloud_api_url = db.Column(db.String(255))
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 关联关系
|
||||
websites = db.relationship('Website', backref='user', lazy=True, cascade='all, delete-orphan')
|
||||
notifications = db.relationship('Notification', backref='user', lazy=True)
|
||||
|
||||
|
||||
class Website(db.Model):
|
||||
"""网站表"""
|
||||
__tablename__ = 'websites'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
url = db.Column(db.String(255), nullable=False)
|
||||
login_check_selector = db.Column(db.String(255))
|
||||
success_text = db.Column(db.String(255))
|
||||
interval_minutes = db.Column(db.Integer, default=30, name='interval')
|
||||
status = db.Column(db.Integer, default=1)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 关联关系
|
||||
cookies = db.relationship('Cookie', backref='website', lazy=True, cascade='all, delete-orphan')
|
||||
detections = db.relationship('Detection', backref='website', lazy=True, cascade='all, delete-orphan')
|
||||
notifications = db.relationship('Notification', backref='website', lazy=True)
|
||||
|
||||
|
||||
class Cookie(db.Model):
|
||||
"""Cookie表"""
|
||||
__tablename__ = 'cookies'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
website_id = db.Column(db.Integer, db.ForeignKey('websites.id'), nullable=False)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
value = db.Column(db.Text, nullable=False)
|
||||
domain = db.Column(db.String(255), nullable=False)
|
||||
path = db.Column(db.String(255), default='/')
|
||||
expiry = db.Column(db.DateTime)
|
||||
secure = db.Column(db.Integer, default=0)
|
||||
http_only = db.Column(db.Integer, default=0)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
|
||||
class Detection(db.Model):
|
||||
"""检测结果表"""
|
||||
__tablename__ = 'detections'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
website_id = db.Column(db.Integer, db.ForeignKey('websites.id'), nullable=False)
|
||||
status = db.Column(db.Integer, nullable=False)
|
||||
response_time = db.Column(db.Float)
|
||||
http_status = db.Column(db.Integer)
|
||||
message = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
class Notification(db.Model):
|
||||
"""通知表"""
|
||||
__tablename__ = 'notifications'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
website_id = db.Column(db.Integer, db.ForeignKey('websites.id'), nullable=False)
|
||||
title = db.Column(db.String(100), nullable=False)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
status = db.Column(db.String(20), default='pending')
|
||||
send_time = db.Column(db.DateTime)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
@@ -0,0 +1,5 @@
|
||||
Flask==3.0.0
|
||||
Flask-SQLAlchemy==3.1.1
|
||||
Flask-CORS==4.0.0
|
||||
requests==2.31.0
|
||||
DrissionPage==4.1.1.2
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
后台管理路由
|
||||
"""
|
||||
from flask import Blueprint, render_template, request, jsonify, session
|
||||
from config.settings import Config
|
||||
|
||||
admin_bp = Blueprint('admin', __name__)
|
||||
|
||||
|
||||
@admin_bp.route('/')
|
||||
@admin_bp.route('/<path:path>')
|
||||
def admin_page(path=None):
|
||||
"""管理后台页面"""
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
@admin_bp.route('/api/admin/login', methods=['POST'])
|
||||
def admin_login():
|
||||
"""后台登录验证"""
|
||||
data = request.json
|
||||
password = data.get('password', '')
|
||||
|
||||
if password == Config.ADMIN_PASSWORD:
|
||||
session['admin_logged_in'] = True
|
||||
return jsonify({'success': True, 'message': '登录成功'})
|
||||
else:
|
||||
return jsonify({'success': False, 'message': '密码错误'}), 401
|
||||
|
||||
|
||||
@admin_bp.route('/api/admin/logout', methods=['POST'])
|
||||
def admin_logout():
|
||||
"""后台退出登录"""
|
||||
session.pop('admin_logged_in', None)
|
||||
return jsonify({'success': True, 'message': '已退出'})
|
||||
|
||||
|
||||
@admin_bp.route('/api/admin/status', methods=['GET'])
|
||||
def admin_status():
|
||||
"""检查后台登录状态"""
|
||||
return jsonify({'logged_in': session.get('admin_logged_in', False)})
|
||||
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Cookie管理路由
|
||||
"""
|
||||
from flask import Blueprint, request, jsonify
|
||||
from models.database import db, Cookie
|
||||
from datetime import datetime
|
||||
|
||||
cookie_bp = Blueprint('cookies', __name__)
|
||||
|
||||
|
||||
@cookie_bp.route('/websites/<int:website_id>', methods=['GET'])
|
||||
def get_cookies(website_id):
|
||||
"""获取网站的cookie"""
|
||||
cookies = Cookie.query.filter_by(website_id=website_id).all()
|
||||
return jsonify([cookie.to_dict() for cookie in cookies])
|
||||
|
||||
|
||||
@cookie_bp.route('/websites/<int:website_id>', methods=['POST'])
|
||||
def create_cookie(website_id):
|
||||
"""创建cookie"""
|
||||
data = request.json
|
||||
|
||||
cookie = Cookie(
|
||||
website_id=website_id,
|
||||
name=data.get('name'),
|
||||
value=data.get('value'),
|
||||
domain=data.get('domain'),
|
||||
path=data.get('path', '/'),
|
||||
expiry=datetime.fromisoformat(data.get('expiry')) if data.get('expiry') else None,
|
||||
secure=data.get('secure', 0),
|
||||
http_only=data.get('http_only', 0)
|
||||
)
|
||||
|
||||
db.session.add(cookie)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(cookie.to_dict()), 201
|
||||
|
||||
|
||||
@cookie_bp.route('/<int:cookie_id>', methods=['PUT'])
|
||||
def update_cookie(cookie_id):
|
||||
"""更新cookie"""
|
||||
cookie = Cookie.query.get_or_404(cookie_id)
|
||||
data = request.json
|
||||
|
||||
cookie.name = data.get('name', cookie.name)
|
||||
cookie.value = data.get('value', cookie.value)
|
||||
cookie.domain = data.get('domain', cookie.domain)
|
||||
cookie.path = data.get('path', cookie.path)
|
||||
cookie.expiry = datetime.fromisoformat(data.get('expiry')) if data.get('expiry') else cookie.expiry
|
||||
cookie.secure = data.get('secure', cookie.secure)
|
||||
cookie.http_only = data.get('http_only', cookie.http_only)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(cookie.to_dict())
|
||||
|
||||
|
||||
@cookie_bp.route('/<int:cookie_id>', methods=['DELETE'])
|
||||
def delete_cookie(cookie_id):
|
||||
"""删除cookie"""
|
||||
cookie = Cookie.query.get_or_404(cookie_id)
|
||||
|
||||
db.session.delete(cookie)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'Cookie已删除'})
|
||||
|
||||
|
||||
@cookie_bp.route('/websites/<int:website_id>', methods=['PUT'])
|
||||
def batch_update_cookies(website_id):
|
||||
"""批量更新网站的cookie"""
|
||||
data = request.json
|
||||
cookies_data = data.get('cookies', [])
|
||||
|
||||
# 删除该网站的所有现有cookie
|
||||
Cookie.query.filter_by(website_id=website_id).delete()
|
||||
|
||||
# 添加新的cookie
|
||||
for cookie_data in cookies_data:
|
||||
cookie = Cookie(
|
||||
website_id=website_id,
|
||||
name=cookie_data.get('name'),
|
||||
value=cookie_data.get('value'),
|
||||
domain=cookie_data.get('domain'),
|
||||
path=cookie_data.get('path', '/'),
|
||||
expiry=datetime.fromisoformat(cookie_data.get('expiry')) if cookie_data.get('expiry') else None,
|
||||
secure=cookie_data.get('secure', 0),
|
||||
http_only=cookie_data.get('http_only', 0)
|
||||
)
|
||||
db.session.add(cookie)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'Cookies已批量更新'})
|
||||
|
||||
|
||||
# 为Cookie模型添加to_dict方法
|
||||
def cookie_to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'website_id': self.website_id,
|
||||
'name': self.name,
|
||||
'value': self.value,
|
||||
'domain': self.domain,
|
||||
'path': self.path,
|
||||
'expiry': self.expiry.isoformat() if self.expiry else None,
|
||||
'secure': self.secure,
|
||||
'http_only': self.http_only,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
Cookie.to_dict = cookie_to_dict
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
检测结果路由
|
||||
"""
|
||||
from flask import Blueprint, request, jsonify
|
||||
from models.database import db, Detection
|
||||
|
||||
detection_bp = Blueprint('detections', __name__)
|
||||
|
||||
|
||||
@detection_bp.route('/', methods=['GET'])
|
||||
def get_detections():
|
||||
"""获取检测结果"""
|
||||
website_id = request.args.get('website_id')
|
||||
|
||||
if website_id:
|
||||
detections = Detection.query.filter_by(website_id=website_id).order_by(Detection.created_at.desc()).all()
|
||||
else:
|
||||
detections = Detection.query.order_by(Detection.created_at.desc()).all()
|
||||
|
||||
return jsonify([detection.to_dict() for detection in detections])
|
||||
|
||||
|
||||
@detection_bp.route('/', methods=['POST'])
|
||||
def create_detection():
|
||||
"""保存检测结果"""
|
||||
data = request.json
|
||||
|
||||
detection = Detection(
|
||||
website_id=data.get('website_id'),
|
||||
status=data.get('status'),
|
||||
response_time=data.get('response_time'),
|
||||
http_status=data.get('http_status'),
|
||||
message=data.get('message')
|
||||
)
|
||||
|
||||
db.session.add(detection)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(detection.to_dict()), 201
|
||||
|
||||
|
||||
@detection_bp.route('/latest', methods=['GET'])
|
||||
def get_latest_detections():
|
||||
"""获取每个网站的最新检测结果"""
|
||||
from sqlalchemy import func
|
||||
subquery = db.session.query(
|
||||
Detection.website_id,
|
||||
func.max(Detection.id).label('max_id')
|
||||
).group_by(Detection.website_id).subquery()
|
||||
|
||||
detections = db.session.query(Detection).join(
|
||||
subquery,
|
||||
(Detection.website_id == subquery.c.website_id) &
|
||||
(Detection.id == subquery.c.max_id)
|
||||
).all()
|
||||
|
||||
result = {}
|
||||
for d in detections:
|
||||
result[d.website_id] = d.to_dict()
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@detection_bp.route('/<int:detection_id>', methods=['GET'])
|
||||
def get_detection(detection_id):
|
||||
"""获取单个检测结果"""
|
||||
detection = Detection.query.get_or_404(detection_id)
|
||||
return jsonify(detection.to_dict())
|
||||
|
||||
|
||||
# 为Detection模型添加to_dict方法
|
||||
def detection_to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'website_id': self.website_id,
|
||||
'status': self.status,
|
||||
'response_time': self.response_time,
|
||||
'http_status': self.http_status,
|
||||
'message': self.message,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
Detection.to_dict = detection_to_dict
|
||||
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
通知管理路由
|
||||
"""
|
||||
import requests
|
||||
from flask import Blueprint, request, jsonify
|
||||
from models.database import db, Notification, User
|
||||
from datetime import datetime
|
||||
|
||||
notification_bp = Blueprint('notifications', __name__)
|
||||
|
||||
|
||||
@notification_bp.route('/', methods=['GET'])
|
||||
def get_notifications():
|
||||
"""获取通知列表"""
|
||||
user_id = request.args.get('user_id')
|
||||
|
||||
if user_id:
|
||||
notifications = Notification.query.filter_by(user_id=user_id).order_by(Notification.created_at.desc()).all()
|
||||
else:
|
||||
notifications = Notification.query.order_by(Notification.created_at.desc()).all()
|
||||
|
||||
return jsonify([notification.to_dict() for notification in notifications])
|
||||
|
||||
|
||||
@notification_bp.route('/', methods=['POST'])
|
||||
def send_notification():
|
||||
"""发送通知"""
|
||||
data = request.json
|
||||
|
||||
user_id = data.get('user_id')
|
||||
website_id = data.get('website_id')
|
||||
title = data.get('title')
|
||||
content = data.get('content')
|
||||
|
||||
# 获取用户的爱语飞飞令牌
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
if not user.iyuu_token:
|
||||
return jsonify({'message': '用户未配置爱语飞飞令牌'}), 400
|
||||
|
||||
# 发送爱语飞飞通知
|
||||
send_success = _send_iyuu_notification(user.iyuu_token, title, content)
|
||||
|
||||
# 创建通知记录
|
||||
notification = Notification(
|
||||
user_id=user_id,
|
||||
website_id=website_id,
|
||||
title=title,
|
||||
content=content,
|
||||
status='sent' if send_success else 'failed',
|
||||
send_time=datetime.utcnow() if send_success else None
|
||||
)
|
||||
|
||||
db.session.add(notification)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(notification.to_dict()), 201
|
||||
|
||||
|
||||
def _send_iyuu_notification(token, title, content):
|
||||
"""通过爱语飞飞发送通知"""
|
||||
try:
|
||||
url = f"https://iyuu.cn/{token}.send"
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
json={
|
||||
'text': title,
|
||||
'desp': content
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('errcode') == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
except requests.RequestException:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# 为Notification模型添加to_dict方法
|
||||
def notification_to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'user_id': self.user_id,
|
||||
'website_id': self.website_id,
|
||||
'title': self.title,
|
||||
'content': self.content,
|
||||
'status': self.status,
|
||||
'send_time': self.send_time.isoformat() if self.send_time else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
Notification.to_dict = notification_to_dict
|
||||
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
用户管理路由
|
||||
"""
|
||||
from flask import Blueprint, request, jsonify
|
||||
from models.database import db, User
|
||||
|
||||
user_bp = Blueprint('users', __name__)
|
||||
|
||||
|
||||
@user_bp.route('/', methods=['GET'])
|
||||
def get_users():
|
||||
"""获取所有用户"""
|
||||
users = User.query.all()
|
||||
return jsonify([user.to_dict() for user in users])
|
||||
|
||||
|
||||
@user_bp.route('/', methods=['POST'])
|
||||
def create_user():
|
||||
"""创建用户"""
|
||||
data = request.json
|
||||
user = User(
|
||||
name=data.get('name'),
|
||||
iyuu_token=data.get('iyuu_token'),
|
||||
max_fail_count=data.get('max_fail_count', 3),
|
||||
cookie_cloud_uuid=data.get('cookie_cloud_uuid'),
|
||||
cookie_cloud_password=data.get('cookie_cloud_password'),
|
||||
cookie_cloud_api_url=data.get('cookie_cloud_api_url')
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return jsonify(user.to_dict()), 201
|
||||
|
||||
|
||||
@user_bp.route('/<int:user_id>', methods=['GET'])
|
||||
def get_user(user_id):
|
||||
"""获取单个用户"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
return jsonify(user.to_dict())
|
||||
|
||||
|
||||
@user_bp.route('/<int:user_id>', methods=['PUT'])
|
||||
def update_user(user_id):
|
||||
"""更新用户"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
data = request.json
|
||||
user.name = data.get('name', user.name)
|
||||
user.iyuu_token = data.get('iyuu_token', user.iyuu_token)
|
||||
user.max_fail_count = data.get('max_fail_count', user.max_fail_count)
|
||||
user.cookie_cloud_uuid = data.get('cookie_cloud_uuid', user.cookie_cloud_uuid)
|
||||
user.cookie_cloud_password = data.get('cookie_cloud_password', user.cookie_cloud_password)
|
||||
user.cookie_cloud_api_url = data.get('cookie_cloud_api_url', user.cookie_cloud_api_url)
|
||||
db.session.commit()
|
||||
return jsonify(user.to_dict())
|
||||
|
||||
|
||||
@user_bp.route('/<int:user_id>', methods=['DELETE'])
|
||||
def delete_user(user_id):
|
||||
"""删除用户"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
return jsonify({'message': '用户已删除'})
|
||||
|
||||
|
||||
def user_to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'iyuu_token': self.iyuu_token,
|
||||
'max_fail_count': self.max_fail_count,
|
||||
'cookie_cloud_uuid': self.cookie_cloud_uuid,
|
||||
'cookie_cloud_password': self.cookie_cloud_password,
|
||||
'cookie_cloud_api_url': self.cookie_cloud_api_url,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
User.to_dict = user_to_dict
|
||||
@@ -0,0 +1,237 @@
|
||||
"""
|
||||
网站管理路由
|
||||
"""
|
||||
from flask import Blueprint, request, jsonify
|
||||
from models.database import db, Website
|
||||
from models.database import Cookie as CookieModel
|
||||
import requests as http_requests
|
||||
from datetime import datetime
|
||||
|
||||
website_bp = Blueprint('websites', __name__)
|
||||
|
||||
|
||||
@website_bp.route('/check-network', methods=['GET'])
|
||||
def check_all_network():
|
||||
"""检测所有网站的网络可达性"""
|
||||
websites = Website.query.all()
|
||||
result = {}
|
||||
|
||||
for website in websites:
|
||||
try:
|
||||
resp = http_requests.get(website.url, timeout=5, allow_redirects=True)
|
||||
result[website.id] = {
|
||||
'status': resp.status_code,
|
||||
'reachable': resp.status_code < 400,
|
||||
'response_time': resp.elapsed.total_seconds() * 1000
|
||||
}
|
||||
except Exception:
|
||||
result[website.id] = {
|
||||
'status': 0,
|
||||
'reachable': False,
|
||||
'response_time': 0
|
||||
}
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@website_bp.route('/check-login/<int:website_id>', methods=['POST'])
|
||||
def check_website_login(website_id):
|
||||
"""检查网站登录状态"""
|
||||
from DrissionPage import ChromiumPage, ChromiumOptions
|
||||
|
||||
website = Website.query.get_or_404(website_id)
|
||||
|
||||
try:
|
||||
# 获取网站的cookie
|
||||
cookies = CookieModel.query.filter_by(website_id=website_id).all()
|
||||
if not cookies:
|
||||
return jsonify({'success': False, 'message': '没有可用的cookie'})
|
||||
|
||||
# 初始化浏览器
|
||||
co = ChromiumOptions()
|
||||
co.set_browser_path(r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe")
|
||||
co.set_argument('--no-sandbox')
|
||||
co.set_argument('--disable-dev-shm-usage')
|
||||
browser = ChromiumPage(addr_or_opts=co)
|
||||
|
||||
# 导入cookie
|
||||
for cookie in cookies:
|
||||
cookie_dict = {
|
||||
'name': cookie.name,
|
||||
'value': cookie.value,
|
||||
'domain': cookie.domain,
|
||||
'path': cookie.path
|
||||
}
|
||||
browser.set.cookies(cookie_dict)
|
||||
|
||||
# 访问网站
|
||||
browser.get(website.url)
|
||||
browser.wait.doc_loaded(timeout=30)
|
||||
|
||||
import time
|
||||
time.sleep(2)
|
||||
browser.refresh()
|
||||
browser.wait.doc_loaded(timeout=30)
|
||||
time.sleep(2)
|
||||
|
||||
# 检查登录状态
|
||||
login_success = False
|
||||
check_selector = website.login_check_selector
|
||||
success_text = website.success_text
|
||||
|
||||
if check_selector:
|
||||
try:
|
||||
element = browser.ele(check_selector, timeout=10)
|
||||
if element:
|
||||
element_text = element.text or ""
|
||||
if not success_text:
|
||||
login_success = True
|
||||
elif success_text in element_text:
|
||||
login_success = True
|
||||
except:
|
||||
pass
|
||||
|
||||
if not login_success and success_text:
|
||||
try:
|
||||
page_text = browser.html or ""
|
||||
if success_text in page_text:
|
||||
login_success = True
|
||||
except:
|
||||
pass
|
||||
|
||||
# 如果登录成功,获取最新cookie
|
||||
if login_success:
|
||||
new_cookies = browser.cookies()
|
||||
# 更新cookie
|
||||
CookieModel.query.filter_by(website_id=website_id).delete()
|
||||
for c in new_cookies:
|
||||
new_cookie = CookieModel(
|
||||
website_id=website_id,
|
||||
name=c.get('name', ''),
|
||||
value=c.get('value', ''),
|
||||
domain=c.get('domain', ''),
|
||||
path=c.get('path', '/')
|
||||
)
|
||||
db.session.add(new_cookie)
|
||||
|
||||
# 保存检测结果
|
||||
from models.database import Detection
|
||||
detection = Detection(
|
||||
website_id=website_id,
|
||||
status=1,
|
||||
response_time=0,
|
||||
http_status=200,
|
||||
message='登录验证成功'
|
||||
)
|
||||
db.session.add(detection)
|
||||
db.session.commit()
|
||||
|
||||
result = {'success': True, 'message': '登录成功'}
|
||||
else:
|
||||
# 保存检测结果
|
||||
from models.database import Detection
|
||||
detection = Detection(
|
||||
website_id=website_id,
|
||||
status=0,
|
||||
response_time=0,
|
||||
http_status=200,
|
||||
message='登录验证失败'
|
||||
)
|
||||
db.session.add(detection)
|
||||
db.session.commit()
|
||||
|
||||
result = {'success': False, 'message': '登录失败'}
|
||||
|
||||
browser.quit()
|
||||
return jsonify(result)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f'检测异常: {str(e)}'})
|
||||
|
||||
|
||||
@website_bp.route('/', methods=['GET'])
|
||||
def get_websites():
|
||||
"""获取所有网站"""
|
||||
user_id = request.args.get('user_id')
|
||||
|
||||
if user_id:
|
||||
websites = Website.query.filter_by(user_id=user_id).all()
|
||||
else:
|
||||
websites = Website.query.all()
|
||||
|
||||
return jsonify([website.to_dict() for website in websites])
|
||||
|
||||
|
||||
@website_bp.route('/', methods=['POST'])
|
||||
def create_website():
|
||||
"""创建网站"""
|
||||
data = request.json
|
||||
|
||||
website = Website(
|
||||
user_id=data.get('user_id'),
|
||||
name=data.get('name'),
|
||||
url=data.get('url'),
|
||||
login_check_selector=data.get('login_check_selector'),
|
||||
success_text=data.get('success_text'),
|
||||
interval_minutes=data.get('interval_minutes', 30),
|
||||
status=data.get('status', 1)
|
||||
)
|
||||
|
||||
db.session.add(website)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(website.to_dict()), 201
|
||||
|
||||
|
||||
@website_bp.route('/<int:website_id>', methods=['GET'])
|
||||
def get_website(website_id):
|
||||
"""获取单个网站"""
|
||||
website = Website.query.get_or_404(website_id)
|
||||
return jsonify(website.to_dict())
|
||||
|
||||
|
||||
@website_bp.route('/<int:website_id>', methods=['PUT'])
|
||||
def update_website(website_id):
|
||||
"""更新网站"""
|
||||
website = Website.query.get_or_404(website_id)
|
||||
data = request.json
|
||||
|
||||
website.name = data.get('name', website.name)
|
||||
website.url = data.get('url', website.url)
|
||||
website.login_check_selector = data.get('login_check_selector', website.login_check_selector)
|
||||
website.success_text = data.get('success_text', website.success_text)
|
||||
website.interval_minutes = data.get('interval_minutes', website.interval_minutes)
|
||||
website.status = data.get('status', website.status)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(website.to_dict())
|
||||
|
||||
|
||||
@website_bp.route('/<int:website_id>', methods=['DELETE'])
|
||||
def delete_website(website_id):
|
||||
"""删除网站"""
|
||||
website = Website.query.get_or_404(website_id)
|
||||
|
||||
db.session.delete(website)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': '网站已删除'})
|
||||
|
||||
|
||||
# 为Website模型添加to_dict方法
|
||||
def website_to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'user_id': self.user_id,
|
||||
'name': self.name,
|
||||
'url': self.url,
|
||||
'login_check_selector': self.login_check_selector,
|
||||
'success_text': self.success_text,
|
||||
'interval_minutes': self.interval_minutes,
|
||||
'status': self.status,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
Website.to_dict = website_to_dict
|
||||
@@ -0,0 +1,527 @@
|
||||
/* Cookie监控管理系统 - 专业版样式 */
|
||||
|
||||
:root {
|
||||
--primary-color: #4f6ef7;
|
||||
--primary-dark: #3b5bdb;
|
||||
--primary-light: #e8edff;
|
||||
--success-color: #10b981;
|
||||
--success-light: #d1fae5;
|
||||
--danger-color: #ef4444;
|
||||
--danger-light: #fee2e2;
|
||||
--warning-color: #f59e0b;
|
||||
--warning-light: #fef3c7;
|
||||
--info-color: #6b7280;
|
||||
--info-light: #f3f4f6;
|
||||
--header-bg: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
--card-bg: #ffffff;
|
||||
--page-bg: #f0f2f5;
|
||||
--text-primary: #1f2937;
|
||||
--text-secondary: #6b7280;
|
||||
--text-light: #9ca3af;
|
||||
--border-color: #e5e7eb;
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
--radius: 8px;
|
||||
--radius-lg: 12px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
background-color: var(--page-bg);
|
||||
color: var(--text-primary);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: var(--page-bg);
|
||||
}
|
||||
|
||||
.el-header {
|
||||
background: var(--header-bg);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 32px;
|
||||
height: 64px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.el-header h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #fff 0%, #e0e7ff 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-right .el-button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.header-right .el-button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.auto-refresh-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 13px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.rotating-icon {
|
||||
animation: rotate 5s linear infinite;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.refresh-text {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.el-main {
|
||||
background: var(--page-bg);
|
||||
padding: 24px 32px;
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
/* 仪表板样式 */
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 28px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.dashboard-header h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.dashboard-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.dashboard-stats .el-statistic {
|
||||
padding: 16px 24px;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
min-width: 140px;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dashboard-stats .el-statistic:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.dashboard-stats .el-statistic__title {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard-stats .el-statistic__content {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 用户区域卡片 */
|
||||
.user-section {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.user-section:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.user-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 2px solid var(--primary-light);
|
||||
}
|
||||
|
||||
.user-header h3 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-header h3::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
background: var(--primary-color);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.user-header .el-tag {
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
.el-table {
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-table th.el-table__cell {
|
||||
background: var(--page-bg);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.el-table td.el-table__cell {
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.el-table--enable-row-hover .el-table__body tr:hover > td.el-table__cell {
|
||||
background-color: var(--primary-light);
|
||||
}
|
||||
|
||||
.el-table::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 状态标签样式 */
|
||||
.el-tag {
|
||||
border-radius: 20px;
|
||||
padding: 4px 12px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tag--success {
|
||||
background: var(--success-light);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.el-tag--danger {
|
||||
background: var(--danger-light);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.el-tag--warning {
|
||||
background: var(--warning-light);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.el-tag--info {
|
||||
background: var(--info-light);
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.el-button--primary {
|
||||
background: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.el-button--primary:hover {
|
||||
background: var(--primary-dark);
|
||||
border-color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.el-button--success {
|
||||
background: var(--success-color);
|
||||
border-color: var(--success-color);
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.el-button--success:hover {
|
||||
background: #059669;
|
||||
border-color: #059669;
|
||||
}
|
||||
|
||||
.el-button--warning {
|
||||
background: var(--warning-color);
|
||||
border-color: var(--warning-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-button--primary.is-plain {
|
||||
background: transparent;
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.el-button--primary.is-plain:hover {
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
/* 链接样式 */
|
||||
.el-link--primary {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-link:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 设置页面样式 */
|
||||
.website-manage-header,
|
||||
.admin-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 2px solid var(--primary-light);
|
||||
}
|
||||
|
||||
.website-manage-header h2,
|
||||
.admin-header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.el-tabs {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
background: var(--primary-color);
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.el-form-item__label {
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border-color);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.el-input__inner:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px var(--primary-light);
|
||||
}
|
||||
|
||||
.el-input-number {
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
/* 对话框样式 */
|
||||
.el-dialog {
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-dialog__title {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.el-divider__text {
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
background: var(--card-bg);
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
/* Cookie值样式 */
|
||||
.cookie-value {
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
word-break: break-all;
|
||||
background: var(--page-bg);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 筛选表单 */
|
||||
.filter-form {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.el-empty {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.el-empty__description {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 统计数字卡片颜色 */
|
||||
.stat-total .el-statistic__content {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.stat-online .el-statistic__content {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.stat-offline .el-statistic__content {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.stat-unknown .el-statistic__content {
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 768px) {
|
||||
.el-header {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.dashboard-stats {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.user-section {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条美化 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--page-bg);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--text-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-secondary);
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
const { createApp, ref, onMounted, computed } = Vue;
|
||||
|
||||
createApp({
|
||||
setup() {
|
||||
const activeTab = ref('dashboard');
|
||||
const settingsTab = ref('users');
|
||||
const manageTab = ref('cookies');
|
||||
const currentWebsite = ref(null);
|
||||
const currentUser = ref(null);
|
||||
const isAdmin = ref(false);
|
||||
|
||||
const users = ref([]);
|
||||
const websites = ref([]);
|
||||
const dashboardData = ref([]);
|
||||
const websiteCookies = ref([]);
|
||||
const websiteDetections = ref([]);
|
||||
const detections = ref([]);
|
||||
const notifications = ref([]);
|
||||
|
||||
const stats = ref({ totalWebsites: 0, onlineCount: 0, offlineCount: 0, unknownCount: 0 });
|
||||
const countdown = ref(5);
|
||||
|
||||
const adminLoginVisible = ref(false);
|
||||
const adminLoginForm = ref({ password: '' });
|
||||
|
||||
const userDialogVisible = ref(false);
|
||||
const userDialogTitle = ref('添加用户');
|
||||
const userForm = ref({ id: null, name: '', iyuu_token: '', max_fail_count: 3, cookie_cloud_uuid: '', cookie_cloud_password: '', cookie_cloud_api_url: '' });
|
||||
|
||||
const websiteDialogVisible = ref(false);
|
||||
const websiteDialogTitle = ref('添加网站');
|
||||
const websiteFormDialog = ref({
|
||||
id: null, user_id: null, name: '', url: '',
|
||||
login_check_selector: '', success_text: '',
|
||||
interval_minutes: 30, status: 1
|
||||
});
|
||||
|
||||
const cookieDialogVisible = ref(false);
|
||||
const cookieDialogTitle = ref('添加Cookie');
|
||||
const cookieForm = ref({ id: null, name: '', value: '', domain: '', path: '/' });
|
||||
|
||||
const websiteForm = ref({
|
||||
id: null, user_id: null, name: '', url: '',
|
||||
login_check_selector: '', success_text: '',
|
||||
interval_minutes: 30
|
||||
});
|
||||
|
||||
const filterWebsiteUser = ref(null);
|
||||
|
||||
const currentUserWebsites = computed(() => {
|
||||
if (!currentUser.value) return [];
|
||||
return websites.value.filter(w => w.user_id === currentUser.value.id);
|
||||
});
|
||||
|
||||
const filteredWebsites = computed(() => {
|
||||
if (!filterWebsiteUser.value) return websites.value;
|
||||
return websites.value.filter(w => w.user_id === filterWebsiteUser.value);
|
||||
});
|
||||
|
||||
const checkAdminStatus = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/admin/status');
|
||||
isAdmin.value = response.data.logged_in;
|
||||
} catch (error) {
|
||||
isAdmin.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const showAdminLogin = () => {
|
||||
adminLoginForm.value = { password: '' };
|
||||
adminLoginVisible.value = true;
|
||||
};
|
||||
|
||||
const adminLogin = async () => {
|
||||
try {
|
||||
const response = await axios.post('/api/admin/login', adminLoginForm.value);
|
||||
if (response.data.success) {
|
||||
isAdmin.value = true;
|
||||
adminLoginVisible.value = false;
|
||||
ElementPlus.ElMessage.success('登录成功');
|
||||
activeTab.value = 'settings';
|
||||
settingsTab.value = 'users';
|
||||
await loadUsersWithWebsites();
|
||||
}
|
||||
} catch (error) {
|
||||
ElementPlus.ElMessage.error('密码错误');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdminCommand = async (command) => {
|
||||
if (command === 'settings') {
|
||||
stopAutoRefresh();
|
||||
activeTab.value = 'settings';
|
||||
settingsTab.value = 'users';
|
||||
await loadUsersWithWebsites();
|
||||
} else if (command === 'dashboard') {
|
||||
activeTab.value = 'dashboard';
|
||||
await loadDashboard();
|
||||
startAutoRefresh();
|
||||
} else if (command === 'logout') {
|
||||
await axios.post('/api/admin/logout');
|
||||
isAdmin.value = false;
|
||||
activeTab.value = 'dashboard';
|
||||
ElementPlus.ElMessage.success('已退出');
|
||||
}
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/users');
|
||||
users.value = response.data;
|
||||
} catch (error) {
|
||||
console.error('加载用户失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadUsersWithWebsites = async () => {
|
||||
try {
|
||||
const usersRes = await axios.get('/api/users');
|
||||
const websitesRes = await axios.get('/api/websites');
|
||||
users.value = usersRes.data;
|
||||
websites.value = websitesRes.data;
|
||||
|
||||
users.value = users.value.map(user => ({
|
||||
...user,
|
||||
website_count: websitesRes.data.filter(w => w.user_id === user.id).length
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('加载用户失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadWebsites = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/websites');
|
||||
websites.value = response.data;
|
||||
} catch (error) {
|
||||
console.error('加载网站失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadDashboard = async () => {
|
||||
try {
|
||||
const usersRes = await axios.get('/api/users');
|
||||
const websitesRes = await axios.get('/api/websites');
|
||||
const latestDetectionsRes = await axios.get('/api/detections/latest');
|
||||
const networkRes = await axios.get('/api/websites/check-network');
|
||||
|
||||
const usersData = usersRes.data;
|
||||
const allWebsites = websitesRes.data;
|
||||
const latestDetections = latestDetectionsRes.data;
|
||||
const networkStatus = networkRes.data;
|
||||
|
||||
let onlineCount = 0, offlineCount = 0, unknownCount = 0, totalWebsites = allWebsites.length;
|
||||
|
||||
dashboardData.value = usersData.map(user => {
|
||||
const userWebsites = allWebsites.filter(w => w.user_id === user.id);
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
websites: userWebsites.map(w => {
|
||||
const detection = latestDetections[w.id];
|
||||
const net = networkStatus[w.id];
|
||||
let loginStatus = 'unknown';
|
||||
let lastCheckTime = '-';
|
||||
let networkStatusValue = 'unknown';
|
||||
|
||||
if (net) {
|
||||
networkStatusValue = net.reachable ? 'online' : 'offline';
|
||||
}
|
||||
|
||||
if (detection) {
|
||||
if (detection.status === 1) {
|
||||
loginStatus = 'success';
|
||||
onlineCount++;
|
||||
} else {
|
||||
loginStatus = 'failed';
|
||||
offlineCount++;
|
||||
}
|
||||
lastCheckTime = new Date(detection.created_at).toLocaleString();
|
||||
} else {
|
||||
unknownCount++;
|
||||
}
|
||||
|
||||
return {
|
||||
...w,
|
||||
networkStatus: networkStatusValue,
|
||||
loginStatus: loginStatus,
|
||||
lastCheckTime: lastCheckTime
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
stats.value = { totalWebsites, onlineCount, offlineCount, unknownCount };
|
||||
} catch (error) {
|
||||
console.error('加载仪表板失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshDashboardStatus = async () => {
|
||||
if (activeTab.value !== 'dashboard') return;
|
||||
if (isRefreshing) return;
|
||||
|
||||
isRefreshing = true;
|
||||
try {
|
||||
const [detectionsRes, networkRes] = await Promise.all([
|
||||
axios.get('/api/detections/latest'),
|
||||
axios.get('/api/websites/check-network')
|
||||
]);
|
||||
|
||||
const latestDetections = detectionsRes.data;
|
||||
const networkStatus = networkRes.data;
|
||||
|
||||
let onlineCount = 0, offlineCount = 0, unknownCount = 0;
|
||||
|
||||
dashboardData.value = dashboardData.value.map(user => {
|
||||
return {
|
||||
...user,
|
||||
websites: user.websites.map(w => {
|
||||
const detection = latestDetections[w.id];
|
||||
const net = networkStatus[w.id];
|
||||
let loginStatus = w.loginStatus;
|
||||
let lastCheckTime = w.lastCheckTime;
|
||||
let networkStatusValue = 'unknown';
|
||||
|
||||
if (net) {
|
||||
networkStatusValue = net.reachable ? 'online' : 'offline';
|
||||
}
|
||||
|
||||
if (detection) {
|
||||
if (detection.status === 1) {
|
||||
loginStatus = 'success';
|
||||
onlineCount++;
|
||||
} else {
|
||||
loginStatus = 'failed';
|
||||
offlineCount++;
|
||||
}
|
||||
lastCheckTime = new Date(detection.created_at).toLocaleString();
|
||||
} else {
|
||||
unknownCount++;
|
||||
}
|
||||
|
||||
return {
|
||||
...w,
|
||||
networkStatus: networkStatusValue,
|
||||
loginStatus: loginStatus,
|
||||
lastCheckTime: lastCheckTime
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
stats.value = {
|
||||
totalWebsites: onlineCount + offlineCount + unknownCount,
|
||||
onlineCount,
|
||||
offlineCount,
|
||||
unknownCount
|
||||
};
|
||||
} catch (error) {
|
||||
// 静默失败,不显示错误提示
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
if (activeTab.value === 'dashboard') {
|
||||
countdown.value = 5;
|
||||
startCountdown();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let refreshTimer = null;
|
||||
let countdownTimer = null;
|
||||
let isRefreshing = false;
|
||||
|
||||
const startAutoRefresh = () => {
|
||||
stopAutoRefresh();
|
||||
countdown.value = 5;
|
||||
startCountdown();
|
||||
};
|
||||
|
||||
const startCountdown = () => {
|
||||
if (countdownTimer) clearInterval(countdownTimer);
|
||||
countdownTimer = setInterval(() => {
|
||||
countdown.value--;
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
refreshDashboardStatus();
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
}
|
||||
countdown.value = 5;
|
||||
};
|
||||
|
||||
const checkLogin = async (website) => {
|
||||
website.loginStatus = 'checking';
|
||||
|
||||
try {
|
||||
const response = await axios.post(`/api/websites/check-login/${website.id}`);
|
||||
|
||||
if (response.data.success) {
|
||||
website.loginStatus = 'success';
|
||||
updateStats(website.id, 'success');
|
||||
ElementPlus.ElMessage.success(response.data.message);
|
||||
} else {
|
||||
website.loginStatus = 'failed';
|
||||
updateStats(website.id, 'failed');
|
||||
ElementPlus.ElMessage.error(response.data.message);
|
||||
}
|
||||
|
||||
website.lastCheckTime = new Date().toLocaleString();
|
||||
} catch (error) {
|
||||
website.loginStatus = 'failed';
|
||||
updateStats(website.id, 'failed');
|
||||
ElementPlus.ElMessage.error('检测失败');
|
||||
}
|
||||
};
|
||||
|
||||
const updateStats = (websiteId, status) => {
|
||||
let onlineCount = 0, offlineCount = 0, unknownCount = 0;
|
||||
dashboardData.value.forEach(user => {
|
||||
user.websites.forEach(w => {
|
||||
if (w.id === websiteId) {
|
||||
if (status === 'success') onlineCount++;
|
||||
else if (status === 'failed') offlineCount++;
|
||||
} else {
|
||||
if (w.loginStatus === 'success') onlineCount++;
|
||||
else if (w.loginStatus === 'failed') offlineCount++;
|
||||
else unknownCount++;
|
||||
}
|
||||
});
|
||||
});
|
||||
stats.value = { totalWebsites: onlineCount + offlineCount + unknownCount, onlineCount, offlineCount, unknownCount };
|
||||
};
|
||||
|
||||
const manageWebsite = async (website) => {
|
||||
stopAutoRefresh();
|
||||
currentWebsite.value = website;
|
||||
websiteForm.value = { ...website };
|
||||
activeTab.value = 'website_manage';
|
||||
manageTab.value = 'cookies';
|
||||
await loadWebsiteCookies();
|
||||
};
|
||||
|
||||
const manageUserWebsites = (user) => {
|
||||
stopAutoRefresh();
|
||||
currentUser.value = user;
|
||||
activeTab.value = 'user_websites';
|
||||
};
|
||||
|
||||
const loadWebsiteCookies = async () => {
|
||||
if (!currentWebsite.value) return;
|
||||
try {
|
||||
const response = await axios.get(`/api/cookies/websites/${currentWebsite.value.id}`);
|
||||
websiteCookies.value = response.data;
|
||||
} catch (error) {
|
||||
console.error('加载Cookie失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadWebsiteDetections = async () => {
|
||||
if (!currentWebsite.value) return;
|
||||
try {
|
||||
const response = await axios.get(`/api/detections?website_id=${currentWebsite.value.id}`);
|
||||
websiteDetections.value = response.data;
|
||||
} catch (error) {
|
||||
console.error('加载检测记录失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const showAddUserDialog = () => {
|
||||
userDialogTitle.value = '添加用户';
|
||||
userForm.value = { id: null, name: '', iyuu_token: '', max_fail_count: 3, cookie_cloud_uuid: '', cookie_cloud_password: '', cookie_cloud_api_url: '' };
|
||||
userDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const editUser = (user) => {
|
||||
userDialogTitle.value = '编辑用户';
|
||||
userForm.value = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
iyuu_token: user.iyuu_token,
|
||||
max_fail_count: user.max_fail_count,
|
||||
cookie_cloud_uuid: user.cookie_cloud_uuid || '',
|
||||
cookie_cloud_password: user.cookie_cloud_password || '',
|
||||
cookie_cloud_api_url: user.cookie_cloud_api_url || ''
|
||||
};
|
||||
userDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const saveUser = async () => {
|
||||
try {
|
||||
if (userForm.value.id) {
|
||||
await axios.put(`/api/users/${userForm.value.id}`, userForm.value);
|
||||
ElementPlus.ElMessage.success('更新成功');
|
||||
} else {
|
||||
await axios.post('/api/users', userForm.value);
|
||||
ElementPlus.ElMessage.success('添加成功');
|
||||
}
|
||||
userDialogVisible.value = false;
|
||||
await loadUsersWithWebsites();
|
||||
} catch (error) {
|
||||
ElementPlus.ElMessage.error('保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteUser = async (user) => {
|
||||
try {
|
||||
await ElementPlus.ElMessageBox.confirm('确定要删除该用户吗?', '提示', { type: 'warning' });
|
||||
await axios.delete(`/api/users/${user.id}`);
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
await loadUsersWithWebsites();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElementPlus.ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const showAddWebsiteDialog = () => {
|
||||
websiteDialogTitle.value = '添加网站';
|
||||
websiteFormDialog.value = {
|
||||
id: null, user_id: filterWebsiteUser.value || users.value[0]?.id,
|
||||
name: '', url: '', login_check_selector: '', success_text: '',
|
||||
interval_minutes: 30, status: 1
|
||||
};
|
||||
websiteDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const showAddWebsiteDialogForUser = () => {
|
||||
websiteDialogTitle.value = '添加网站';
|
||||
websiteFormDialog.value = {
|
||||
id: null, user_id: currentUser.value?.id,
|
||||
name: '', url: '', login_check_selector: '', success_text: '',
|
||||
interval_minutes: 30, status: 1
|
||||
};
|
||||
websiteDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const editWebsite = (website) => {
|
||||
websiteDialogTitle.value = '编辑网站';
|
||||
websiteFormDialog.value = { ...website };
|
||||
websiteDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const saveWebsite = async () => {
|
||||
try {
|
||||
if (websiteFormDialog.value.id) {
|
||||
await axios.put(`/api/websites/${websiteFormDialog.value.id}`, websiteFormDialog.value);
|
||||
ElementPlus.ElMessage.success('更新成功');
|
||||
} else {
|
||||
await axios.post('/api/websites', websiteFormDialog.value);
|
||||
ElementPlus.ElMessage.success('添加成功');
|
||||
}
|
||||
websiteDialogVisible.value = false;
|
||||
await loadWebsites();
|
||||
await loadUsersWithWebsites();
|
||||
} catch (error) {
|
||||
ElementPlus.ElMessage.error('保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteWebsite = async (website) => {
|
||||
try {
|
||||
await ElementPlus.ElMessageBox.confirm('确定要删除该网站吗?', '提示', { type: 'warning' });
|
||||
await axios.delete(`/api/websites/${website.id}`);
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
await loadWebsites();
|
||||
await loadUsersWithWebsites();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElementPlus.ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const showAddCookieDialog = () => {
|
||||
cookieDialogTitle.value = '添加Cookie';
|
||||
cookieForm.value = { id: null, name: '', value: '', domain: currentWebsite.value?.url ? new URL(currentWebsite.value.url).hostname : '', path: '/' };
|
||||
cookieDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const editCookie = (cookie) => {
|
||||
cookieDialogTitle.value = '编辑Cookie';
|
||||
cookieForm.value = { ...cookie };
|
||||
cookieDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const saveCookie = async () => {
|
||||
try {
|
||||
if (cookieForm.value.id) {
|
||||
await axios.put(`/api/cookies/${cookieForm.value.id}`, cookieForm.value);
|
||||
ElementPlus.ElMessage.success('更新成功');
|
||||
} else {
|
||||
await axios.post(`/api/cookies/websites/${currentWebsite.value.id}`, cookieForm.value);
|
||||
ElementPlus.ElMessage.success('添加成功');
|
||||
}
|
||||
cookieDialogVisible.value = false;
|
||||
await loadWebsiteCookies();
|
||||
} catch (error) {
|
||||
ElementPlus.ElMessage.error('保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteCookie = async (cookie) => {
|
||||
try {
|
||||
await ElementPlus.ElMessageBox.confirm('确定要删除该Cookie吗?', '提示', { type: 'warning' });
|
||||
await axios.delete(`/api/cookies/${cookie.id}`);
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
await loadWebsiteCookies();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElementPlus.ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const saveWebsiteSettings = async () => {
|
||||
try {
|
||||
await axios.put(`/api/websites/${currentWebsite.value.id}`, websiteForm.value);
|
||||
ElementPlus.ElMessage.success('保存成功');
|
||||
} catch (error) {
|
||||
ElementPlus.ElMessage.error('保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const maskCookieValue = (value) => {
|
||||
if (!value) return '';
|
||||
return value.length > 30 ? value.substring(0, 30) + '...' : value;
|
||||
};
|
||||
|
||||
const loadDetections = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/detections');
|
||||
detections.value = response.data;
|
||||
} catch (error) {
|
||||
console.error('加载检测结果失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadNotifications = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/notifications');
|
||||
notifications.value = response.data;
|
||||
} catch (error) {
|
||||
console.error('加载通知失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const viewNotificationDetail = (notification) => {
|
||||
ElementPlus.ElMessageBox.alert(notification.content, notification.title, { confirmButtonText: '确定' });
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await checkAdminStatus();
|
||||
await loadUsers();
|
||||
await loadWebsites();
|
||||
await loadDashboard();
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
return {
|
||||
activeTab, settingsTab, manageTab, currentWebsite, currentUser, isAdmin,
|
||||
users, websites, dashboardData, websiteCookies, websiteDetections, detections, notifications,
|
||||
stats, countdown,
|
||||
adminLoginVisible, adminLoginForm,
|
||||
userDialogVisible, userDialogTitle, userForm,
|
||||
websiteDialogVisible, websiteDialogTitle, websiteFormDialog,
|
||||
cookieDialogVisible, cookieDialogTitle, cookieForm,
|
||||
websiteForm, filterWebsiteUser,
|
||||
currentUserWebsites, filteredWebsites,
|
||||
checkAdminStatus, showAdminLogin, adminLogin, handleAdminCommand,
|
||||
loadUsers, loadUsersWithWebsites, loadWebsites, loadDashboard, refreshDashboardStatus,
|
||||
startAutoRefresh, stopAutoRefresh,
|
||||
checkLogin, manageWebsite, manageUserWebsites,
|
||||
loadWebsiteCookies, loadWebsiteDetections,
|
||||
showAddUserDialog, editUser, saveUser, deleteUser,
|
||||
showAddWebsiteDialog, showAddWebsiteDialogForUser, editWebsite, saveWebsite, deleteWebsite,
|
||||
showAddCookieDialog, editCookie, saveCookie, deleteCookie,
|
||||
saveWebsiteSettings, maskCookieValue,
|
||||
loadDetections, loadNotifications, viewNotificationDetail
|
||||
};
|
||||
}
|
||||
}).use(ElementPlus).mount('#app');
|
||||
@@ -0,0 +1,398 @@
|
||||
{% raw %}<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cookie监控管理系统</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<script src="https://unpkg.com/element-plus"></script>
|
||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="container">
|
||||
<el-container>
|
||||
<el-header>
|
||||
<div class="header-left">
|
||||
<h1>Cookie监控管理系统</h1>
|
||||
<el-button v-if="activeTab === 'settings'" @click="activeTab = 'dashboard'" type="primary" plain>返回监控首页</el-button>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div v-if="activeTab === 'dashboard'" class="auto-refresh-indicator">
|
||||
<el-icon class="rotating-icon"><Refresh /></el-icon>
|
||||
<span class="refresh-text">{{ countdown }}秒后自动刷新</span>
|
||||
</div>
|
||||
<el-button v-if="activeTab === 'dashboard'" @click="loadDashboard" type="primary" plain>刷新</el-button>
|
||||
<el-button @click="showAdminLogin" type="warning" plain v-if="!isAdmin">登录后台</el-button>
|
||||
<el-dropdown @command="handleAdminCommand" v-else>
|
||||
<el-button type="success" plain>后台管理 <el-icon><arrow-down /></el-icon></el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="settings">系统设置</el-dropdown-item>
|
||||
<el-dropdown-item command="dashboard">监控首页</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<el-main>
|
||||
<!-- 监控仪表板 -->
|
||||
<div v-if="activeTab === 'dashboard'">
|
||||
<div class="dashboard-header">
|
||||
<h2>网站监控仪表板</h2>
|
||||
<div class="dashboard-stats">
|
||||
<el-statistic title="网站总数" :value="stats.totalWebsites" />
|
||||
<el-statistic title="在线" :value="stats.onlineCount">
|
||||
<template #suffix><el-icon><circle-check /></el-icon></template>
|
||||
</el-statistic>
|
||||
<el-statistic title="离线" :value="stats.offlineCount">
|
||||
<template #suffix><el-icon><circle-close /></el-icon></template>
|
||||
</el-statistic>
|
||||
<el-statistic title="未检测" :value="stats.unknownCount" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="user in dashboardData" :key="user.id" class="user-section">
|
||||
<div class="user-header">
|
||||
<h3>{{ user.name }}</h3>
|
||||
<el-tag :type="user.websites.some(w => w.loginStatus === 'success') ? 'success' : user.websites.some(w => w.loginStatus === 'failed') ? 'danger' : 'info'">
|
||||
{{ user.websites.filter(w => w.loginStatus === 'success').length }}/{{ user.websites.length }} 在线
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<el-table :data="user.websites" style="width: 100%; margin-bottom: 20px">
|
||||
<el-table-column prop="name" label="网站名称" width="150"></el-table-column>
|
||||
<el-table-column prop="url" label="URL" width="250">
|
||||
<template #default="scope">
|
||||
<el-link :href="scope.row.url" target="_blank" type="primary">{{ scope.row.url }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="网络状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.networkStatus === 'online' ? 'success' : scope.row.networkStatus === 'offline' ? 'danger' : 'info'">
|
||||
{{ scope.row.networkStatus === 'online' ? '在线' : scope.row.networkStatus === 'offline' ? '离线' : '未知' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="登录状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.loginStatus === 'success' ? 'success' : scope.row.loginStatus === 'failed' ? 'danger' : scope.row.loginStatus === 'checking' ? 'warning' : 'info'">
|
||||
{{ scope.row.loginStatus === 'success' ? '已登录' : scope.row.loginStatus === 'failed' ? '未登录' : scope.row.loginStatus === 'checking' ? '检测中' : '未检测' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="lastCheckTime" label="最后检测" width="180"></el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" @click="checkLogin(scope.row)" :loading="scope.row.loginStatus === 'checking'" :disabled="scope.row.loginStatus === 'checking'">
|
||||
{{ scope.row.loginStatus === 'checking' ? '检测中' : '检测登录' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="success" @click="manageWebsite(scope.row)" v-if="scope.row.loginStatus === 'success'">管理</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="dashboardData.length === 0" description="暂无网站数据"></el-empty>
|
||||
</div>
|
||||
|
||||
<!-- 网站管理 -->
|
||||
<div v-if="activeTab === 'website_manage'">
|
||||
<div class="website-manage-header">
|
||||
<el-button @click="activeTab = 'dashboard'" type="info">返回监控首页</el-button>
|
||||
<h2>网站管理 - {{ currentWebsite?.name }}</h2>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="manageTab">
|
||||
<el-tab-pane label="Cookie管理" name="cookies">
|
||||
<el-button @click="showAddCookieDialog" type="primary" size="small">添加Cookie</el-button>
|
||||
<el-button @click="loadWebsiteCookies" size="small">刷新</el-button>
|
||||
<el-table :data="websiteCookies" style="width: 100%; margin-top: 10px">
|
||||
<el-table-column prop="name" label="名称" width="200"></el-table-column>
|
||||
<el-table-column prop="value" label="值" width="300">
|
||||
<template #default="scope">
|
||||
<span class="cookie-value">{{ maskCookieValue(scope.row.value) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="domain" label="域名" width="150"></el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="editCookie(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteCookie(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="网站设置" name="settings">
|
||||
<el-form :model="websiteForm" label-width="120px" style="max-width: 500px">
|
||||
<el-form-item label="网站名称">
|
||||
<el-input v-model="websiteForm.name"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="URL">
|
||||
<el-input v-model="websiteForm.url"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="登录检测选择器">
|
||||
<el-input v-model="websiteForm.login_check_selector"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="登录成功文本">
|
||||
<el-input v-model="websiteForm.success_text"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="检测间隔(分钟)">
|
||||
<el-input-number v-model="websiteForm.interval_minutes" :min="1" :max="1440"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveWebsiteSettings">保存设置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="检测记录" name="history">
|
||||
<el-button @click="loadWebsiteDetections" type="primary" size="small">刷新</el-button>
|
||||
<el-table :data="websiteDetections" style="width: 100%; margin-top: 10px">
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="message" label="消息"></el-table-column>
|
||||
<el-table-column prop="created_at" label="时间" width="180"></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 系统设置页面 -->
|
||||
<div v-if="activeTab === 'settings'">
|
||||
<h2>系统设置</h2>
|
||||
<el-tabs v-model="settingsTab">
|
||||
<el-tab-pane label="用户管理" name="users">
|
||||
<el-button @click="showAddUserDialog" type="primary" size="small" style="margin-bottom: 10px">添加用户</el-button>
|
||||
<el-table :data="users" style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="name" label="用户名" width="150"></el-table-column>
|
||||
<el-table-column prop="iyuu_token" label="爱语飞飞令牌" width="200"></el-table-column>
|
||||
<el-table-column prop="max_fail_count" label="最大失败次数" width="120"></el-table-column>
|
||||
<el-table-column prop="website_count" label="网站数量" width="100"></el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="editUser(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="warning" @click="manageUserWebsites(scope.row)">管理网站</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteUser(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="网站管理" name="websites">
|
||||
<div class="filter-form">
|
||||
<el-select v-model="filterWebsiteUser" placeholder="选择用户" @change="loadWebsites">
|
||||
<el-option label="全部用户" :value="null"></el-option>
|
||||
<el-option v-for="user in users" :key="user.id" :label="user.name" :value="user.id"></el-option>
|
||||
</el-select>
|
||||
<el-button @click="showAddWebsiteDialog" type="primary" size="small" style="margin-left: 10px">添加网站</el-button>
|
||||
</div>
|
||||
<el-table :data="filteredWebsites" style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="user_id" label="用户ID" width="100"></el-table-column>
|
||||
<el-table-column prop="name" label="网站名称" width="150"></el-table-column>
|
||||
<el-table-column prop="url" label="URL" width="250"></el-table-column>
|
||||
<el-table-column prop="interval_minutes" label="间隔(分钟)" width="100"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="editWebsite(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteWebsite(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="检测记录" name="detections">
|
||||
<el-button @click="loadDetections" type="primary" size="small" style="margin-bottom: 10px">刷新</el-button>
|
||||
<el-table :data="detections" style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="website_id" label="网站ID" width="100"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="response_time" label="响应时间(ms)" width="120"></el-table-column>
|
||||
<el-table-column prop="http_status" label="HTTP状态" width="100"></el-table-column>
|
||||
<el-table-column prop="message" label="消息"></el-table-column>
|
||||
<el-table-column prop="created_at" label="检测时间" width="180"></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="通知记录" name="notifications">
|
||||
<el-button @click="loadNotifications" type="primary" size="small" style="margin-bottom: 10px">刷新</el-button>
|
||||
<el-table :data="notifications" style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="user_id" label="用户ID" width="100"></el-table-column>
|
||||
<el-table-column prop="website_id" label="网站ID" width="100"></el-table-column>
|
||||
<el-table-column prop="title" label="标题" width="200"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 'sent' ? 'success' : scope.row.status === 'failed' ? 'danger' : 'warning'">
|
||||
{{ scope.row.status === 'sent' ? '已发送' : scope.row.status === 'failed' ? '失败' : '待发送' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="send_time" label="发送时间" width="180"></el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="180"></el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="viewNotificationDetail(scope.row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 用户网站管理 -->
|
||||
<div v-if="activeTab === 'user_websites'">
|
||||
<div class="website-manage-header">
|
||||
<el-button @click="activeTab = 'settings'; settingsTab = 'users'" type="info">返回设置</el-button>
|
||||
<h2>网站管理 - {{ currentUser?.name }}</h2>
|
||||
<el-button @click="showAddWebsiteDialogForUser" type="primary">添加网站</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="currentUserWebsites" style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="name" label="网站名称" width="150"></el-table-column>
|
||||
<el-table-column prop="url" label="URL" width="250"></el-table-column>
|
||||
<el-table-column prop="interval_minutes" label="间隔(分钟)" width="100"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="editWebsite(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteWebsite(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
|
||||
<!-- 后台登录对话框 -->
|
||||
<el-dialog v-model="adminLoginVisible" title="登录后台" width="400px">
|
||||
<el-form :model="adminLoginForm" label-width="80px">
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="adminLoginForm.password" type="password" show-password @keyup.enter="adminLogin"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="adminLoginVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="adminLogin">登录</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 用户对话框 -->
|
||||
<el-dialog v-model="userDialogVisible" :title="userDialogTitle" width="600px">
|
||||
<el-form :model="userForm" label-width="120px">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="userForm.name"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="爱语飞飞令牌">
|
||||
<el-input v-model="userForm.iyuu_token"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="最大失败次数">
|
||||
<el-input-number v-model="userForm.max_fail_count" :min="1" :max="10"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-divider content-position="left">Cookie Cloud 设置</el-divider>
|
||||
<el-form-item label="UUID">
|
||||
<el-input v-model="userForm.cookie_cloud_uuid" placeholder="hu6n2vcUmzpu7mqUN2rVCg"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="userForm.cookie_cloud_password" show-password></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="API地址">
|
||||
<el-input v-model="userForm.cookie_cloud_api_url" placeholder="https://movie-pilot.org/cookiecloud"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="userDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveUser">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 网站对话框 -->
|
||||
<el-dialog v-model="websiteDialogVisible" :title="websiteDialogTitle">
|
||||
<el-form :model="websiteFormDialog" label-width="120px">
|
||||
<el-form-item label="所属用户">
|
||||
<el-select v-model="websiteFormDialog.user_id" placeholder="选择用户">
|
||||
<el-option v-for="user in users" :key="user.id" :label="user.name" :value="user.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="网站名称">
|
||||
<el-input v-model="websiteFormDialog.name"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="URL">
|
||||
<el-input v-model="websiteFormDialog.url"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="登录检测选择器">
|
||||
<el-input v-model="websiteFormDialog.login_check_selector"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="登录成功文本">
|
||||
<el-input v-model="websiteFormDialog.success_text"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="检测间隔(分钟)">
|
||||
<el-input-number v-model="websiteFormDialog.interval_minutes" :min="1" :max="1440"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-switch v-model="websiteFormDialog.status" :active-value="1" :inactive-value="0"></el-switch>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="websiteDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveWebsite">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Cookie对话框 -->
|
||||
<el-dialog v-model="cookieDialogVisible" :title="cookieDialogTitle">
|
||||
<el-form :model="cookieForm" label-width="100px">
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="cookieForm.name"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="值">
|
||||
<el-input v-model="cookieForm.value" type="textarea" :rows="3"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="域名">
|
||||
<el-input v-model="cookieForm.domain"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="路径">
|
||||
<el-input v-model="cookieForm.path"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="cookieDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveCookie">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
</html>{% endraw %}
|
||||
@@ -1,232 +0,0 @@
|
||||
"""浏览器管理模块"""
|
||||
import time
|
||||
from DrissionPage import ChromiumPage, ChromiumOptions
|
||||
from logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class BrowserManager:
|
||||
"""浏览器管理器"""
|
||||
|
||||
def __init__(self, browser_type: str = 'edge', headless: bool = False):
|
||||
"""
|
||||
初始化浏览器管理器
|
||||
|
||||
Args:
|
||||
browser_type: 浏览器类型 (edge 或 chrome)
|
||||
headless: 是否无头模式
|
||||
"""
|
||||
self.browser_type = browser_type
|
||||
self.headless = headless
|
||||
self.page = None
|
||||
|
||||
def init_browser(self):
|
||||
"""初始化浏览器"""
|
||||
co = ChromiumOptions()
|
||||
if self.browser_type == 'edge':
|
||||
co.set_browser_path(r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe")
|
||||
co.set_argument('--no-sandbox')
|
||||
co.set_argument('--disable-dev-shm-usage')
|
||||
if self.headless:
|
||||
co.set_argument('--headless')
|
||||
self.page = ChromiumPage(addr_or_opts=co)
|
||||
logger.info("浏览器初始化成功")
|
||||
|
||||
def wait_page_loaded(self, timeout=30):
|
||||
"""等待页面加载完成"""
|
||||
if self.page:
|
||||
self.page.wait.doc_loaded(timeout=timeout)
|
||||
self.page.ele('tag:body', timeout=timeout)
|
||||
|
||||
def ensure_connection(self):
|
||||
"""确保浏览器连接正常"""
|
||||
try:
|
||||
if self.page:
|
||||
self.page.get("about:blank")
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.init_browser()
|
||||
self.page.get("about:blank")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"无法建立浏览器连接: {e}")
|
||||
return False
|
||||
|
||||
def import_cookies(self, cookies: list, domain: str):
|
||||
"""
|
||||
导入cookies到浏览器
|
||||
|
||||
Args:
|
||||
cookies: cookie列表
|
||||
domain: 域名
|
||||
|
||||
Returns:
|
||||
成功导入的cookie数量
|
||||
"""
|
||||
if not self.ensure_connection():
|
||||
return 0
|
||||
|
||||
if not cookies:
|
||||
return 0
|
||||
|
||||
success_count = 0
|
||||
# 直接使用DrissionPage的set.cookies方法批量导入
|
||||
# 该方法内部已经优化了导入逻辑
|
||||
for cookie in cookies:
|
||||
try:
|
||||
if not cookie.get('name') or not cookie.get('value'):
|
||||
continue
|
||||
|
||||
cookie_dict = {
|
||||
'name': cookie.get('name'),
|
||||
'value': cookie.get('value'),
|
||||
'domain': cookie.get('domain', domain),
|
||||
'path': cookie.get('path', '/')
|
||||
}
|
||||
|
||||
self.page.set.cookies(cookie_dict)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
# 忽略导入失败的cookie,继续处理其他cookie
|
||||
pass
|
||||
|
||||
return success_count
|
||||
|
||||
def import_local_storage(self, local_storage: dict):
|
||||
"""
|
||||
导入localStorage到浏览器
|
||||
|
||||
Args:
|
||||
local_storage: localStorage字典
|
||||
|
||||
Returns:
|
||||
成功导入的项目数量
|
||||
"""
|
||||
if not self.ensure_connection():
|
||||
return 0
|
||||
|
||||
ls_count = 0
|
||||
for domain, items in local_storage.items():
|
||||
try:
|
||||
for key, value in items.items():
|
||||
try:
|
||||
self.page.set.local_storage(key, value)
|
||||
ls_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"导入localStorage失败: {key} - {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"导入localStorage域失败: {domain} - {e}")
|
||||
|
||||
logger.info(f"成功导入 {ls_count} 个 localStorage 项目")
|
||||
return ls_count
|
||||
|
||||
def verify_login(self, url: str, check_selector: str = "", success_text: str = "") -> bool:
|
||||
"""
|
||||
验证登录状态
|
||||
|
||||
Args:
|
||||
url: 网站地址
|
||||
check_selector: 登录检测选择器
|
||||
success_text: 登录成功时显示的文本
|
||||
|
||||
Returns:
|
||||
是否已登录
|
||||
"""
|
||||
if not self.ensure_connection():
|
||||
return False
|
||||
|
||||
try:
|
||||
self.page.get(url)
|
||||
self.wait_page_loaded()
|
||||
time.sleep(3) # 等待网页加载完毕并暂停3秒
|
||||
|
||||
for i in range(3):
|
||||
time.sleep(3) # 每次刷新前暂停3秒
|
||||
self.page.refresh()
|
||||
self.wait_page_loaded()
|
||||
time.sleep(3) # 每次刷新后暂停3秒
|
||||
|
||||
if not check_selector and not success_text:
|
||||
return False
|
||||
|
||||
if check_selector:
|
||||
try:
|
||||
element = self.page.ele(check_selector, timeout=10)
|
||||
if element:
|
||||
element_text = element.text or ""
|
||||
if not success_text:
|
||||
return True
|
||||
if success_text in element_text:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
if success_text:
|
||||
try:
|
||||
page_text = self.page.html or ""
|
||||
if success_text in page_text:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"验证登录状态失败: {e}")
|
||||
return False
|
||||
|
||||
def get_cookies(self) -> list:
|
||||
"""
|
||||
获取当前浏览器的所有cookies
|
||||
|
||||
Returns:
|
||||
cookie列表
|
||||
"""
|
||||
if not self.page:
|
||||
return []
|
||||
|
||||
try:
|
||||
return self.page.cookies()
|
||||
except Exception as e:
|
||||
logger.error(f"获取cookies失败: {e}")
|
||||
return []
|
||||
|
||||
def get_local_storage(self) -> dict:
|
||||
"""
|
||||
获取当前浏览器的localStorage
|
||||
|
||||
Returns:
|
||||
localStorage字典
|
||||
"""
|
||||
if not self.page:
|
||||
return {}
|
||||
|
||||
try:
|
||||
import json
|
||||
js_code = """
|
||||
var result = {};
|
||||
for (var i = 0; i < localStorage.length; i++) {
|
||||
var key = localStorage.key(i);
|
||||
result[key] = localStorage.getItem(key);
|
||||
}
|
||||
JSON.stringify(result);
|
||||
"""
|
||||
result = self.page.run_js(js_code)
|
||||
return json.loads(result) if result else {}
|
||||
except Exception as e:
|
||||
logger.error(f"获取localStorage失败: {e}")
|
||||
return {}
|
||||
|
||||
def close(self):
|
||||
"""关闭浏览器"""
|
||||
if self.page:
|
||||
try:
|
||||
self.page.quit()
|
||||
except Exception as e:
|
||||
logger.error(f"关闭浏览器失败: {e}")
|
||||
finally:
|
||||
self.page = None
|
||||
@@ -1,292 +0,0 @@
|
||||
"""配置管理模块"""
|
||||
import json
|
||||
import os
|
||||
from logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
DEFAULT_CONFIG_FILE = "config.json"
|
||||
DEFAULT_COOKIE_DATA_FILE = "cookie_data.json"
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""配置管理器"""
|
||||
|
||||
def __init__(self, config_file: str = DEFAULT_CONFIG_FILE):
|
||||
"""
|
||||
初始化配置管理器
|
||||
|
||||
Args:
|
||||
config_file: 配置文件路径
|
||||
"""
|
||||
self.config_file = config_file
|
||||
self.config = self._load_config()
|
||||
|
||||
def _load_config(self) -> dict:
|
||||
"""加载配置文件"""
|
||||
try:
|
||||
if os.path.exists(self.config_file):
|
||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
logger.info(f"配置文件加载成功: {self.config_file}")
|
||||
return config
|
||||
else:
|
||||
logger.warning(f"配置文件不存在: {self.config_file}")
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"加载配置文件失败: {e}")
|
||||
return {}
|
||||
|
||||
def save_config(self, config: dict) -> bool:
|
||||
"""
|
||||
保存配置文件
|
||||
|
||||
Args:
|
||||
config: 配置字典
|
||||
|
||||
Returns:
|
||||
是否保存成功
|
||||
"""
|
||||
try:
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||
logger.info(f"配置文件保存成功: {self.config_file}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"保存配置文件失败: {e}")
|
||||
return False
|
||||
|
||||
def get(self, key: str, default=None):
|
||||
"""
|
||||
获取配置项
|
||||
|
||||
Args:
|
||||
key: 配置键
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
配置值
|
||||
"""
|
||||
return self.config.get(key, default)
|
||||
|
||||
def set(self, key: str, value):
|
||||
"""
|
||||
设置配置项
|
||||
|
||||
Args:
|
||||
key: 配置键
|
||||
value: 配置值
|
||||
"""
|
||||
self.config[key] = value
|
||||
self.save_config(self.config)
|
||||
|
||||
def get_users(self) -> list:
|
||||
"""
|
||||
获取所有用户配置
|
||||
|
||||
Returns:
|
||||
用户配置列表
|
||||
"""
|
||||
return self.config.get('users', [])
|
||||
|
||||
def get_user_config(self, user_name: str) -> dict:
|
||||
"""
|
||||
获取指定用户的配置
|
||||
|
||||
Args:
|
||||
user_name: 用户名
|
||||
|
||||
Returns:
|
||||
用户配置字典
|
||||
"""
|
||||
users = self.get_users()
|
||||
for user in users:
|
||||
if user.get('name') == user_name:
|
||||
return user
|
||||
return {}
|
||||
|
||||
def get_cookie_cloud_config(self, user_name: str) -> dict:
|
||||
"""
|
||||
获取用户的Cookie Cloud配置
|
||||
|
||||
Args:
|
||||
user_name: 用户名
|
||||
|
||||
Returns:
|
||||
Cookie Cloud配置字典
|
||||
"""
|
||||
user_config = self.get_user_config(user_name)
|
||||
return user_config.get('cookie_cloud', {})
|
||||
|
||||
def get_notification_config(self, user_name: str) -> dict:
|
||||
"""
|
||||
获取用户的通知配置
|
||||
|
||||
Args:
|
||||
user_name: 用户名
|
||||
|
||||
Returns:
|
||||
通知配置字典
|
||||
"""
|
||||
user_config = self.get_user_config(user_name)
|
||||
return user_config.get('notification', {})
|
||||
|
||||
def get_browser_config(self, user_name: str) -> dict:
|
||||
"""
|
||||
获取用户的浏览器配置
|
||||
|
||||
Args:
|
||||
user_name: 用户名
|
||||
|
||||
Returns:
|
||||
浏览器配置字典
|
||||
"""
|
||||
user_config = self.get_user_config(user_name)
|
||||
return user_config.get('browser', {})
|
||||
|
||||
def get_websites(self, user_name: str) -> list:
|
||||
"""
|
||||
获取用户的网站配置
|
||||
|
||||
Args:
|
||||
user_name: 用户名
|
||||
|
||||
Returns:
|
||||
网站配置列表
|
||||
"""
|
||||
user_config = self.get_user_config(user_name)
|
||||
return user_config.get('websites', [])
|
||||
|
||||
|
||||
class CookieDataManager:
|
||||
"""Cookie数据管理器"""
|
||||
|
||||
def __init__(self, data_file: str = DEFAULT_COOKIE_DATA_FILE):
|
||||
"""
|
||||
初始化Cookie数据管理器
|
||||
|
||||
Args:
|
||||
data_file: Cookie数据文件路径
|
||||
"""
|
||||
self.data_file = data_file
|
||||
self.data = self._load_data()
|
||||
|
||||
def _load_data(self) -> dict:
|
||||
"""加载Cookie数据文件"""
|
||||
try:
|
||||
if os.path.exists(self.data_file):
|
||||
with open(self.data_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
logger.info(f"Cookie数据文件加载成功: {self.data_file}")
|
||||
return data
|
||||
else:
|
||||
logger.warning(f"Cookie数据文件不存在: {self.data_file}")
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"加载Cookie数据文件失败: {e}")
|
||||
return {}
|
||||
|
||||
def save_data(self, data: dict) -> bool:
|
||||
"""
|
||||
保存Cookie数据文件
|
||||
|
||||
Args:
|
||||
data: Cookie数据字典
|
||||
|
||||
Returns:
|
||||
是否保存成功
|
||||
"""
|
||||
try:
|
||||
with open(self.data_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
logger.info(f"Cookie数据文件保存成功: {self.data_file}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"保存Cookie数据文件失败: {e}")
|
||||
return False
|
||||
|
||||
def get_user_cookies(self, user_name: str) -> dict:
|
||||
"""
|
||||
获取用户的Cookie数据
|
||||
|
||||
Args:
|
||||
user_name: 用户名
|
||||
|
||||
Returns:
|
||||
用户Cookie数据字典
|
||||
"""
|
||||
return self.data.get(user_name, {})
|
||||
|
||||
def save_user_cookies(self, user_name: str, cookies: list, url: str, website_name: str = ""):
|
||||
"""
|
||||
保存用户的Cookie数据
|
||||
|
||||
Args:
|
||||
user_name: 用户名
|
||||
cookies: Cookie列表
|
||||
url: 网站URL
|
||||
website_name: 网站名称
|
||||
"""
|
||||
if user_name not in self.data:
|
||||
self.data[user_name] = {}
|
||||
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(url)
|
||||
domain = parsed.netloc
|
||||
|
||||
cookie_entry = {
|
||||
"cookies": cookies,
|
||||
"url": url,
|
||||
"website_name": website_name
|
||||
}
|
||||
|
||||
self.data[user_name][domain] = cookie_entry
|
||||
self.save_data(self.data)
|
||||
logger.info(f"已保存 {len(cookies)} 个 cookie ({user_name} -> {domain})")
|
||||
|
||||
def get_domain_cookies(self, user_name: str, domain: str) -> dict:
|
||||
"""
|
||||
获取指定域名的Cookie数据
|
||||
|
||||
Args:
|
||||
user_name: 用户名
|
||||
domain: 域名
|
||||
|
||||
Returns:
|
||||
域名Cookie数据字典
|
||||
"""
|
||||
user_cookies = self.get_user_cookies(user_name)
|
||||
|
||||
for cookie_domain, cookie_data in user_cookies.items():
|
||||
clean_domain = cookie_domain.lstrip('.')
|
||||
if domain in clean_domain or clean_domain in domain:
|
||||
return cookie_data
|
||||
|
||||
return {}
|
||||
|
||||
def save_simple_cookies(self, cookies: list, url: str):
|
||||
"""
|
||||
保存简单的Cookie数据(用于Cookie提取器)
|
||||
|
||||
Args:
|
||||
cookies: Cookie列表
|
||||
url: 网站URL
|
||||
"""
|
||||
data = {
|
||||
"cookies": cookies,
|
||||
"url": url
|
||||
}
|
||||
self.save_data(data)
|
||||
logger.info(f"已保存 {len(cookies)} 个 cookie")
|
||||
|
||||
def load_simple_cookies(self) -> dict:
|
||||
"""
|
||||
加载简单的Cookie数据(用于Cookie提取器)
|
||||
|
||||
Returns:
|
||||
Cookie数据字典
|
||||
"""
|
||||
return {
|
||||
"cookies": self.data.get("cookies", []),
|
||||
"url": self.data.get("url", "")
|
||||
}
|
||||
-255
@@ -1,255 +0,0 @@
|
||||
"""Cookie Cloud 模块 - 从 Cookie Cloud 服务器获取并处理 cookie 数据"""
|
||||
import json
|
||||
import hashlib
|
||||
import base64
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from Crypto.Cipher import AES
|
||||
from dataclasses import dataclass
|
||||
from logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CookieCloudConfig:
|
||||
"""CookieCloud 配置"""
|
||||
api_url: str
|
||||
uuid: str
|
||||
password: str
|
||||
timeout: int = 30
|
||||
|
||||
|
||||
class CookieCloud:
|
||||
"""Cookie Cloud 客户端"""
|
||||
|
||||
def __init__(self, api_url: str, uuid: str, password: str):
|
||||
"""
|
||||
初始化 Cookie Cloud 客户端
|
||||
|
||||
Args:
|
||||
api_url: Cookie Cloud API 地址
|
||||
uuid: 用户 UUID
|
||||
password: 用户密码
|
||||
"""
|
||||
self.config = CookieCloudConfig(
|
||||
api_url=api_url.rstrip('/'),
|
||||
uuid=uuid,
|
||||
password=password,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
def _get_crypt_key(self) -> bytes:
|
||||
"""生成加密密钥"""
|
||||
combined_string = f"{self.config.uuid}-{self.config.password}"
|
||||
return hashlib.md5(combined_string.encode('utf-8')).hexdigest()[:16].encode("utf-8")
|
||||
|
||||
def _bytes_to_key(self, data: bytes, salt: bytes, output: int = 48) -> bytes:
|
||||
"""OpenSSL EVP_BytesToKey 密钥派生算法"""
|
||||
assert len(salt) == 8, len(salt)
|
||||
data += salt
|
||||
key = hashlib.md5(data).digest()
|
||||
final_key = key
|
||||
while len(final_key) < output:
|
||||
key = hashlib.md5(key + data).digest()
|
||||
final_key += key
|
||||
return final_key[:output]
|
||||
|
||||
def _decrypt(self, encrypted: str, passphrase: bytes) -> bytes:
|
||||
"""解密数据"""
|
||||
encrypted_bytes = base64.b64decode(encrypted)
|
||||
assert encrypted_bytes.startswith(b"Salted__"), "Invalid encrypted data format"
|
||||
salt = encrypted_bytes[8:16]
|
||||
key_iv = self._bytes_to_key(passphrase, salt, 32 + 16)
|
||||
key = key_iv[:32]
|
||||
iv = key_iv[32:]
|
||||
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||
decrypted_padded = aes.decrypt(encrypted_bytes[16:])
|
||||
padding_length = decrypted_padded[-1]
|
||||
if isinstance(padding_length, str):
|
||||
padding_length = ord(padding_length)
|
||||
return decrypted_padded[:-padding_length]
|
||||
|
||||
def _download_all(self) -> Tuple[Optional[Dict], str]:
|
||||
"""下载所有cookie和local storage数据"""
|
||||
try:
|
||||
url = f"{self.config.api_url}/get/{self.config.uuid}"
|
||||
request = urllib.request.Request(
|
||||
url,
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'CookieCloud-Client/1.0'
|
||||
},
|
||||
method='GET'
|
||||
)
|
||||
|
||||
response = urllib.request.urlopen(request, timeout=self.config.timeout)
|
||||
|
||||
if response.status != 200:
|
||||
return None, f"服务器返回错误状态码: {response.status}"
|
||||
|
||||
result = json.loads(response.read().decode('utf-8'))
|
||||
|
||||
if not result:
|
||||
return None, "服务器返回数据为空"
|
||||
|
||||
encrypted = result.get("encrypted")
|
||||
if not encrypted:
|
||||
return None, "未获取到cookie密文"
|
||||
|
||||
crypt_key = self._get_crypt_key()
|
||||
try:
|
||||
decrypted_data = self._decrypt(encrypted, crypt_key)
|
||||
result = json.loads(decrypted_data.decode("utf-8"))
|
||||
except Exception as e:
|
||||
return None, f"cookie解密失败: {str(e)}"
|
||||
|
||||
if not result:
|
||||
return None, "cookie解密为空"
|
||||
|
||||
return result, ""
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 401:
|
||||
return None, "认证失败,请检查用户名和密码"
|
||||
else:
|
||||
return None, f"HTTP错误: {e.code} {e.reason}"
|
||||
except urllib.error.URLError as e:
|
||||
return None, f"网络连接失败: {e.reason}"
|
||||
except Exception as e:
|
||||
return None, f"下载失败: {str(e)}"
|
||||
|
||||
def get_cookies(self) -> Dict[str, List[Dict]]:
|
||||
"""
|
||||
从 Cookie Cloud 服务器获取 cookie 数据
|
||||
|
||||
Returns:
|
||||
按域名分组的 cookie 数据字典
|
||||
格式: {"domain.com": [{"name": "cookie1", "value": "val1", ...}, ...], ...}
|
||||
"""
|
||||
data, error = self._download_all()
|
||||
|
||||
if error:
|
||||
logger.error(error)
|
||||
return {}
|
||||
|
||||
# 处理数据结构
|
||||
cookie_data = {}
|
||||
|
||||
# 兼容直接按域名分组的格式
|
||||
if isinstance(data, dict) and not data.get('cookie_data'):
|
||||
cookie_data = data
|
||||
# 兼容包含 cookie_data 的格式
|
||||
elif isinstance(data, dict) and data.get('cookie_data'):
|
||||
cookie_data = data.get('cookie_data', {})
|
||||
|
||||
# 处理 sameSite 字段
|
||||
processed_cookies = {}
|
||||
for domain, cookies in cookie_data.items():
|
||||
if not cookies:
|
||||
continue
|
||||
processed_cookies[domain] = []
|
||||
for cookie in cookies:
|
||||
if cookie.get('sameSite') == 'unspecified':
|
||||
cookie['sameSite'] = 'Lax'
|
||||
processed_cookies[domain].append(cookie)
|
||||
|
||||
logger.info(f"获取到 {len(processed_cookies)} 个域名的 cookies")
|
||||
return processed_cookies
|
||||
|
||||
def get_local_storage(self) -> Dict[str, Dict]:
|
||||
"""
|
||||
从 Cookie Cloud 服务器获取 local storage 数据
|
||||
|
||||
Returns:
|
||||
按域名分组的 local storage 数据字典
|
||||
格式: {"domain.com": {"key1": "value1", ...}, ...}
|
||||
"""
|
||||
data, error = self._download_all()
|
||||
|
||||
if error:
|
||||
logger.error(error)
|
||||
return {}
|
||||
|
||||
# 处理数据结构
|
||||
local_storage_data = {}
|
||||
|
||||
if isinstance(data, dict) and data.get('local_storage_data'):
|
||||
local_storage_data = data.get('local_storage_data', {})
|
||||
|
||||
logger.info(f"获取到 {len(local_storage_data)} 个域名的 local storage")
|
||||
return local_storage_data
|
||||
|
||||
def get_cookies_for_domain(self, domain: str) -> List[Dict]:
|
||||
"""
|
||||
获取指定域名的 cookie
|
||||
|
||||
Args:
|
||||
domain: 域名
|
||||
|
||||
Returns:
|
||||
cookie 列表
|
||||
"""
|
||||
all_cookies = self.get_cookies()
|
||||
|
||||
matched_cookies = []
|
||||
for cookie_domain, cookies in all_cookies.items():
|
||||
clean_domain = cookie_domain.lstrip('.')
|
||||
if domain in clean_domain or clean_domain in domain:
|
||||
matched_cookies.extend(cookies)
|
||||
|
||||
logger.info(f"获取到 {len(matched_cookies)} 个 {domain} 的 cookies")
|
||||
return matched_cookies
|
||||
|
||||
def get_local_storage_for_domain(self, domain: str) -> Dict:
|
||||
"""
|
||||
获取指定域名的 local storage
|
||||
|
||||
Args:
|
||||
domain: 域名
|
||||
|
||||
Returns:
|
||||
local storage 字典
|
||||
"""
|
||||
all_local_storage = self.get_local_storage()
|
||||
|
||||
for ls_domain, data in all_local_storage.items():
|
||||
clean_domain = ls_domain.lstrip('.')
|
||||
if domain in clean_domain or clean_domain in domain:
|
||||
logger.info(f"获取到 {domain} 的 local storage")
|
||||
return data
|
||||
|
||||
return {}
|
||||
|
||||
def get_all_data(self) -> Dict:
|
||||
"""
|
||||
获取所有数据(包括 cookies 和 local storage)
|
||||
|
||||
Returns:
|
||||
包含所有数据的字典
|
||||
"""
|
||||
data, error = self._download_all()
|
||||
|
||||
if error:
|
||||
logger.error(error)
|
||||
return {}
|
||||
|
||||
result = {
|
||||
'cookies': {},
|
||||
'local_storage': {}
|
||||
}
|
||||
|
||||
# 处理 cookies
|
||||
if isinstance(data, dict):
|
||||
if not data.get('cookie_data'):
|
||||
result['cookies'] = data
|
||||
elif data.get('cookie_data'):
|
||||
result['cookies'] = data.get('cookie_data', {})
|
||||
|
||||
# 处理 local storage
|
||||
if data.get('local_storage_data'):
|
||||
result['local_storage'] = data.get('local_storage_data', {})
|
||||
|
||||
logger.info(f"获取到所有数据: {len(result['cookies'])} 个域名的 cookies, {len(result['local_storage'])} 个域名的 local storage")
|
||||
return result
|
||||
@@ -1,223 +0,0 @@
|
||||
"""Cookie提取器 - 使用DrissionPage库"""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
from browser_manager import BrowserManager
|
||||
from config_manager import CookieDataManager
|
||||
from logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class CookieExtractorApp:
|
||||
"""Cookie提取器应用"""
|
||||
|
||||
def __init__(self, root):
|
||||
"""
|
||||
初始化应用
|
||||
|
||||
Args:
|
||||
root: 根窗口
|
||||
"""
|
||||
self.root = root
|
||||
self.root.title("Cookie提取器")
|
||||
self.root.geometry("600x250")
|
||||
self.root.resizable(True, True)
|
||||
|
||||
self.browser_manager = BrowserManager()
|
||||
self.cookie_data_manager = CookieDataManager()
|
||||
|
||||
self._create_widgets()
|
||||
|
||||
def _create_widgets(self):
|
||||
"""创建界面控件"""
|
||||
main_frame = ttk.Frame(self.root, padding="20")
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
url_frame = ttk.LabelFrame(main_frame, text="网址输入", padding="10")
|
||||
url_frame.pack(fill=tk.X, pady=10)
|
||||
|
||||
ttk.Label(url_frame, text="网址:").pack(side=tk.LEFT, padx=5)
|
||||
self.url_var = tk.StringVar(value="http://192.168.8.147:5666/login")
|
||||
self.url_entry = ttk.Entry(url_frame, textvariable=self.url_var, width=50)
|
||||
self.url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
|
||||
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.pack(fill=tk.X, pady=10)
|
||||
|
||||
self.open_button = ttk.Button(button_frame, text="打开", command=self.open_website, width=15)
|
||||
self.open_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.save_button = ttk.Button(button_frame, text="保存", command=self.save_cookies, width=15, state=tk.DISABLED)
|
||||
self.save_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.import_button = ttk.Button(button_frame, text="导入", command=self.import_cookies, width=15)
|
||||
self.import_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.close_button = ttk.Button(button_frame, text="关闭", command=self.close_app, width=15)
|
||||
self.close_button.pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
self.status_var = tk.StringVar(value="就绪")
|
||||
status_frame = ttk.Frame(main_frame)
|
||||
status_frame.pack(fill=tk.X, pady=10)
|
||||
|
||||
ttk.Label(status_frame, text="状态:").pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(status_frame, textvariable=self.status_var, foreground="blue").pack(side=tk.LEFT, padx=5)
|
||||
|
||||
def open_website(self):
|
||||
"""打开指定网站"""
|
||||
url = self.url_var.get().strip()
|
||||
|
||||
if not url:
|
||||
messagebox.showerror("错误", "请输入网址")
|
||||
return
|
||||
|
||||
if not (url.startswith("http://") or url.startswith("https://")):
|
||||
url = "https://" + url
|
||||
self.url_var.set(url)
|
||||
|
||||
try:
|
||||
self._cleanup_browser()
|
||||
|
||||
self.status_var.set("正在打开浏览器...")
|
||||
self.root.update()
|
||||
|
||||
self.browser_manager.init_browser()
|
||||
|
||||
self.status_var.set(f"正在访问: {url}")
|
||||
self.root.update()
|
||||
|
||||
self.browser_manager.page.get(url)
|
||||
self.browser_manager.wait_page_loaded()
|
||||
|
||||
self.status_var.set(f"已打开: {url}")
|
||||
self.save_button.config(state=tk.NORMAL)
|
||||
messagebox.showinfo("成功", f"已成功打开网址: {url}\n\n请在浏览器中完成登录操作后,点击'保存'按钮")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"打开网站失败: {e}")
|
||||
messagebox.showerror("错误", f"打开网站失败: {str(e)}")
|
||||
self.status_var.set("就绪")
|
||||
self._cleanup_browser()
|
||||
|
||||
def save_cookies(self):
|
||||
"""保存当前的cookie信息,并强制关闭浏览器"""
|
||||
if not self.browser_manager.page:
|
||||
messagebox.showerror("错误", "请先打开网站")
|
||||
return
|
||||
|
||||
try:
|
||||
self.status_var.set("正在提取cookies...")
|
||||
self.root.update()
|
||||
|
||||
cookies = self.browser_manager.get_cookies()
|
||||
logger.info(f"成功提取 {len(cookies)} 个 cookies")
|
||||
|
||||
self.cookie_data_manager.save_simple_cookies(cookies, self.url_var.get())
|
||||
|
||||
self.status_var.set("正在关闭浏览器...")
|
||||
self.root.update()
|
||||
|
||||
self._cleanup_browser()
|
||||
|
||||
self.status_var.set(f"Cookies已保存到 cookie_data.json")
|
||||
messagebox.showinfo('成功', 'Cookies保存成功,浏览器已关闭')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存cookies失败: {e}")
|
||||
messagebox.showerror("错误", f"保存cookies失败: {str(e)}")
|
||||
self.status_var.set("就绪")
|
||||
self._cleanup_browser()
|
||||
|
||||
def import_cookies(self):
|
||||
"""打开浏览器,导入cookie信息,然后打开指定网站"""
|
||||
import time
|
||||
try:
|
||||
data = self.cookie_data_manager.load_simple_cookies()
|
||||
|
||||
url = data.get("url", "")
|
||||
cookies = data.get("cookies", [])
|
||||
|
||||
if not url:
|
||||
url = self.url_var.get().strip()
|
||||
|
||||
if not url:
|
||||
messagebox.showerror("错误", "没有有效的网址")
|
||||
return
|
||||
|
||||
if not (url.startswith("http://") or url.startswith("https://")):
|
||||
url = "https://" + url
|
||||
|
||||
self._cleanup_browser()
|
||||
|
||||
self.status_var.set("正在打开浏览器...")
|
||||
self.root.update()
|
||||
|
||||
self.browser_manager.init_browser()
|
||||
|
||||
self.status_var.set("等待浏览器就绪...")
|
||||
self.root.update()
|
||||
self.browser_manager.page.get("about:blank")
|
||||
time.sleep(1)
|
||||
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(url)
|
||||
domain = parsed.netloc
|
||||
|
||||
if cookies:
|
||||
self.status_var.set("正在导入cookies...")
|
||||
self.root.update()
|
||||
success_count = self.browser_manager.import_cookies(cookies, domain)
|
||||
logger.info(f"成功导入 {success_count} 个 cookies")
|
||||
|
||||
self.status_var.set(f"正在访问: {url}")
|
||||
self.root.update()
|
||||
self.browser_manager.page.get(url)
|
||||
self.browser_manager.wait_page_loaded()
|
||||
|
||||
self.status_var.set("刷新页面验证登录状态...")
|
||||
self.root.update()
|
||||
for i in range(3):
|
||||
self.browser_manager.page.refresh()
|
||||
self.browser_manager.wait_page_loaded()
|
||||
time.sleep(1)
|
||||
self.status_var.set(f"刷新第 {i+1} 次...")
|
||||
self.root.update()
|
||||
|
||||
self.url_var.set(url)
|
||||
self.status_var.set(f"已导入并打开: {url}")
|
||||
self.save_button.config(state=tk.NORMAL)
|
||||
messagebox.showinfo("成功", f"已成功导入 {success_count} 个cookie并打开网址: {url}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"导入cookies失败: {e}")
|
||||
messagebox.showerror("错误", f"导入cookies失败: {str(e)}")
|
||||
self.status_var.set("就绪")
|
||||
self._cleanup_browser()
|
||||
|
||||
def close_app(self):
|
||||
"""关闭应用"""
|
||||
self._cleanup_browser()
|
||||
self.root.destroy()
|
||||
|
||||
def _cleanup_browser(self):
|
||||
"""清理浏览器资源"""
|
||||
self.browser_manager.close()
|
||||
self.save_button.config(state=tk.DISABLED)
|
||||
self.status_var.set("就绪")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
root = tk.Tk()
|
||||
app = CookieExtractorApp(root)
|
||||
|
||||
def on_closing():
|
||||
app._cleanup_browser()
|
||||
root.destroy()
|
||||
|
||||
root.protocol("WM_DELETE_WINDOW", on_closing)
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,248 +0,0 @@
|
||||
# CookieCloud Client
|
||||
|
||||
一个独立的、可复用的CookieCloud服务器客户端Python模块。
|
||||
|
||||
## 特性
|
||||
|
||||
- ✅ **完全独立** - 无外部依赖,仅使用Python标准库
|
||||
- ✅ **类型安全** - 完整的类型提示支持
|
||||
- ✅ **异常处理** - 完善的异常体系
|
||||
- ✅ **易于使用** - 简洁的API设计
|
||||
- ✅ **可扩展** - 支持自定义配置
|
||||
- ✅ **文档完善** - 详细的使用文档和示例
|
||||
|
||||
## 安装
|
||||
|
||||
将 `cookiecloud_client` 目录复制到您的项目中即可使用。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 基本使用
|
||||
|
||||
```python
|
||||
from cookiecloud_client import CookieCloudClient, CookieConfig
|
||||
|
||||
# 创建配置
|
||||
config = CookieConfig(
|
||||
server="https://cookiecloud.example.com",
|
||||
username="your_username",
|
||||
password="your_password"
|
||||
)
|
||||
|
||||
# 创建客户端
|
||||
client = CookieCloudClient(config)
|
||||
|
||||
# 下载所有cookies
|
||||
result = client.download()
|
||||
|
||||
if result.success:
|
||||
print(f"下载成功!")
|
||||
print(f"总域名数: {result.total_domains}")
|
||||
print(f"总Cookie数: {result.total_cookies}")
|
||||
print(f"下载耗时: {result.download_time:.2f}秒")
|
||||
|
||||
# 获取指定域名的cookie
|
||||
cookie_str = result.get_cookie_string("example.com")
|
||||
print(f"example.com的cookie: {cookie_str}")
|
||||
else:
|
||||
print(f"下载失败: {result.error_message}")
|
||||
```
|
||||
|
||||
### 下载指定域名的Cookie
|
||||
|
||||
```python
|
||||
# 下载单个域名
|
||||
cookie_str = client.download_for_domain("baidu.com")
|
||||
if cookie_str:
|
||||
print(f"baidu.com的cookie: {cookie_str}")
|
||||
|
||||
# 批量下载多个域名
|
||||
domains = ["baidu.com", "google.com", "github.com"]
|
||||
cookies = client.download_for_domains(domains)
|
||||
for domain, cookie in cookies.items():
|
||||
print(f"{domain}: {cookie}")
|
||||
```
|
||||
|
||||
### 测试连接
|
||||
|
||||
```python
|
||||
success, message = client.test_connection()
|
||||
if success:
|
||||
print("连接成功!")
|
||||
else:
|
||||
print(f"连接失败: {message}")
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 自定义配置
|
||||
|
||||
```python
|
||||
config = CookieConfig(
|
||||
server="https://cookiecloud.example.com",
|
||||
username="your_username",
|
||||
password="your_password",
|
||||
timeout=60, # 超时时间(秒)
|
||||
verify_ssl=False, # 是否验证SSL证书
|
||||
ignore_cookies=[ # 忽略的cookie名称
|
||||
"CookieAutoDeleteBrowsingDataCleanup",
|
||||
"CookieAutoDeleteCleaningDiscarded"
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 异常处理
|
||||
|
||||
```python
|
||||
from cookiecloud_client import (
|
||||
CookieCloudClient,
|
||||
CookieConfig,
|
||||
CookieCloudError,
|
||||
ConnectionError,
|
||||
AuthenticationError,
|
||||
DataParseError,
|
||||
NetworkError
|
||||
)
|
||||
|
||||
try:
|
||||
client = CookieCloudClient(config)
|
||||
result = client.download()
|
||||
|
||||
except AuthenticationError as e:
|
||||
print(f"认证失败: {e.message}")
|
||||
print(f"用户名: {e.details.get('username')}")
|
||||
|
||||
except ConnectionError as e:
|
||||
print(f"连接失败: {e.message}")
|
||||
print(f"服务器: {e.details.get('server')}")
|
||||
print(f"状态码: {e.details.get('status_code')}")
|
||||
|
||||
except DataParseError as e:
|
||||
print(f"数据解析失败: {e.message}")
|
||||
|
||||
except NetworkError as e:
|
||||
print(f"网络错误: {e.message}")
|
||||
print(f"原始错误: {e.details.get('original_error')}")
|
||||
|
||||
except CookieCloudError as e:
|
||||
print(f"CookieCloud错误: {e.message}")
|
||||
```
|
||||
|
||||
### 使用Cookie数据对象
|
||||
|
||||
```python
|
||||
result = client.download()
|
||||
|
||||
# 获取所有域名
|
||||
domains = result.get_domains()
|
||||
print(f"所有域名: {domains}")
|
||||
|
||||
# 遍历所有cookie
|
||||
for domain, collection in result.cookies.items():
|
||||
print(f"\n域名: {domain}")
|
||||
for cookie in collection.cookies:
|
||||
print(f" - {cookie.name}={cookie.value[:20]}...")
|
||||
print(f" 路径: {cookie.path}")
|
||||
print(f" 安全: {cookie.secure}")
|
||||
print(f" HttpOnly: {cookie.http_only}")
|
||||
```
|
||||
|
||||
## API文档
|
||||
|
||||
### CookieConfig
|
||||
|
||||
配置类,用于存储CookieCloud服务器配置。
|
||||
|
||||
**参数:**
|
||||
- `server` (str): CookieCloud服务器地址
|
||||
- `username` (str): 用户名
|
||||
- `password` (str): 密码
|
||||
- `timeout` (int): 请求超时时间(秒),默认30
|
||||
- `verify_ssl` (bool): 是否验证SSL证书,默认True
|
||||
- `ignore_cookies` (List[str]): 忽略的cookie名称列表
|
||||
|
||||
### CookieCloudClient
|
||||
|
||||
客户端类,用于与CookieCloud服务器交互。
|
||||
|
||||
**方法:**
|
||||
|
||||
#### `download() -> DownloadResult`
|
||||
下载所有cookies
|
||||
|
||||
**返回:** DownloadResult对象
|
||||
|
||||
#### `download_for_domain(domain: str) -> Optional[str]`
|
||||
下载指定域名的cookie字符串
|
||||
|
||||
**参数:**
|
||||
- `domain` (str): 目标域名
|
||||
|
||||
**返回:** cookie字符串或None
|
||||
|
||||
#### `download_for_domains(domains: List[str]) -> Dict[str, Optional[str]]`
|
||||
批量下载多个域名的cookie字符串
|
||||
|
||||
**参数:**
|
||||
- `domains` (List[str]): 目标域名列表
|
||||
|
||||
**返回:** {domain: cookie_string}字典
|
||||
|
||||
#### `test_connection() -> Tuple[bool, str]`
|
||||
测试与CookieCloud服务器的连接
|
||||
|
||||
**返回:** (是否成功, 消息)元组
|
||||
|
||||
### DownloadResult
|
||||
|
||||
下载结果类。
|
||||
|
||||
**属性:**
|
||||
- `success` (bool): 是否成功
|
||||
- `cookies` (Dict[str, CookieCollection]): cookie集合字典
|
||||
- `error_message` (str): 错误信息
|
||||
- `total_domains` (int): 总域名数
|
||||
- `total_cookies` (int): 总cookie数
|
||||
- `download_time` (float): 下载耗时(秒)
|
||||
|
||||
**方法:**
|
||||
- `get_cookie_string(domain: str) -> Optional[str]`: 获取指定域名的cookie字符串
|
||||
- `get_domains() -> List[str]`: 获取所有域名列表
|
||||
|
||||
## 异常类
|
||||
|
||||
### CookieCloudError
|
||||
基础异常类,所有其他异常都继承自此类。
|
||||
|
||||
### ConfigurationError
|
||||
配置错误异常
|
||||
|
||||
### ConnectionError
|
||||
连接错误异常
|
||||
|
||||
### AuthenticationError
|
||||
认证错误异常
|
||||
|
||||
### DataParseError
|
||||
数据解析错误异常
|
||||
|
||||
### NetworkError
|
||||
网络错误异常
|
||||
|
||||
## 依赖
|
||||
|
||||
- Python 3.7+
|
||||
- 仅使用Python标准库
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2026-03-02)
|
||||
- 初始版本发布
|
||||
- 完整的CookieCloud客户端功能
|
||||
- 独立模块,无外部依赖
|
||||
- 完善的异常处理
|
||||
- 详细的文档和示例
|
||||
@@ -1,33 +0,0 @@
|
||||
"""
|
||||
CookieCloud客户端模块
|
||||
一个独立的、可复用的CookieCloud服务器客户端
|
||||
|
||||
作者: CookieManager Team
|
||||
版本: 1.0.0
|
||||
许可证: MIT
|
||||
"""
|
||||
|
||||
from .client import CookieCloudClient
|
||||
from .exceptions import (
|
||||
CookieCloudError,
|
||||
ConnectionError,
|
||||
AuthenticationError,
|
||||
DataParseError,
|
||||
ConfigurationError,
|
||||
NetworkError
|
||||
)
|
||||
from .models import CookieData, CookieConfig
|
||||
from .version import __version__
|
||||
|
||||
__all__ = [
|
||||
'CookieCloudClient',
|
||||
'CookieCloudError',
|
||||
'ConnectionError',
|
||||
'AuthenticationError',
|
||||
'DataParseError',
|
||||
'ConfigurationError',
|
||||
'NetworkError',
|
||||
'CookieData',
|
||||
'CookieConfig',
|
||||
'__version__'
|
||||
]
|
||||
@@ -1,347 +0,0 @@
|
||||
"""
|
||||
CookieCloud客户端核心实现
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from .models import CookieConfig, CookieData, CookieCollection, DownloadResult
|
||||
from .exceptions import (
|
||||
CookieCloudError,
|
||||
ConfigurationError,
|
||||
ConnectionError,
|
||||
AuthenticationError,
|
||||
DataParseError,
|
||||
NetworkError
|
||||
)
|
||||
|
||||
|
||||
class CookieCloudClient:
|
||||
"""
|
||||
CookieCloud客户端
|
||||
|
||||
用于从CookieCloud服务器下载和管理cookies的独立客户端
|
||||
|
||||
示例:
|
||||
>>> config = CookieConfig(
|
||||
... server="https://cookiecloud.example.com",
|
||||
... username="your_username",
|
||||
... password="your_password"
|
||||
... )
|
||||
>>> client = CookieCloudClient(config)
|
||||
>>> result = client.download()
|
||||
>>> if result.success:
|
||||
... print(f"下载成功,共{result.total_domains}个域名")
|
||||
... cookie_str = result.get_cookie_string("example.com")
|
||||
"""
|
||||
|
||||
def __init__(self, config: CookieConfig):
|
||||
"""
|
||||
初始化客户端
|
||||
|
||||
Args:
|
||||
config: CookieCloud配置对象
|
||||
|
||||
Raises:
|
||||
ConfigurationError: 配置验证失败
|
||||
"""
|
||||
if not isinstance(config, CookieConfig):
|
||||
raise ConfigurationError("配置参数必须是CookieConfig类型")
|
||||
|
||||
self.config = config
|
||||
self._last_download_time = None
|
||||
self._download_count = 0
|
||||
|
||||
def download(self) -> DownloadResult:
|
||||
"""
|
||||
从CookieCloud服务器下载所有cookies
|
||||
|
||||
Returns:
|
||||
DownloadResult: 下载结果对象
|
||||
|
||||
Raises:
|
||||
ConnectionError: 连接服务器失败
|
||||
AuthenticationError: 认证失败
|
||||
DataParseError: 数据解析失败
|
||||
NetworkError: 网络错误
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
raw_data = self._fetch_data()
|
||||
cookies = self._parse_cookies(raw_data)
|
||||
|
||||
download_time = time.time() - start_time
|
||||
self._last_download_time = time.time()
|
||||
self._download_count += 1
|
||||
|
||||
total_cookies = sum(len(c.cookies) for c in cookies.values())
|
||||
|
||||
return DownloadResult(
|
||||
success=True,
|
||||
cookies=cookies,
|
||||
total_domains=len(cookies),
|
||||
total_cookies=total_cookies,
|
||||
download_time=download_time
|
||||
)
|
||||
|
||||
except CookieCloudError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise CookieCloudError(f"下载cookies失败: {str(e)}")
|
||||
|
||||
def download_for_domain(self, domain: str) -> Optional[str]:
|
||||
"""
|
||||
下载指定域名的cookie字符串
|
||||
|
||||
Args:
|
||||
domain: 目标域名
|
||||
|
||||
Returns:
|
||||
Optional[str]: cookie字符串,如果不存在则返回None
|
||||
|
||||
Raises:
|
||||
ConnectionError: 连接服务器失败
|
||||
AuthenticationError: 认证失败
|
||||
DataParseError: 数据解析失败
|
||||
"""
|
||||
result = self.download()
|
||||
return result.get_cookie_string(domain)
|
||||
|
||||
def download_for_domains(self, domains: List[str]) -> Dict[str, Optional[str]]:
|
||||
"""
|
||||
批量下载多个域名的cookie字符串
|
||||
|
||||
Args:
|
||||
domains: 目标域名列表
|
||||
|
||||
Returns:
|
||||
Dict[str, Optional[str]]: {domain: cookie_string}
|
||||
"""
|
||||
result = self.download()
|
||||
return {domain: result.get_cookie_string(domain) for domain in domains}
|
||||
|
||||
def test_connection(self) -> Tuple[bool, str]:
|
||||
"""
|
||||
测试与CookieCloud服务器的连接
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (是否成功, 消息)
|
||||
"""
|
||||
try:
|
||||
self._fetch_data()
|
||||
return True, "连接成功"
|
||||
except AuthenticationError as e:
|
||||
return False, f"认证失败: {e.message}"
|
||||
except ConnectionError as e:
|
||||
return False, f"连接失败: {e.message}"
|
||||
except Exception as e:
|
||||
return False, f"测试失败: {str(e)}"
|
||||
|
||||
def _fetch_data(self) -> Dict:
|
||||
"""
|
||||
从服务器获取原始数据
|
||||
|
||||
Returns:
|
||||
Dict: 原始JSON数据
|
||||
|
||||
Raises:
|
||||
ConnectionError: 连接失败
|
||||
AuthenticationError: 认证失败
|
||||
NetworkError: 网络错误
|
||||
"""
|
||||
url = f"{self.config.server}/get/{self.config.username}"
|
||||
|
||||
try:
|
||||
data = json.dumps({"password": self.config.password}).encode('utf-8')
|
||||
|
||||
request = urllib.request.Request(
|
||||
url,
|
||||
data=data,
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'CookieCloudClient/1.0'
|
||||
},
|
||||
method='POST'
|
||||
)
|
||||
|
||||
response = urllib.request.urlopen(
|
||||
request,
|
||||
timeout=self.config.timeout
|
||||
)
|
||||
|
||||
if response.status != 200:
|
||||
if response.status == 401:
|
||||
raise AuthenticationError(
|
||||
"认证失败,请检查用户名和密码",
|
||||
username=self.config.username
|
||||
)
|
||||
elif response.status == 404:
|
||||
raise ConnectionError(
|
||||
"用户不存在,请检查用户名",
|
||||
server=self.config.server,
|
||||
status_code=response.status
|
||||
)
|
||||
else:
|
||||
raise ConnectionError(
|
||||
f"服务器返回错误状态码: {response.status}",
|
||||
server=self.config.server,
|
||||
status_code=response.status
|
||||
)
|
||||
|
||||
result = json.loads(response.read().decode('utf-8'))
|
||||
|
||||
if not result:
|
||||
raise DataParseError("服务器返回数据为空")
|
||||
|
||||
return result
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 401:
|
||||
raise AuthenticationError(
|
||||
"认证失败,请检查用户名和密码",
|
||||
username=self.config.username
|
||||
)
|
||||
else:
|
||||
raise ConnectionError(
|
||||
f"HTTP错误: {e.code} {e.reason}",
|
||||
server=self.config.server,
|
||||
status_code=e.code
|
||||
)
|
||||
except urllib.error.URLError as e:
|
||||
raise NetworkError(
|
||||
f"网络连接失败: {e.reason}",
|
||||
original_error=e
|
||||
)
|
||||
except json.JSONDecodeError as e:
|
||||
raise DataParseError(
|
||||
f"JSON解析失败: {str(e)}"
|
||||
)
|
||||
except Exception as e:
|
||||
if isinstance(e, CookieCloudError):
|
||||
raise
|
||||
raise NetworkError(
|
||||
f"请求失败: {str(e)}",
|
||||
original_error=e
|
||||
)
|
||||
|
||||
def _parse_cookies(self, raw_data: Dict) -> Dict[str, CookieCollection]:
|
||||
"""
|
||||
解析原始cookie数据
|
||||
|
||||
Args:
|
||||
raw_data: 原始JSON数据
|
||||
|
||||
Returns:
|
||||
Dict[str, CookieCollection]: {domain: CookieCollection}
|
||||
"""
|
||||
if raw_data.get("cookie_data"):
|
||||
contents = raw_data.get("cookie_data")
|
||||
else:
|
||||
contents = raw_data
|
||||
|
||||
domain_groups = self._group_by_domain(contents)
|
||||
|
||||
cookies = {}
|
||||
for domain, cookie_list in domain_groups.items():
|
||||
if not cookie_list:
|
||||
continue
|
||||
|
||||
if self._is_cloudflare_only(cookie_list):
|
||||
continue
|
||||
|
||||
collection = CookieCollection(domain=domain)
|
||||
|
||||
for cookie_data in cookie_list:
|
||||
cookie = CookieData(
|
||||
domain=cookie_data.get('domain', ''),
|
||||
name=cookie_data.get('name', ''),
|
||||
value=cookie_data.get('value', ''),
|
||||
path=cookie_data.get('path', '/'),
|
||||
secure=cookie_data.get('secure', False),
|
||||
http_only=cookie_data.get('httpOnly', False)
|
||||
)
|
||||
collection.add_cookie(cookie)
|
||||
|
||||
cookies[domain] = collection
|
||||
|
||||
return cookies
|
||||
|
||||
def _group_by_domain(self, contents: Dict) -> Dict[str, List[Dict]]:
|
||||
"""
|
||||
按域名分组cookies
|
||||
|
||||
Args:
|
||||
contents: 原始cookie内容
|
||||
|
||||
Returns:
|
||||
Dict[str, List[Dict]]: {domain: [cookie_data]}
|
||||
"""
|
||||
domain_groups = {}
|
||||
|
||||
for site, cookies in contents.items():
|
||||
for cookie in cookies:
|
||||
domain = cookie.get("domain", "")
|
||||
if not domain:
|
||||
continue
|
||||
|
||||
domain_key = self._extract_domain(domain)
|
||||
if not domain_key:
|
||||
continue
|
||||
|
||||
if domain_key not in domain_groups:
|
||||
domain_groups[domain_key] = []
|
||||
|
||||
domain_groups[domain_key].append(cookie)
|
||||
|
||||
return domain_groups
|
||||
|
||||
def _extract_domain(self, domain: str) -> Optional[str]:
|
||||
"""
|
||||
提取主域名
|
||||
|
||||
Args:
|
||||
domain: 原始域名
|
||||
|
||||
Returns:
|
||||
Optional[str]: 主域名
|
||||
"""
|
||||
if not domain:
|
||||
return None
|
||||
|
||||
domain = domain.lstrip('.')
|
||||
|
||||
parts = domain.split('.')
|
||||
if len(parts) < 2:
|
||||
return domain
|
||||
|
||||
if len(parts) == 2:
|
||||
return domain
|
||||
|
||||
return '.'.join(parts[-2:])
|
||||
|
||||
def _is_cloudflare_only(self, cookie_list: List[Dict]) -> bool:
|
||||
"""
|
||||
检查是否仅包含Cloudflare验证cookie
|
||||
|
||||
Args:
|
||||
cookie_list: cookie列表
|
||||
|
||||
Returns:
|
||||
bool: 是否仅包含cf_clearance
|
||||
"""
|
||||
for cookie in cookie_list:
|
||||
if cookie.get("name") != "cf_clearance":
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def last_download_time(self) -> Optional[float]:
|
||||
"""获取最后下载时间"""
|
||||
return self._last_download_time
|
||||
|
||||
@property
|
||||
def download_count(self) -> int:
|
||||
"""获取下载次数"""
|
||||
return self._download_count
|
||||
@@ -1,259 +0,0 @@
|
||||
"""
|
||||
CookieCloud客户端使用示例
|
||||
演示如何使用cookiecloud_client模块
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加父目录到路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# 示例1: 基本使用
|
||||
def example_basic_usage():
|
||||
"""基本使用示例"""
|
||||
from cookiecloud_client import CookieCloudClient, CookieConfig
|
||||
|
||||
print("=" * 60)
|
||||
print("示例1: 基本使用")
|
||||
print("=" * 60)
|
||||
|
||||
# 创建配置
|
||||
config = CookieConfig(
|
||||
server="https://movie-pilot.org/cookiecloud",
|
||||
username="hu6n2vcUmzpu7mqUN2rVCg",
|
||||
password="jtDM5dV9AyqXkZdQVeA9f6"
|
||||
)
|
||||
|
||||
# 创建客户端
|
||||
client = CookieCloudClient(config)
|
||||
|
||||
# 测试连接
|
||||
success, message = client.test_connection()
|
||||
print(f"连接测试: {message}")
|
||||
|
||||
if success:
|
||||
# 下载所有cookies
|
||||
result = client.download()
|
||||
|
||||
if result.success:
|
||||
print(f"\n✓ 下载成功!")
|
||||
print(f" 总域名数: {result.total_domains}")
|
||||
print(f" 总Cookie数: {result.total_cookies}")
|
||||
print(f" 下载耗时: {result.download_time:.2f}秒")
|
||||
|
||||
# 显示前5个域名
|
||||
domains = result.get_domains()[:5]
|
||||
print(f"\n前5个域名:")
|
||||
for idx, domain in enumerate(domains, 1):
|
||||
cookie_str = result.get_cookie_string(domain)
|
||||
preview = cookie_str[:50] + "..." if len(cookie_str) > 50 else cookie_str
|
||||
print(f" {idx}. {domain}: {preview}")
|
||||
else:
|
||||
print(f"\n✗ 下载失败: {result.error_message}")
|
||||
|
||||
|
||||
# 示例2: 下载指定域名的Cookie
|
||||
def example_download_specific_domain():
|
||||
"""下载指定域名示例"""
|
||||
from cookiecloud_client import CookieCloudClient, CookieConfig
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("示例2: 下载指定域名的Cookie")
|
||||
print("=" * 60)
|
||||
|
||||
config = CookieConfig(
|
||||
server="https://movie-pilot.org/cookiecloud",
|
||||
username="hu6n2vcUmzpu7mqUN2rVCg",
|
||||
password="jtDM5dV9AyqXkZdQVeA9f6"
|
||||
)
|
||||
|
||||
client = CookieCloudClient(config)
|
||||
|
||||
# 下载单个域名
|
||||
domain = "baidu.com"
|
||||
cookie_str = client.download_for_domain(domain)
|
||||
|
||||
if cookie_str:
|
||||
print(f"✓ {domain}的Cookie:")
|
||||
print(f" {cookie_str[:100]}...")
|
||||
else:
|
||||
print(f"✗ 未找到{domain}的Cookie")
|
||||
|
||||
|
||||
# 示例3: 批量下载多个域名
|
||||
def example_download_multiple_domains():
|
||||
"""批量下载示例"""
|
||||
from cookiecloud_client import CookieCloudClient, CookieConfig
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("示例3: 批量下载多个域名")
|
||||
print("=" * 60)
|
||||
|
||||
config = CookieConfig(
|
||||
server="https://movie-pilot.org/cookiecloud",
|
||||
username="hu6n2vcUmzpu7mqUN2rVCg",
|
||||
password="jtDM5dV9AyqXkZdQVeA9f6"
|
||||
)
|
||||
|
||||
client = CookieCloudClient(config)
|
||||
|
||||
# 批量下载
|
||||
domains = ["baidu.com", "github.com", "google.com", "bing.com"]
|
||||
cookies = client.download_for_domains(domains)
|
||||
|
||||
print("批量下载结果:")
|
||||
for domain, cookie_str in cookies.items():
|
||||
if cookie_str:
|
||||
preview = cookie_str[:50] + "..."
|
||||
print(f" ✓ {domain}: {preview}")
|
||||
else:
|
||||
print(f" ✗ {domain}: 未找到Cookie")
|
||||
|
||||
|
||||
# 示例4: 异常处理
|
||||
def example_error_handling():
|
||||
"""异常处理示例"""
|
||||
from cookiecloud_client import (
|
||||
CookieCloudClient,
|
||||
CookieConfig,
|
||||
CookieCloudError,
|
||||
ConnectionError,
|
||||
AuthenticationError,
|
||||
NetworkError
|
||||
)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("示例4: 异常处理")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# 使用错误的凭据
|
||||
config = CookieConfig(
|
||||
server="https://movie-pilot.org/cookiecloud",
|
||||
username="wrong_user",
|
||||
password="wrong_pass"
|
||||
)
|
||||
|
||||
client = CookieCloudClient(config)
|
||||
result = client.download()
|
||||
|
||||
except AuthenticationError as e:
|
||||
print(f"✗ 认证失败: {e.message}")
|
||||
print(f" 用户名: {e.details.get('username')}")
|
||||
|
||||
except ConnectionError as e:
|
||||
print(f"✗ 连接失败: {e.message}")
|
||||
print(f" 服务器: {e.details.get('server')}")
|
||||
print(f" 状态码: {e.details.get('status_code')}")
|
||||
|
||||
except NetworkError as e:
|
||||
print(f"✗ 网络错误: {e.message}")
|
||||
print(f" 原始错误: {e.details.get('original_error')}")
|
||||
|
||||
except CookieCloudError as e:
|
||||
print(f"✗ CookieCloud错误: {e.message}")
|
||||
|
||||
|
||||
# 示例5: 自定义配置
|
||||
def example_custom_config():
|
||||
"""自定义配置示例"""
|
||||
from cookiecloud_client import CookieCloudClient, CookieConfig
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("示例5: 自定义配置")
|
||||
print("=" * 60)
|
||||
|
||||
# 自定义配置
|
||||
config = CookieConfig(
|
||||
server="https://movie-pilot.org/cookiecloud",
|
||||
username="hu6n2vcUmzpu7mqUN2rVCg",
|
||||
password="jtDM5dV9AyqXkZdQVeA9f6",
|
||||
timeout=60, # 60秒超时
|
||||
verify_ssl=False, # 不验证SSL证书
|
||||
ignore_cookies=[ # 忽略的cookie
|
||||
"CookieAutoDeleteBrowsingDataCleanup",
|
||||
"CookieAutoDeleteCleaningDiscarded",
|
||||
"_ga", # 忽略Google Analytics
|
||||
]
|
||||
)
|
||||
|
||||
client = CookieCloudClient(config)
|
||||
|
||||
print(f"配置信息:")
|
||||
print(f" 服务器: {config.server}")
|
||||
print(f" 用户名: {config.username}")
|
||||
print(f" 超时时间: {config.timeout}秒")
|
||||
print(f" 验证SSL: {config.verify_ssl}")
|
||||
print(f" 忽略Cookie: {len(config.ignore_cookies)}个")
|
||||
|
||||
result = client.download()
|
||||
|
||||
if result.success:
|
||||
print(f"\n✓ 下载成功")
|
||||
print(f" 总域名数: {result.total_domains}")
|
||||
print(f" 总Cookie数: {result.total_cookies}")
|
||||
|
||||
|
||||
# 示例6: 使用Cookie数据对象
|
||||
def example_cookie_data_objects():
|
||||
"""使用Cookie数据对象示例"""
|
||||
from cookiecloud_client import CookieCloudClient, CookieConfig
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("示例6: 使用Cookie数据对象")
|
||||
print("=" * 60)
|
||||
|
||||
config = CookieConfig(
|
||||
server="https://movie-pilot.org/cookiecloud",
|
||||
username="hu6n2vcUmzpu7mqUN2rVCg",
|
||||
password="jtDM5dV9AyqXkZdQVeA9f6"
|
||||
)
|
||||
|
||||
client = CookieCloudClient(config)
|
||||
result = client.download()
|
||||
|
||||
if result.success:
|
||||
# 获取第一个域名的详细信息
|
||||
first_domain = result.get_domains()[0]
|
||||
collection = result.cookies.get(first_domain)
|
||||
|
||||
if collection:
|
||||
print(f"域名: {collection.domain}")
|
||||
print(f"Cookie数量: {len(collection.cookies)}")
|
||||
print(f"\nCookie详情:")
|
||||
|
||||
for idx, cookie in enumerate(collection.cookies[:3], 1):
|
||||
print(f" {idx}. {cookie.name}")
|
||||
print(f" 值: {cookie.value[:30]}...")
|
||||
print(f" 路径: {cookie.path}")
|
||||
print(f" 安全: {cookie.secure}")
|
||||
print(f" HttpOnly: {cookie.http_only}")
|
||||
print()
|
||||
|
||||
|
||||
# 主函数
|
||||
def main():
|
||||
"""运行所有示例"""
|
||||
print("\n")
|
||||
print("╔" + "=" * 58 + "╗")
|
||||
print("║" + " " * 15 + "CookieCloud客户端使用示例" + " " * 17 + "║")
|
||||
print("╚" + "=" * 58 + "╝")
|
||||
|
||||
try:
|
||||
example_basic_usage()
|
||||
example_download_specific_domain()
|
||||
example_download_multiple_domains()
|
||||
example_error_handling()
|
||||
example_custom_config()
|
||||
example_cookie_data_objects()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("所有示例执行完成!")
|
||||
print("=" * 60)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n示例执行出错: {str(e)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,65 +0,0 @@
|
||||
"""
|
||||
CookieCloud客户端异常类
|
||||
"""
|
||||
|
||||
|
||||
class CookieCloudError(Exception):
|
||||
"""CookieCloud基础异常类"""
|
||||
|
||||
def __init__(self, message: str, details: dict = None):
|
||||
self.message = message
|
||||
self.details = details or {}
|
||||
super().__init__(self.message)
|
||||
|
||||
def __str__(self):
|
||||
if self.details:
|
||||
return f"{self.message} - 详情: {self.details}"
|
||||
return self.message
|
||||
|
||||
|
||||
class ConfigurationError(CookieCloudError):
|
||||
"""配置错误异常"""
|
||||
|
||||
def __init__(self, message: str, config_key: str = None):
|
||||
details = {'config_key': config_key} if config_key else {}
|
||||
super().__init__(message, details)
|
||||
|
||||
|
||||
class ConnectionError(CookieCloudError):
|
||||
"""连接错误异常"""
|
||||
|
||||
def __init__(self, message: str, server: str = None, status_code: int = None):
|
||||
details = {}
|
||||
if server:
|
||||
details['server'] = server
|
||||
if status_code:
|
||||
details['status_code'] = status_code
|
||||
super().__init__(message, details)
|
||||
|
||||
|
||||
class AuthenticationError(CookieCloudError):
|
||||
"""认证错误异常"""
|
||||
|
||||
def __init__(self, message: str, username: str = None):
|
||||
details = {'username': username} if username else {}
|
||||
super().__init__(message, details)
|
||||
|
||||
|
||||
class DataParseError(CookieCloudError):
|
||||
"""数据解析错误异常"""
|
||||
|
||||
def __init__(self, message: str, raw_data: str = None):
|
||||
details = {}
|
||||
if raw_data:
|
||||
details['raw_data_length'] = len(raw_data)
|
||||
super().__init__(message, details)
|
||||
|
||||
|
||||
class NetworkError(CookieCloudError):
|
||||
"""网络错误异常"""
|
||||
|
||||
def __init__(self, message: str, original_error: Exception = None):
|
||||
details = {}
|
||||
if original_error:
|
||||
details['original_error'] = str(original_error)
|
||||
super().__init__(message, details)
|
||||
@@ -1,126 +0,0 @@
|
||||
"""
|
||||
CookieCloud数据模型
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict, List
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class CookieConfig:
|
||||
"""CookieCloud配置"""
|
||||
server: str
|
||||
username: str
|
||||
password: str
|
||||
timeout: int = 30
|
||||
verify_ssl: bool = True
|
||||
ignore_cookies: List[str] = field(default_factory=lambda: [
|
||||
"CookieAutoDeleteBrowsingDataCleanup",
|
||||
"CookieAutoDeleteCleaningDiscarded"
|
||||
])
|
||||
|
||||
def __post_init__(self):
|
||||
"""验证配置参数"""
|
||||
if not self.server:
|
||||
raise ValueError("服务器地址不能为空")
|
||||
if not self.username:
|
||||
raise ValueError("用户名不能为空")
|
||||
if not self.password:
|
||||
raise ValueError("密码不能为空")
|
||||
if not self.server.startswith(('http://', 'https://')):
|
||||
self.server = f"https://{self.server}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CookieData:
|
||||
"""Cookie数据"""
|
||||
domain: str
|
||||
name: str
|
||||
value: str
|
||||
path: str = "/"
|
||||
secure: bool = False
|
||||
http_only: bool = False
|
||||
expiry: Optional[datetime] = None
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
'domain': self.domain,
|
||||
'name': self.name,
|
||||
'value': self.value,
|
||||
'path': self.path,
|
||||
'secure': self.secure,
|
||||
'httpOnly': self.http_only,
|
||||
'expiry': self.expiry.isoformat() if self.expiry else None
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict) -> 'CookieData':
|
||||
"""从字典创建"""
|
||||
expiry = None
|
||||
if data.get('expiry'):
|
||||
try:
|
||||
expiry = datetime.fromisoformat(data['expiry'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return cls(
|
||||
domain=data.get('domain', ''),
|
||||
name=data.get('name', ''),
|
||||
value=data.get('value', ''),
|
||||
path=data.get('path', '/'),
|
||||
secure=data.get('secure', False),
|
||||
http_only=data.get('httpOnly', False),
|
||||
expiry=expiry
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CookieCollection:
|
||||
"""Cookie集合"""
|
||||
domain: str
|
||||
cookies: List[CookieData] = field(default_factory=list)
|
||||
|
||||
def to_cookie_string(self, ignore_list: List[str] = None) -> str:
|
||||
"""转换为cookie字符串"""
|
||||
ignore_list = ignore_list or []
|
||||
cookie_parts = []
|
||||
|
||||
for cookie in self.cookies:
|
||||
if cookie.name not in ignore_list:
|
||||
cookie_parts.append(f"{cookie.name}={cookie.value}")
|
||||
|
||||
return ";".join(cookie_parts)
|
||||
|
||||
def add_cookie(self, cookie: CookieData):
|
||||
"""添加cookie"""
|
||||
self.cookies.append(cookie)
|
||||
|
||||
def get_cookie_by_name(self, name: str) -> Optional[CookieData]:
|
||||
"""根据名称获取cookie"""
|
||||
for cookie in self.cookies:
|
||||
if cookie.name == name:
|
||||
return cookie
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class DownloadResult:
|
||||
"""下载结果"""
|
||||
success: bool
|
||||
cookies: Dict[str, CookieCollection] = field(default_factory=dict)
|
||||
error_message: str = ""
|
||||
total_domains: int = 0
|
||||
total_cookies: int = 0
|
||||
download_time: float = 0.0
|
||||
|
||||
def get_cookie_string(self, domain: str) -> Optional[str]:
|
||||
"""获取指定域名的cookie字符串"""
|
||||
collection = self.cookies.get(domain)
|
||||
if collection:
|
||||
return collection.to_cookie_string()
|
||||
return None
|
||||
|
||||
def get_domains(self) -> List[str]:
|
||||
"""获取所有域名列表"""
|
||||
return list(self.cookies.keys())
|
||||
@@ -1,14 +0,0 @@
|
||||
# CookieCloud客户端依赖项
|
||||
# 此模块完全独立,仅使用Python标准库
|
||||
|
||||
# Python版本要求
|
||||
Python>=3.7
|
||||
|
||||
# 无外部依赖
|
||||
# 仅使用Python标准库模块:
|
||||
# - json
|
||||
# - urllib
|
||||
# - dataclasses
|
||||
# - typing
|
||||
# - datetime
|
||||
# - unittest (用于测试)
|
||||
@@ -1,54 +0,0 @@
|
||||
"""
|
||||
CookieCloud客户端安装脚本
|
||||
"""
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
with open("README.md", "r", encoding="utf-8") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setup(
|
||||
name="cookiecloud-client",
|
||||
version="1.0.0",
|
||||
author="CookieManager Team",
|
||||
author_email="support@example.com",
|
||||
description="一个独立的、可复用的CookieCloud服务器客户端",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/example/cookiecloud-client",
|
||||
packages=find_packages(),
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
],
|
||||
python_requires=">=3.7",
|
||||
install_requires=[
|
||||
# 无外部依赖,仅使用Python标准库
|
||||
],
|
||||
extras_require={
|
||||
"dev": [
|
||||
"pytest>=6.0",
|
||||
"pytest-cov>=2.0",
|
||||
"black>=21.0",
|
||||
"flake8>=3.9",
|
||||
],
|
||||
},
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"cookiecloud-client=cookiecloud_client.cli:main",
|
||||
],
|
||||
},
|
||||
project_urls={
|
||||
"Bug Reports": "https://github.com/example/cookiecloud-client/issues",
|
||||
"Source": "https://github.com/example/cookiecloud-client",
|
||||
"Documentation": "https://github.com/example/cookiecloud-client#readme",
|
||||
},
|
||||
)
|
||||
@@ -1,295 +0,0 @@
|
||||
"""
|
||||
CookieCloud客户端单元测试
|
||||
"""
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import json
|
||||
from cookiecloud_client import (
|
||||
CookieCloudClient,
|
||||
CookieConfig,
|
||||
CookieCloudError,
|
||||
ConfigurationError,
|
||||
ConnectionError,
|
||||
AuthenticationError,
|
||||
DataParseError,
|
||||
NetworkError
|
||||
)
|
||||
|
||||
|
||||
class TestCookieConfig(unittest.TestCase):
|
||||
"""测试CookieConfig配置类"""
|
||||
|
||||
def test_valid_config(self):
|
||||
"""测试有效配置"""
|
||||
config = CookieConfig(
|
||||
server="https://example.com",
|
||||
username="user",
|
||||
password="pass"
|
||||
)
|
||||
self.assertEqual(config.server, "https://example.com")
|
||||
self.assertEqual(config.username, "user")
|
||||
self.assertEqual(config.password, "pass")
|
||||
self.assertEqual(config.timeout, 30)
|
||||
self.assertTrue(config.verify_ssl)
|
||||
|
||||
def test_config_with_custom_params(self):
|
||||
"""测试自定义参数配置"""
|
||||
config = CookieConfig(
|
||||
server="https://example.com",
|
||||
username="user",
|
||||
password="pass",
|
||||
timeout=60,
|
||||
verify_ssl=False,
|
||||
ignore_cookies=["test_cookie"]
|
||||
)
|
||||
self.assertEqual(config.timeout, 60)
|
||||
self.assertFalse(config.verify_ssl)
|
||||
self.assertEqual(config.ignore_cookies, ["test_cookie"])
|
||||
|
||||
def test_config_auto_add_protocol(self):
|
||||
"""测试自动添加协议"""
|
||||
config = CookieConfig(
|
||||
server="example.com",
|
||||
username="user",
|
||||
password="pass"
|
||||
)
|
||||
self.assertTrue(config.server.startswith("https://"))
|
||||
|
||||
def test_config_empty_server(self):
|
||||
"""测试空服务器地址"""
|
||||
with self.assertRaises(ValueError):
|
||||
CookieConfig(server="", username="user", password="pass")
|
||||
|
||||
def test_config_empty_username(self):
|
||||
"""测试空用户名"""
|
||||
with self.assertRaises(ValueError):
|
||||
CookieConfig(server="https://example.com", username="", password="pass")
|
||||
|
||||
def test_config_empty_password(self):
|
||||
"""测试空密码"""
|
||||
with self.assertRaises(ValueError):
|
||||
CookieConfig(server="https://example.com", username="user", password="")
|
||||
|
||||
|
||||
class TestCookieCloudClient(unittest.TestCase):
|
||||
"""测试CookieCloudClient客户端类"""
|
||||
|
||||
def setUp(self):
|
||||
"""测试前准备"""
|
||||
self.config = CookieConfig(
|
||||
server="https://test.example.com",
|
||||
username="testuser",
|
||||
password="testpass"
|
||||
)
|
||||
self.client = CookieCloudClient(self.config)
|
||||
|
||||
def test_client_initialization(self):
|
||||
"""测试客户端初始化"""
|
||||
self.assertIsInstance(self.client.config, CookieConfig)
|
||||
self.assertIsNone(self.client.last_download_time)
|
||||
self.assertEqual(self.client.download_count, 0)
|
||||
|
||||
def test_client_invalid_config(self):
|
||||
"""测试无效配置"""
|
||||
with self.assertRaises(ConfigurationError):
|
||||
CookieCloudClient("invalid_config")
|
||||
|
||||
@patch('urllib.request.urlopen')
|
||||
def test_download_success(self, mock_urlopen):
|
||||
"""测试成功下载"""
|
||||
mock_response = Mock()
|
||||
mock_response.status = 200
|
||||
mock_response.read.return_value = json.dumps({
|
||||
"cookie_data": {
|
||||
"test.com": [
|
||||
{
|
||||
"domain": "test.com",
|
||||
"name": "session",
|
||||
"value": "test_value",
|
||||
"path": "/",
|
||||
"secure": False,
|
||||
"httpOnly": False
|
||||
}
|
||||
]
|
||||
}
|
||||
}).encode('utf-8')
|
||||
mock_urlopen.return_value = mock_response
|
||||
|
||||
result = self.client.download()
|
||||
|
||||
self.assertTrue(result.success)
|
||||
self.assertEqual(result.total_domains, 1)
|
||||
self.assertEqual(result.total_cookies, 1)
|
||||
self.assertIsNotNone(self.client.last_download_time)
|
||||
self.assertEqual(self.client.download_count, 1)
|
||||
|
||||
@patch('urllib.request.urlopen')
|
||||
def test_download_authentication_error(self, mock_urlopen):
|
||||
"""测试认证失败"""
|
||||
import urllib.error
|
||||
|
||||
mock_urlopen.side_effect = urllib.error.HTTPError(
|
||||
url="https://test.example.com/get/testuser",
|
||||
code=401,
|
||||
msg="Unauthorized",
|
||||
hdrs={},
|
||||
fp=None
|
||||
)
|
||||
|
||||
with self.assertRaises(AuthenticationError):
|
||||
self.client.download()
|
||||
|
||||
@patch('urllib.request.urlopen')
|
||||
def test_download_connection_error(self, mock_urlopen):
|
||||
"""测试连接错误"""
|
||||
import urllib.error
|
||||
|
||||
mock_urlopen.side_effect = urllib.error.HTTPError(
|
||||
url="https://test.example.com/get/testuser",
|
||||
code=404,
|
||||
msg="Not Found",
|
||||
hdrs={},
|
||||
fp=None
|
||||
)
|
||||
|
||||
with self.assertRaises(ConnectionError):
|
||||
self.client.download()
|
||||
|
||||
@patch('urllib.request.urlopen')
|
||||
def test_download_network_error(self, mock_urlopen):
|
||||
"""测试网络错误"""
|
||||
import urllib.error
|
||||
|
||||
mock_urlopen.side_effect = urllib.error.URLError("Connection refused")
|
||||
|
||||
with self.assertRaises(NetworkError):
|
||||
self.client.download()
|
||||
|
||||
@patch('urllib.request.urlopen')
|
||||
def test_download_empty_data(self, mock_urlopen):
|
||||
"""测试空数据"""
|
||||
mock_response = Mock()
|
||||
mock_response.status = 200
|
||||
mock_response.read.return_value = json.dumps({}).encode('utf-8')
|
||||
mock_urlopen.return_value = mock_response
|
||||
|
||||
with self.assertRaises(DataParseError):
|
||||
self.client.download()
|
||||
|
||||
@patch('urllib.request.urlopen')
|
||||
def test_test_connection_success(self, mock_urlopen):
|
||||
"""测试连接测试成功"""
|
||||
mock_response = Mock()
|
||||
mock_response.status = 200
|
||||
mock_response.read.return_value = json.dumps({
|
||||
"cookie_data": {}
|
||||
}).encode('utf-8')
|
||||
mock_urlopen.return_value = mock_response
|
||||
|
||||
success, message = self.client.test_connection()
|
||||
|
||||
self.assertTrue(success)
|
||||
self.assertEqual(message, "连接成功")
|
||||
|
||||
@patch('urllib.request.urlopen')
|
||||
def test_test_connection_failure(self, mock_urlopen):
|
||||
"""测试连接测试失败"""
|
||||
import urllib.error
|
||||
|
||||
mock_urlopen.side_effect = urllib.error.HTTPError(
|
||||
url="https://test.example.com/get/testuser",
|
||||
code=401,
|
||||
msg="Unauthorized",
|
||||
hdrs={},
|
||||
fp=None
|
||||
)
|
||||
|
||||
success, message = self.client.test_connection()
|
||||
|
||||
self.assertFalse(success)
|
||||
self.assertIn("认证失败", message)
|
||||
|
||||
@patch('urllib.request.urlopen')
|
||||
def test_download_for_domain(self, mock_urlopen):
|
||||
"""测试下载指定域名"""
|
||||
mock_response = Mock()
|
||||
mock_response.status = 200
|
||||
mock_response.read.return_value = json.dumps({
|
||||
"cookie_data": {
|
||||
"test.com": [
|
||||
{
|
||||
"domain": "test.com",
|
||||
"name": "session",
|
||||
"value": "test_value",
|
||||
"path": "/",
|
||||
"secure": False,
|
||||
"httpOnly": False
|
||||
}
|
||||
]
|
||||
}
|
||||
}).encode('utf-8')
|
||||
mock_urlopen.return_value = mock_response
|
||||
|
||||
cookie_str = self.client.download_for_domain("test.com")
|
||||
|
||||
self.assertIsNotNone(cookie_str)
|
||||
self.assertIn("session=test_value", cookie_str)
|
||||
|
||||
@patch('urllib.request.urlopen')
|
||||
def test_download_for_domains(self, mock_urlopen):
|
||||
"""测试批量下载多个域名"""
|
||||
mock_response = Mock()
|
||||
mock_response.status = 200
|
||||
mock_response.read.return_value = json.dumps({
|
||||
"cookie_data": {
|
||||
"test1.com": [
|
||||
{"domain": "test1.com", "name": "cookie1", "value": "value1", "path": "/"}
|
||||
],
|
||||
"test2.com": [
|
||||
{"domain": "test2.com", "name": "cookie2", "value": "value2", "path": "/"}
|
||||
]
|
||||
}
|
||||
}).encode('utf-8')
|
||||
mock_urlopen.return_value = mock_response
|
||||
|
||||
domains = ["test1.com", "test2.com", "test3.com"]
|
||||
cookies = self.client.download_for_domains(domains)
|
||||
|
||||
self.assertEqual(len(cookies), 3)
|
||||
self.assertIn("cookie1=value1", cookies["test1.com"])
|
||||
self.assertIn("cookie2=value2", cookies["test2.com"])
|
||||
self.assertIsNone(cookies["test3.com"])
|
||||
|
||||
|
||||
class TestExceptions(unittest.TestCase):
|
||||
"""测试异常类"""
|
||||
|
||||
def test_cookie_cloud_error(self):
|
||||
"""测试基础异常"""
|
||||
error = CookieCloudError("测试错误", {"key": "value"})
|
||||
self.assertEqual(error.message, "测试错误")
|
||||
self.assertEqual(error.details, {"key": "value"})
|
||||
self.assertIn("测试错误", str(error))
|
||||
|
||||
def test_configuration_error(self):
|
||||
"""测试配置错误"""
|
||||
error = ConfigurationError("配置错误", config_key="server")
|
||||
self.assertEqual(error.message, "配置错误")
|
||||
self.assertEqual(error.details["config_key"], "server")
|
||||
|
||||
def test_connection_error(self):
|
||||
"""测试连接错误"""
|
||||
error = ConnectionError("连接失败", server="example.com", status_code=404)
|
||||
self.assertEqual(error.message, "连接失败")
|
||||
self.assertEqual(error.details["server"], "example.com")
|
||||
self.assertEqual(error.details["status_code"], 404)
|
||||
|
||||
def test_authentication_error(self):
|
||||
"""测试认证错误"""
|
||||
error = AuthenticationError("认证失败", username="testuser")
|
||||
self.assertEqual(error.message, "认证失败")
|
||||
self.assertEqual(error.details["username"], "testuser")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,8 +0,0 @@
|
||||
"""
|
||||
版本信息
|
||||
"""
|
||||
|
||||
__version__ = '1.0.0'
|
||||
__author__ = 'CookieManager Team'
|
||||
__email__ = 'support@example.com'
|
||||
__license__ = 'MIT'
|
||||
@@ -1,12 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "language",
|
||||
"value": "zh-CN",
|
||||
"domain": "192.168.8.156"
|
||||
},
|
||||
{
|
||||
"name": "fnos-token",
|
||||
"value": "sxRTKZf7p2lA4txCiYB1dvB0xT+sVmDkrmxaX3L61a4=",
|
||||
"domain": "192.168.8.156"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Cookie监控管理系统 - 检测客户端"""
|
||||
import time
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
from DrissionPage import ChromiumPage, ChromiumOptions
|
||||
from utils.config import BACKEND_URL, BROWSER_TYPE, HEADLESS_MODE
|
||||
@@ -0,0 +1,512 @@
|
||||
"""
|
||||
Cookie监控管理系统 - 检测客户端
|
||||
|
||||
前端启动时,不需要登录后端,直接读取所有用户需要登录的网站,
|
||||
然后分用户、分网站登录。
|
||||
"""
|
||||
import time
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
from DrissionPage import ChromiumPage, ChromiumOptions
|
||||
from config.config import BACKEND_URL, BROWSER_TYPE, HEADLESS_MODE, PAGE_LOAD_TIMEOUT
|
||||
|
||||
|
||||
class DetectionClient:
|
||||
"""检测客户端"""
|
||||
|
||||
def __init__(self):
|
||||
self.backend_url = BACKEND_URL
|
||||
self.browser = None
|
||||
self.failed_websites = [] # 记录失败的网站
|
||||
|
||||
def init_browser(self):
|
||||
"""初始化浏览器"""
|
||||
co = ChromiumOptions()
|
||||
if BROWSER_TYPE == 'edge':
|
||||
co.set_browser_path(r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe")
|
||||
co.set_argument('--no-sandbox')
|
||||
co.set_argument('--disable-dev-shm-usage')
|
||||
if HEADLESS_MODE:
|
||||
co.set_argument('--headless')
|
||||
self.browser = ChromiumPage(addr_or_opts=co)
|
||||
print("浏览器初始化成功")
|
||||
|
||||
def close_browser(self):
|
||||
"""关闭浏览器"""
|
||||
if self.browser:
|
||||
try:
|
||||
self.browser.quit()
|
||||
except Exception as e:
|
||||
print(f"关闭浏览器失败: {e}")
|
||||
finally:
|
||||
self.browser = None
|
||||
|
||||
def get_all_users(self) -> List[Dict]:
|
||||
"""获取所有用户"""
|
||||
try:
|
||||
response = requests.get(f"{self.backend_url}/api/users")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
print(f"获取用户失败: {e}")
|
||||
return []
|
||||
|
||||
def get_user_websites(self, user_id: int) -> List[Dict]:
|
||||
"""获取用户的网站列表"""
|
||||
try:
|
||||
response = requests.get(f"{self.backend_url}/api/websites?user_id={user_id}")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
print(f"获取用户 {user_id} 的网站失败: {e}")
|
||||
return []
|
||||
|
||||
def get_website_cookies(self, website_id: int) -> List[Dict]:
|
||||
"""获取网站的cookie"""
|
||||
try:
|
||||
response = requests.get(f"{self.backend_url}/api/cookies/websites/{website_id}")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
print(f"获取网站 {website_id} 的cookie失败: {e}")
|
||||
return []
|
||||
|
||||
def update_website_cookies(self, website_id: int, cookies: List[Dict]):
|
||||
"""更新网站的cookie"""
|
||||
try:
|
||||
response = requests.put(
|
||||
f"{self.backend_url}/api/cookies/websites/{website_id}",
|
||||
json={'cookies': cookies}
|
||||
)
|
||||
response.raise_for_status()
|
||||
print(f"网站 {website_id} 的cookie已更新")
|
||||
except requests.RequestException as e:
|
||||
print(f"更新网站 {website_id} 的cookie失败: {e}")
|
||||
|
||||
def save_detection_result(self, website_id: int, status: int, response_time: float,
|
||||
http_status: int, message: str):
|
||||
"""保存检测结果"""
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.backend_url}/api/detections",
|
||||
json={
|
||||
'website_id': website_id,
|
||||
'status': status,
|
||||
'response_time': response_time,
|
||||
'http_status': http_status,
|
||||
'message': message
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
print(f"保存检测结果失败: {e}")
|
||||
|
||||
def send_notification(self, user_id: int, website_id: int, title: str, content: str):
|
||||
"""发送通知"""
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.backend_url}/api/notifications",
|
||||
json={
|
||||
'user_id': user_id,
|
||||
'website_id': website_id,
|
||||
'title': title,
|
||||
'content': content
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
print(f"通知已发送")
|
||||
except requests.RequestException as e:
|
||||
print(f"发送通知失败: {e}")
|
||||
|
||||
def import_cookies_to_browser(self, cookies: List[Dict], domain: str) -> int:
|
||||
"""导入cookies到浏览器"""
|
||||
if not self.browser or not cookies:
|
||||
return 0
|
||||
|
||||
success_count = 0
|
||||
for cookie in cookies:
|
||||
try:
|
||||
if not cookie.get('name') or not cookie.get('value'):
|
||||
continue
|
||||
|
||||
cookie_dict = {
|
||||
'name': cookie.get('name'),
|
||||
'value': cookie.get('value'),
|
||||
'domain': cookie.get('domain', domain),
|
||||
'path': cookie.get('path', '/')
|
||||
}
|
||||
|
||||
self.browser.set.cookies(cookie_dict)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
print(f"导入cookie失败: {e}")
|
||||
|
||||
return success_count
|
||||
|
||||
def verify_login(self, url: str, check_selector: str = "", success_text: str = "") -> bool:
|
||||
"""
|
||||
验证登录状态
|
||||
|
||||
Args:
|
||||
url: 网站地址
|
||||
check_selector: 登录检测选择器
|
||||
success_text: 登录成功时显示的文本
|
||||
|
||||
Returns:
|
||||
是否已登录
|
||||
"""
|
||||
if not self.browser:
|
||||
return False
|
||||
|
||||
try:
|
||||
self.browser.get(url)
|
||||
self.browser.wait.doc_loaded(timeout=PAGE_LOAD_TIMEOUT)
|
||||
time.sleep(3)
|
||||
|
||||
# 刷新页面几次确保登录状态
|
||||
for i in range(3):
|
||||
time.sleep(3)
|
||||
self.browser.refresh()
|
||||
self.browser.wait.doc_loaded(timeout=PAGE_LOAD_TIMEOUT)
|
||||
time.sleep(3)
|
||||
|
||||
if not check_selector and not success_text:
|
||||
return False
|
||||
|
||||
# 检查选择器
|
||||
if check_selector:
|
||||
try:
|
||||
element = self.browser.ele(check_selector, timeout=10)
|
||||
if element:
|
||||
element_text = element.text or ""
|
||||
if not success_text:
|
||||
return True
|
||||
if success_text in element_text:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
# 检查成功文本
|
||||
if success_text:
|
||||
try:
|
||||
page_text = self.browser.html or ""
|
||||
if success_text in page_text:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"验证登录状态失败: {e}")
|
||||
return False
|
||||
|
||||
def get_current_cookies(self) -> List[Dict]:
|
||||
"""获取当前浏览器的所有cookies"""
|
||||
if not self.browser:
|
||||
return []
|
||||
|
||||
try:
|
||||
return self.browser.cookies()
|
||||
except Exception as e:
|
||||
print(f"获取cookies失败: {e}")
|
||||
return []
|
||||
|
||||
def check_website(self, user: Dict, website: Dict) -> bool:
|
||||
"""
|
||||
检测单个网站
|
||||
|
||||
Args:
|
||||
user: 用户信息
|
||||
website: 网站信息
|
||||
|
||||
Returns:
|
||||
是否登录成功
|
||||
"""
|
||||
website_id = website.get('id')
|
||||
website_name = website.get('name')
|
||||
url = website.get('url')
|
||||
check_selector = website.get('login_check_selector', '')
|
||||
success_text = website.get('success_text', '')
|
||||
|
||||
print(f"\n检测网站: {website_name} ({url})")
|
||||
print(f"用户: {user.get('name')}")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# 获取网站的cookie
|
||||
cookies = self.get_website_cookies(website_id)
|
||||
if not cookies:
|
||||
print(f" 警告: 未找到网站 {website_name} 的cookie")
|
||||
self.save_detection_result(
|
||||
website_id=website_id,
|
||||
status=0,
|
||||
response_time=time.time() - start_time,
|
||||
http_status=0,
|
||||
message="未找到cookie"
|
||||
)
|
||||
return False
|
||||
|
||||
# 导入cookie到浏览器
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(url)
|
||||
domain = parsed.netloc
|
||||
self.import_cookies_to_browser(cookies, domain)
|
||||
|
||||
# 访问网站
|
||||
self.browser.get(url)
|
||||
self.browser.wait.doc_loaded(timeout=PAGE_LOAD_TIMEOUT)
|
||||
time.sleep(3)
|
||||
|
||||
# 验证登录状态,如果未登录则以30秒为周期连续检测6次
|
||||
login_success = self._check_login_with_retry(url, check_selector, success_text)
|
||||
response_time = time.time() - start_time
|
||||
|
||||
if login_success:
|
||||
print(f" ✓ 登录验证成功")
|
||||
|
||||
# 获取最新cookie并保存到后台
|
||||
current_cookies = self.get_current_cookies()
|
||||
if current_cookies:
|
||||
self.update_website_cookies(website_id, current_cookies)
|
||||
print(f" 已更新cookie ({len(current_cookies)} 个)")
|
||||
|
||||
# 保存检测结果
|
||||
self.save_detection_result(
|
||||
website_id=website_id,
|
||||
status=1,
|
||||
response_time=response_time,
|
||||
http_status=200,
|
||||
message="登录验证成功"
|
||||
)
|
||||
return True
|
||||
else:
|
||||
print(f" ✗ 登录验证失败(连续6次检测未登录)")
|
||||
|
||||
# 记录失败信息
|
||||
self.failed_websites.append({
|
||||
'user': user,
|
||||
'website': website,
|
||||
'reason': '登录验证失败(连续6次检测未登录)'
|
||||
})
|
||||
|
||||
# 保存检测结果
|
||||
self.save_detection_result(
|
||||
website_id=website_id,
|
||||
status=0,
|
||||
response_time=response_time,
|
||||
http_status=200,
|
||||
message="登录验证失败(连续6次检测未登录)"
|
||||
)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
response_time = time.time() - start_time
|
||||
print(f" ✗ 检测异常: {e}")
|
||||
|
||||
# 记录失败信息
|
||||
self.failed_websites.append({
|
||||
'user': user,
|
||||
'website': website,
|
||||
'reason': str(e)
|
||||
})
|
||||
|
||||
# 保存检测结果
|
||||
self.save_detection_result(
|
||||
website_id=website_id,
|
||||
status=0,
|
||||
response_time=response_time,
|
||||
http_status=0,
|
||||
message=f"检测异常: {e}"
|
||||
)
|
||||
return False
|
||||
|
||||
def _check_login_with_retry(self, url: str, check_selector: str, success_text: str,
|
||||
retry_count: int = 6, retry_interval: int = 30) -> bool:
|
||||
"""
|
||||
检查登录状态,如果未登录则重复检测
|
||||
|
||||
Args:
|
||||
url: 网站地址
|
||||
check_selector: 登录检测选择器
|
||||
success_text: 登录成功文本
|
||||
retry_count: 最大重试次数
|
||||
retry_interval: 重试间隔(秒)
|
||||
|
||||
Returns:
|
||||
是否登录成功
|
||||
"""
|
||||
# 第一次检测
|
||||
if self._is_logged_in(check_selector, success_text):
|
||||
return True
|
||||
|
||||
print(f" 首次检测未登录,开始周期性检测(最多{retry_count}次,间隔{retry_interval}秒)")
|
||||
|
||||
for i in range(1, retry_count + 1):
|
||||
print(f" 第 {i}/{retry_count} 次检测...")
|
||||
time.sleep(retry_interval)
|
||||
|
||||
# 刷新页面
|
||||
self.browser.refresh()
|
||||
self.browser.wait.doc_loaded(timeout=PAGE_LOAD_TIMEOUT)
|
||||
time.sleep(3)
|
||||
|
||||
if self._is_logged_in(check_selector, success_text):
|
||||
print(f" 第 {i} 次检测登录成功")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _is_logged_in(self, check_selector: str, success_text: str) -> bool:
|
||||
"""
|
||||
判断是否已登录
|
||||
|
||||
Returns:
|
||||
是否已登录
|
||||
"""
|
||||
if not check_selector and not success_text:
|
||||
return False
|
||||
|
||||
# 检查选择器
|
||||
if check_selector:
|
||||
try:
|
||||
element = self.browser.ele(check_selector, timeout=10)
|
||||
if element:
|
||||
element_text = element.text or ""
|
||||
if not success_text:
|
||||
return True
|
||||
if success_text in element_text:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
# 检查成功文本
|
||||
if success_text:
|
||||
try:
|
||||
page_text = self.browser.html or ""
|
||||
if success_text in page_text:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
def send_failed_notifications(self):
|
||||
"""发送失败网站的通知"""
|
||||
if not self.failed_websites:
|
||||
return
|
||||
|
||||
print(f"\n发现 {len(self.failed_websites)} 个失败的网站,发送通知")
|
||||
|
||||
# 按用户分组
|
||||
user_notifications = {}
|
||||
for failed in self.failed_websites:
|
||||
user_id = failed['user'].get('id')
|
||||
if user_id not in user_notifications:
|
||||
user_notifications[user_id] = {
|
||||
'user': failed['user'],
|
||||
'websites': []
|
||||
}
|
||||
user_notifications[user_id]['websites'].append(failed)
|
||||
|
||||
# 为每个用户发送通知
|
||||
for user_id, notif_data in user_notifications.items():
|
||||
user = notif_data['user']
|
||||
websites = notif_data['websites']
|
||||
|
||||
# 构建通知内容
|
||||
content_lines = [
|
||||
f"**{user.get('name')} Cookie登录报告**",
|
||||
f"",
|
||||
f"检测时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
f"失败数量: {len(websites)} 个网站",
|
||||
f"",
|
||||
"失败详情:"
|
||||
]
|
||||
|
||||
for failed in websites:
|
||||
website = failed['website']
|
||||
reason = failed['reason']
|
||||
content_lines.append(f"- {website.get('name')}: {reason}")
|
||||
|
||||
content = "\n".join(content_lines)
|
||||
|
||||
# 发送通知
|
||||
if websites:
|
||||
self.send_notification(
|
||||
user_id=user_id,
|
||||
website_id=websites[0]['website'].get('id'),
|
||||
title=f"Cookie登录失败 ({len(websites)} 个网站)",
|
||||
content=content
|
||||
)
|
||||
|
||||
def run_detection_cycle(self):
|
||||
"""执行一轮检测"""
|
||||
print(f"\n{'='*50}")
|
||||
print(f"开始执行检测任务 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f"{'='*50}")
|
||||
|
||||
self.failed_websites = []
|
||||
|
||||
# 初始化浏览器
|
||||
self.init_browser()
|
||||
|
||||
try:
|
||||
# 获取所有用户
|
||||
users = self.get_all_users()
|
||||
if not users:
|
||||
print("未找到用户,跳过检测")
|
||||
return
|
||||
|
||||
print(f"找到 {len(users)} 个用户")
|
||||
|
||||
# 分用户处理
|
||||
for user in users:
|
||||
print(f"\n处理用户: {user.get('name')}")
|
||||
|
||||
# 获取用户的网站
|
||||
websites = self.get_user_websites(user.get('id'))
|
||||
if not websites:
|
||||
print(f" 用户 {user.get('name')} 没有网站,跳过")
|
||||
continue
|
||||
|
||||
print(f" 找到 {len(websites)} 个网站")
|
||||
|
||||
# 分网站处理
|
||||
for website in websites:
|
||||
if website.get('status', 1) != 1:
|
||||
print(f" 网站 {website.get('name')} 已禁用,跳过")
|
||||
continue
|
||||
|
||||
self.check_website(user, website)
|
||||
|
||||
# 发送失败通知
|
||||
self.send_failed_notifications()
|
||||
|
||||
except Exception as e:
|
||||
print(f"检测任务执行异常: {e}")
|
||||
finally:
|
||||
self.close_browser()
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"检测任务完成 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f"{'='*50}")
|
||||
|
||||
def run(self):
|
||||
"""运行检测客户端"""
|
||||
print("Cookie监控检测客户端启动")
|
||||
|
||||
# 执行一轮检测
|
||||
self.run_detection_cycle()
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
client = DetectionClient()
|
||||
client.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
配置文件
|
||||
"""
|
||||
# 后台服务地址
|
||||
BACKEND_URL = "http://localhost:5000"
|
||||
|
||||
# 浏览器配置
|
||||
BROWSER_TYPE = "edge" # edge 或 chrome
|
||||
HEADLESS_MODE = False # 是否无头模式
|
||||
|
||||
# 检测间隔(秒)
|
||||
CHECK_INTERVAL = 30
|
||||
|
||||
# 页面加载超时时间(秒)
|
||||
PAGE_LOAD_TIMEOUT = 30
|
||||
@@ -1,62 +0,0 @@
|
||||
"""日志管理模块"""
|
||||
import os
|
||||
import logging
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
|
||||
def setup_logging(
|
||||
log_dir: str = "log",
|
||||
log_file: str = "app.log",
|
||||
level: int = logging.INFO,
|
||||
format_string: str = '%(asctime)s - %(levelname)s - %(message)s',
|
||||
when: str = "H",
|
||||
interval: int = 12,
|
||||
backup_count: int = 7
|
||||
) -> None:
|
||||
"""
|
||||
配置日志系统
|
||||
|
||||
Args:
|
||||
log_dir: 日志目录
|
||||
log_file: 日志文件名
|
||||
level: 日志级别
|
||||
format_string: 日志格式
|
||||
when: 轮转时间单位 (S:秒, M:分, H:时, D:天, W:周)
|
||||
interval: 轮转间隔
|
||||
backup_count: 保留的备份文件数量
|
||||
"""
|
||||
# 确保日志目录存在
|
||||
if not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir)
|
||||
|
||||
# 配置日志处理器
|
||||
handlers = [
|
||||
TimedRotatingFileHandler(
|
||||
os.path.join(log_dir, log_file),
|
||||
when=when,
|
||||
interval=interval,
|
||||
backupCount=backup_count,
|
||||
encoding='utf-8'
|
||||
),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=level,
|
||||
format=format_string,
|
||||
handlers=handlers
|
||||
)
|
||||
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
"""
|
||||
获取日志记录器
|
||||
|
||||
Args:
|
||||
name: 日志记录器名称,通常使用 __name__
|
||||
|
||||
Returns:
|
||||
Logger 实例
|
||||
"""
|
||||
return logging.getLogger(name)
|
||||
-480
@@ -1,480 +0,0 @@
|
||||
"""Cookie 监控主程序"""
|
||||
import time
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse
|
||||
from cookie_cloud import CookieCloud
|
||||
from notification import create_notification_manager
|
||||
from config_manager import ConfigManager, CookieDataManager
|
||||
from browser_manager import BrowserManager
|
||||
from logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
NOTIFY_TIMES = [6, 21]
|
||||
STATE_FILE = "notify_state.json"
|
||||
|
||||
|
||||
def load_notify_state() -> dict:
|
||||
"""加载通知状态"""
|
||||
import json
|
||||
import os
|
||||
if os.path.exists(STATE_FILE):
|
||||
try:
|
||||
with open(STATE_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def save_notify_state(state: dict):
|
||||
"""保存通知状态"""
|
||||
import json
|
||||
try:
|
||||
with open(STATE_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(state, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"保存通知状态失败: {e}")
|
||||
|
||||
|
||||
def should_send_notification(user_name: str) -> bool:
|
||||
"""判断是否应该发送通知"""
|
||||
# 去掉时间限制,每次都发送通知
|
||||
return True
|
||||
|
||||
|
||||
def mark_notification_sent(user_name: str):
|
||||
"""标记通知已发送"""
|
||||
now = datetime.now()
|
||||
state = load_notify_state()
|
||||
|
||||
if user_name not in state:
|
||||
state[user_name] = {}
|
||||
|
||||
state[user_name]['date'] = now.strftime('%Y-%m-%d')
|
||||
state[user_name]['hour'] = now.hour
|
||||
|
||||
save_notify_state(state)
|
||||
|
||||
|
||||
class CookieMonitor:
|
||||
"""Cookie 监控器"""
|
||||
|
||||
def __init__(self, config_file: str = "config.json"):
|
||||
"""
|
||||
初始化监控器
|
||||
|
||||
Args:
|
||||
config_file: 配置文件路径
|
||||
"""
|
||||
self.config_manager = ConfigManager(config_file)
|
||||
self.cookie_data_manager = CookieDataManager()
|
||||
self.all_results = []
|
||||
|
||||
def process_user(self, user_config: dict):
|
||||
"""
|
||||
处理单个用户:依次验证各网站登录状态
|
||||
|
||||
Args:
|
||||
user_config: 用户配置字典
|
||||
"""
|
||||
user_name = user_config.get('name', '未知用户')
|
||||
logger.info(f"开始处理用户: {user_name}")
|
||||
|
||||
websites = user_config.get('websites', [])
|
||||
|
||||
if not websites:
|
||||
logger.info(f"用户 {user_name} 没有配置网站,跳过")
|
||||
return
|
||||
|
||||
browser_config = user_config.get('browser', {})
|
||||
browser = BrowserManager(
|
||||
browser_type=browser_config.get('type', 'edge'),
|
||||
headless=browser_config.get('headless', False)
|
||||
)
|
||||
|
||||
user_results = {
|
||||
'user_name': user_name,
|
||||
'notification_config': user_config.get('notification', {}),
|
||||
'websites': [],
|
||||
'success_count': 0,
|
||||
'fail_count': 0
|
||||
}
|
||||
|
||||
try:
|
||||
local_cookies = self.cookie_data_manager.get_user_cookies(user_name)
|
||||
|
||||
cookie_cloud_config = user_config.get('cookie_cloud', {})
|
||||
cookie_cloud = CookieCloud(
|
||||
api_url=cookie_cloud_config.get('api_url', ''),
|
||||
uuid=cookie_cloud_config.get('uuid', ''),
|
||||
password=cookie_cloud_config.get('password', '')
|
||||
)
|
||||
all_cookies = cookie_cloud.get_cookies()
|
||||
|
||||
browser.init_browser()
|
||||
|
||||
login_failed = False
|
||||
failed_website = None
|
||||
|
||||
# 初始流程:逐个网站验证,使用本地cookie
|
||||
for website in websites:
|
||||
website_name = website.get('name', '未知网站')
|
||||
url = website.get('url', '')
|
||||
parsed = urlparse(url)
|
||||
domain = parsed.netloc
|
||||
|
||||
domain_cookies = []
|
||||
for cookie_domain, cookie_data in local_cookies.items():
|
||||
clean_domain = cookie_domain.lstrip('.')
|
||||
if domain in clean_domain or clean_domain in domain:
|
||||
if isinstance(cookie_data, dict):
|
||||
cookies = cookie_data.get('cookies', [])
|
||||
elif isinstance(cookie_data, list):
|
||||
cookies = cookie_data
|
||||
else:
|
||||
cookies = []
|
||||
domain_cookies.extend(cookies)
|
||||
|
||||
if domain_cookies:
|
||||
success_count = browser.import_cookies(domain_cookies, domain)
|
||||
|
||||
login_check_selector = website.get('login_check_selector', '')
|
||||
success_text = website.get('success_text', '')
|
||||
|
||||
login_success = browser.verify_login(
|
||||
url=url,
|
||||
check_selector=login_check_selector,
|
||||
success_text=success_text
|
||||
)
|
||||
|
||||
if login_success:
|
||||
current_cookies = browser.get_cookies()
|
||||
if current_cookies:
|
||||
self.cookie_data_manager.save_user_cookies(
|
||||
user_name=user_name,
|
||||
cookies=current_cookies,
|
||||
url=url,
|
||||
website_name=website_name
|
||||
)
|
||||
|
||||
user_results['websites'].append({
|
||||
'name': website_name,
|
||||
'url': url,
|
||||
'success': True,
|
||||
'error': ''
|
||||
})
|
||||
user_results['success_count'] += 1
|
||||
else:
|
||||
login_failed = True
|
||||
failed_website = website
|
||||
break
|
||||
|
||||
# 错误处理机制:如果任何一个网站登录失败
|
||||
if login_failed:
|
||||
browser.close()
|
||||
|
||||
browser = BrowserManager(
|
||||
browser_type=browser_config.get('type', 'edge'),
|
||||
headless=browser_config.get('headless', False)
|
||||
)
|
||||
browser.init_browser()
|
||||
|
||||
relevant_cookies = []
|
||||
|
||||
target_domains = []
|
||||
for website in websites:
|
||||
url = website.get('url', '')
|
||||
if url:
|
||||
parsed = urlparse(url)
|
||||
target_domains.append(parsed.netloc)
|
||||
|
||||
for domain_cookies in all_cookies.values():
|
||||
if isinstance(domain_cookies, dict):
|
||||
cookies = domain_cookies.get('cookies', [])
|
||||
elif isinstance(domain_cookies, list):
|
||||
cookies = domain_cookies
|
||||
else:
|
||||
cookies = []
|
||||
|
||||
for cookie in cookies:
|
||||
cookie_domain = cookie.get('domain', '')
|
||||
if cookie_domain:
|
||||
clean_cookie_domain = cookie_domain.lstrip('.')
|
||||
for target_domain in target_domains:
|
||||
clean_target_domain = target_domain.lstrip('.')
|
||||
if clean_cookie_domain in clean_target_domain or clean_target_domain in clean_cookie_domain:
|
||||
relevant_cookies.append(cookie)
|
||||
break
|
||||
|
||||
if relevant_cookies:
|
||||
browser.import_cookies(relevant_cookies, domain='')
|
||||
|
||||
user_results['websites'] = []
|
||||
user_results['success_count'] = 0
|
||||
user_results['fail_count'] = 0
|
||||
|
||||
for website in websites:
|
||||
website_name = website.get('name', '未知网站')
|
||||
url = website.get('url', '')
|
||||
login_check_selector = website.get('login_check_selector', '')
|
||||
success_text = website.get('success_text', '')
|
||||
|
||||
login_success = browser.verify_login(
|
||||
url=url,
|
||||
check_selector=login_check_selector,
|
||||
success_text=success_text
|
||||
)
|
||||
|
||||
if login_success:
|
||||
current_cookies = browser.get_cookies()
|
||||
if current_cookies:
|
||||
self.cookie_data_manager.save_user_cookies(
|
||||
user_name=user_name,
|
||||
cookies=current_cookies,
|
||||
url=url,
|
||||
website_name=website_name
|
||||
)
|
||||
|
||||
user_results['websites'].append({
|
||||
'name': website_name,
|
||||
'url': url,
|
||||
'success': True,
|
||||
'error': ''
|
||||
})
|
||||
user_results['success_count'] += 1
|
||||
else:
|
||||
user_results['websites'].append({
|
||||
'name': website_name,
|
||||
'url': url,
|
||||
'success': False,
|
||||
'error': '登录状态验证失败'
|
||||
})
|
||||
user_results['fail_count'] += 1
|
||||
|
||||
self.all_results.append(user_results)
|
||||
self._print_summary(user_name, user_results['websites'])
|
||||
|
||||
if should_send_notification(user_name):
|
||||
self._send_user_notification(user_results)
|
||||
|
||||
finally:
|
||||
browser.close()
|
||||
|
||||
def process_website(
|
||||
self,
|
||||
user_name: str,
|
||||
website_config: dict,
|
||||
browser: BrowserManager,
|
||||
all_cookies: dict,
|
||||
local_cookies: dict
|
||||
) -> dict:
|
||||
"""
|
||||
验证网站登录状态
|
||||
|
||||
Args:
|
||||
user_name: 用户名
|
||||
website_config: 网站配置
|
||||
browser: 浏览器管理器
|
||||
all_cookies: 所有cookies
|
||||
local_cookies: 本地cookies
|
||||
|
||||
Returns:
|
||||
验证结果字典
|
||||
"""
|
||||
website_name = website_config.get('name', '未知网站')
|
||||
url = website_config.get('url', '')
|
||||
check_selector = website_config.get('login_check_selector', '')
|
||||
success_text = website_config.get('success_text', '')
|
||||
|
||||
logger.info(f"\n验证网站: {website_name}")
|
||||
logger.info(f" URL: {url}")
|
||||
|
||||
result = {
|
||||
'name': website_name,
|
||||
'url': url,
|
||||
'success': False,
|
||||
'error': ''
|
||||
}
|
||||
|
||||
if not url:
|
||||
result['error'] = 'URL 未配置'
|
||||
logger.error(f" 失败: {result['error']}")
|
||||
return result
|
||||
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
domain = parsed.netloc
|
||||
|
||||
domain_cookies = []
|
||||
for cookie_domain, cookie_data in local_cookies.items():
|
||||
clean_domain = cookie_domain.lstrip('.')
|
||||
if domain in clean_domain or clean_domain in domain:
|
||||
if isinstance(cookie_data, dict):
|
||||
cookies = cookie_data.get('cookies', [])
|
||||
elif isinstance(cookie_data, list):
|
||||
cookies = cookie_data
|
||||
else:
|
||||
cookies = []
|
||||
domain_cookies.extend(cookies)
|
||||
|
||||
if domain_cookies:
|
||||
logger.info(f" 找到 {len(domain_cookies)} 个本地 cookie")
|
||||
success_count = browser.import_cookies(domain_cookies, domain)
|
||||
logger.info(f" 成功导入 {success_count} 个 cookie")
|
||||
else:
|
||||
logger.warning(f" 未找到本地 cookie")
|
||||
|
||||
login_success = browser.verify_login(
|
||||
url=url,
|
||||
check_selector=check_selector,
|
||||
success_text=success_text
|
||||
)
|
||||
|
||||
if login_success:
|
||||
result['success'] = True
|
||||
logger.info(f" ✓ 使用本地 cookie 登录验证成功")
|
||||
|
||||
if domain_cookies:
|
||||
logger.info(f" 保存 {len(domain_cookies)} 个 cookie 到本地文件")
|
||||
self.cookie_data_manager.save_user_cookies(
|
||||
user_name=user_name,
|
||||
cookies=domain_cookies,
|
||||
url=url,
|
||||
website_name=website_name
|
||||
)
|
||||
else:
|
||||
logger.warning(f" 登录验证成功但 cookies 为空,跳过保存")
|
||||
else:
|
||||
logger.warning(f" ✗ 本地 cookie 登录失败,尝试使用服务器 cookie")
|
||||
|
||||
domain_cookies = []
|
||||
for cookie_domain, cookie_data in all_cookies.items():
|
||||
clean_domain = cookie_domain.lstrip('.')
|
||||
if domain in clean_domain or clean_domain in domain:
|
||||
if isinstance(cookie_data, dict):
|
||||
cookies = cookie_data.get('cookies', [])
|
||||
elif isinstance(cookie_data, list):
|
||||
cookies = cookie_data
|
||||
else:
|
||||
cookies = []
|
||||
domain_cookies.extend(cookies)
|
||||
|
||||
if domain_cookies:
|
||||
logger.info(f" 找到 {len(domain_cookies)} 个服务器 cookie")
|
||||
success_count = browser.import_cookies(domain_cookies, domain)
|
||||
logger.info(f" 成功导入 {success_count} 个 cookie")
|
||||
else:
|
||||
logger.warning(f" 未找到服务器 cookie")
|
||||
|
||||
login_success = browser.verify_login(
|
||||
url=url,
|
||||
check_selector=check_selector,
|
||||
success_text=success_text
|
||||
)
|
||||
|
||||
if login_success:
|
||||
result['success'] = True
|
||||
logger.info(f" ✓ 使用服务器 cookie 登录验证成功")
|
||||
|
||||
if domain_cookies:
|
||||
logger.info(f" 保存 {len(domain_cookies)} 个 cookie 到本地文件")
|
||||
self.cookie_data_manager.save_user_cookies(
|
||||
user_name=user_name,
|
||||
cookies=domain_cookies,
|
||||
url=url,
|
||||
website_name=website_name
|
||||
)
|
||||
else:
|
||||
logger.warning(f" 登录验证成功但 cookies 为空,跳过保存")
|
||||
else:
|
||||
result['error'] = '登录状态验证失败(本地和服务器 cookie 都无效)'
|
||||
logger.warning(f" ✗ {result['error']}")
|
||||
|
||||
except Exception as e:
|
||||
result['error'] = f'处理异常: {str(e)}'
|
||||
logger.error(f" 异常: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def _print_summary(self, user_name: str, results: list):
|
||||
"""打印处理结果汇总"""
|
||||
success_count = sum(1 for r in results if r['success'])
|
||||
fail_count = len(results) - success_count
|
||||
|
||||
logger.info(f"\n{'='*20} 用户 {user_name} 处理结果汇总 {'='*20}")
|
||||
logger.info(f"总网站数: {len(results)}")
|
||||
logger.info(f"成功: {success_count}, 失败: {fail_count}")
|
||||
|
||||
if fail_count > 0:
|
||||
logger.info("\n失败详情:")
|
||||
for r in results:
|
||||
if not r['success']:
|
||||
logger.info(f" - {r['name']}: {r['error']}")
|
||||
|
||||
def _send_user_notification(self, user_result: dict):
|
||||
"""发送单个用户的通知"""
|
||||
user_name = user_result['user_name']
|
||||
notification_config = user_result.get('notification_config', {})
|
||||
|
||||
notifier = create_notification_manager(notification_config)
|
||||
|
||||
if not notifier:
|
||||
return
|
||||
|
||||
success_count = user_result['success_count']
|
||||
fail_count = user_result['fail_count']
|
||||
|
||||
title = f"Cookie登录"
|
||||
|
||||
content_lines = [
|
||||
f"**{user_name} Cookie登录报告**",
|
||||
f"",
|
||||
f"登录时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
f"成功: {success_count} 个网站",
|
||||
f"失败: {fail_count} 个网站",
|
||||
f"",
|
||||
]
|
||||
|
||||
for website in user_result['websites']:
|
||||
status = "成功" if website['success'] else f"失败: {website['error']}"
|
||||
content_lines.append(f"- {website['name']}: {status}")
|
||||
|
||||
content = "\n".join(content_lines)
|
||||
|
||||
if notifier.send(title, content):
|
||||
mark_notification_sent(user_name)
|
||||
else:
|
||||
logger.error(f"用户 {user_name} 通知发送失败")
|
||||
|
||||
|
||||
|
||||
def run(self):
|
||||
"""运行监控"""
|
||||
logger.info("Cookie 监控开始")
|
||||
|
||||
users = self.config_manager.get_users()
|
||||
|
||||
if not users:
|
||||
logger.error("未找到用户配置")
|
||||
return
|
||||
|
||||
for user_config in users:
|
||||
try:
|
||||
self.process_user(user_config)
|
||||
except Exception as e:
|
||||
logger.error(f"处理用户失败: {e}")
|
||||
|
||||
logger.info("Cookie 监控结束")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
from logger import setup_logging
|
||||
setup_logging()
|
||||
monitor = CookieMonitor()
|
||||
monitor.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,44 +0,0 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['monitor.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='monitor',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='monitor',
|
||||
)
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
"""信息发送管理模块"""
|
||||
import requests
|
||||
from logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class NotificationManager:
|
||||
"""通知管理器"""
|
||||
|
||||
def __init__(self, token: str = "", provider: str = "iyuu"):
|
||||
"""
|
||||
初始化通知管理器
|
||||
|
||||
Args:
|
||||
token: 通知token
|
||||
provider: 通知服务提供商 (iyuu, wechat, dingtalk等)
|
||||
"""
|
||||
self.token = token
|
||||
self.provider = provider
|
||||
self.api_url = f"https://iyuu.cn/{token}.send" if provider == "iyuu" else ""
|
||||
|
||||
def send(self, title: str, content: str) -> bool:
|
||||
"""
|
||||
发送通知
|
||||
|
||||
Args:
|
||||
title: 通知标题
|
||||
content: 通知内容
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
if not self.token:
|
||||
logger.warning("未配置通知token,跳过发送")
|
||||
return False
|
||||
|
||||
if self.provider == "iyuu":
|
||||
return self._send_iyuu(title, content)
|
||||
else:
|
||||
logger.warning(f"不支持的通知提供商: {self.provider}")
|
||||
return False
|
||||
|
||||
def _send_iyuu(self, title: str, content: str) -> bool:
|
||||
"""
|
||||
通过爱语飞飞发送通知
|
||||
|
||||
Args:
|
||||
title: 通知标题
|
||||
content: 通知内容
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
try:
|
||||
logger.info(f"发送通知: {title}")
|
||||
|
||||
response = requests.post(
|
||||
self.api_url,
|
||||
json={
|
||||
'text': title,
|
||||
'desp': content
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('errcode') == 0:
|
||||
logger.info("通知发送成功")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"通知发送失败: {result.get('errmsg')}")
|
||||
return False
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"通知请求失败: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"通知发送异常: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def create_notification_manager(config: dict) -> NotificationManager:
|
||||
"""
|
||||
根据配置创建通知管理器
|
||||
|
||||
Args:
|
||||
config: 通知配置字典
|
||||
|
||||
Returns:
|
||||
通知管理器实例
|
||||
"""
|
||||
token = config.get('iyuu_token', '')
|
||||
provider = config.get('provider', 'iyuu')
|
||||
|
||||
if token:
|
||||
return NotificationManager(token=token, provider=provider)
|
||||
|
||||
return None
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"彭峰(个人)": {
|
||||
"date": "2026-03-08",
|
||||
"hour": 9
|
||||
},
|
||||
"彭峰(公司)": {
|
||||
"date": "2026-03-08",
|
||||
"hour": 9
|
||||
},
|
||||
"小余": {
|
||||
"date": "2026-03-08",
|
||||
"hour": 8
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
DrissionPage
|
||||
requests
|
||||
pycryptodome
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
更新用户Cookie Cloud参数
|
||||
"""
|
||||
import json
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
DB_PATH = os.path.join(os.path.dirname(__file__), 'backend', 'instance', 'cookie_monitor.db')
|
||||
|
||||
def update_user_cookie_cloud():
|
||||
"""更新用户Cookie Cloud参数"""
|
||||
with open('config.json', 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 添加新列(如果不存在)
|
||||
columns_to_add = [
|
||||
('cookie_cloud_uuid', 'VARCHAR(255)'),
|
||||
('cookie_cloud_password', 'VARCHAR(255)'),
|
||||
('cookie_cloud_api_url', 'VARCHAR(255)')
|
||||
]
|
||||
|
||||
for col_name, col_type in columns_to_add:
|
||||
try:
|
||||
cursor.execute(f"ALTER TABLE users ADD COLUMN {col_name} {col_type}")
|
||||
print(f"添加列: {col_name}")
|
||||
except sqlite3.OperationalError as e:
|
||||
if 'duplicate column name' in str(e).lower():
|
||||
print(f"列已存在: {col_name}")
|
||||
else:
|
||||
raise
|
||||
|
||||
for user_config in config['users']:
|
||||
user_name = user_config['name']
|
||||
cc = user_config.get('cookie_cloud', {})
|
||||
|
||||
cursor.execute(
|
||||
"UPDATE users SET cookie_cloud_uuid = ?, cookie_cloud_password = ?, cookie_cloud_api_url = ? WHERE name = ?",
|
||||
(
|
||||
cc.get('uuid', ''),
|
||||
cc.get('password', ''),
|
||||
cc.get('api_url', ''),
|
||||
user_name
|
||||
)
|
||||
)
|
||||
print(f"更新用户 {user_name}: UUID={cc.get('uuid', '')}, API={cc.get('api_url', '')}")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("\n更新完成!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
update_user_cookie_cloud()
|
||||
@@ -1,314 +0,0 @@
|
||||
# Cookie 自动监控功能需求文档
|
||||
|
||||
## 1. 功能概述
|
||||
|
||||
本系统提供 Cookie 自动监控、管理和通知功能,确保用户能够持续访问多个网站。
|
||||
|
||||
## 2. 详细功能需求
|
||||
|
||||
### 2.1 Cookie Cloud 集成
|
||||
|
||||
#### 2.1.1 从 Cookie Cloud 获取 Cookie
|
||||
- **功能描述**: 从 Cookie Cloud 服务器获取并解密 Cookie 数据
|
||||
- **输入**: Cookie Cloud API 地址、UUID、密码
|
||||
- **输出**: 按域名分组的 Cookie 数据
|
||||
- **处理逻辑**:
|
||||
1. 发送 GET 请求到 `/get/{uuid}` 接口
|
||||
2. 获取加密的 Cookie 数据
|
||||
3. 使用 AES 算法解密数据
|
||||
4. 解析 JSON 格式的 Cookie 数据
|
||||
5. 按域名组织 Cookie
|
||||
|
||||
#### 2.1.2 按域名获取 Cookie
|
||||
- **功能描述**: 获取指定域名的 Cookie
|
||||
- **输入**: 域名
|
||||
- **输出**: 该域名的 Cookie 列表
|
||||
- **处理逻辑**: 遍历所有 Cookie,匹配域名
|
||||
|
||||
### 2.2 浏览器控制
|
||||
|
||||
#### 2.2.1 创建浏览器实例
|
||||
- **功能描述**: 创建浏览器实例并启动浏览器
|
||||
- **输入**: 浏览器类型(chrome/edge)、是否无头模式
|
||||
- **输出**: 浏览器对象
|
||||
- **支持浏览器**: Chrome、Edge
|
||||
- **运行模式**: 有头模式、无头模式
|
||||
|
||||
#### 2.2.2 检测登录状态
|
||||
- **功能描述**: 检测网站是否已登录
|
||||
- **输入**: 网站 URL、登录检测选择器、成功文本
|
||||
- **输出**: 是否已登录(True/False)
|
||||
- **处理逻辑**:
|
||||
1. 访问网站
|
||||
2. 查找登录检测元素
|
||||
3. 检查元素文本是否包含成功文本
|
||||
|
||||
#### 2.2.3 使用 Cookie 登录
|
||||
- **功能描述**: 使用 Cookie 登录网站
|
||||
- **输入**: 网站 URL、Cookie 列表、登录检测选择器、成功文本
|
||||
- **输出**: 是否登录成功(True/False)
|
||||
- **处理逻辑**:
|
||||
1. 访问网站
|
||||
2. 设置 Cookie
|
||||
3. 刷新页面
|
||||
4. 检测登录状态
|
||||
|
||||
#### 2.2.4 刷新并保存 Cookie
|
||||
- **功能描述**: 刷新页面并获取最新的 Cookie
|
||||
- **输入**: 网站 URL
|
||||
- **输出**: 最新的 Cookie 列表
|
||||
- **处理逻辑**:
|
||||
1. 刷新页面
|
||||
2. 获取当前所有 Cookie
|
||||
3. 返回 Cookie 列表
|
||||
|
||||
### 2.3 Cookie 管理
|
||||
|
||||
#### 2.3.1 保存 Cookie
|
||||
- **功能描述**: 保存指定用户和网站的 Cookie
|
||||
- **输入**: 用户名、网站名、Cookie 列表
|
||||
- **输出**: 无
|
||||
- **存储位置**: `cookies.json` 文件
|
||||
- **存储格式**:
|
||||
```json
|
||||
{
|
||||
"用户名": {
|
||||
"网站名": [
|
||||
{
|
||||
"name": "cookie名称",
|
||||
"value": "cookie值",
|
||||
"domain": "域名",
|
||||
"path": "路径",
|
||||
"expiry": "过期时间"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3.2 读取 Cookie
|
||||
- **功能描述**: 读取指定用户和网站的 Cookie
|
||||
- **输入**: 用户名、网站名
|
||||
- **输出**: Cookie 列表
|
||||
- **数据来源**: `cookies.json` 文件
|
||||
|
||||
#### 2.3.3 按域名获取 Cookie
|
||||
- **功能描述**: 从所有 Cookie 中查找指定域名的 Cookie
|
||||
- **输入**: 域名
|
||||
- **输出**: Cookie 列表
|
||||
- **匹配规则**: 域名完全匹配或包含关系
|
||||
|
||||
### 2.4 失败追踪
|
||||
|
||||
#### 2.4.1 获取失败次数
|
||||
- **功能描述**: 获取指定用户和网站的失败次数
|
||||
- **输入**: 用户名、网站名
|
||||
- **输出**: 失败次数
|
||||
- **数据来源**: `state.json` 文件
|
||||
|
||||
#### 2.4.2 增加失败次数
|
||||
- **功能描述**: 增加指定用户和网站的失败次数
|
||||
- **输入**: 用户名、网站名
|
||||
- **输出**: 增加后的失败次数
|
||||
- **存储位置**: `state.json` 文件
|
||||
|
||||
#### 2.4.3 重置失败次数
|
||||
- **功能描述**: 重置指定用户和网站的失败次数
|
||||
- **输入**: 用户名、网站名
|
||||
- **输出**: 无
|
||||
|
||||
#### 2.4.4 检查并发送通知
|
||||
- **功能描述**: 检查失败次数,达到阈值时发送通知
|
||||
- **输入**: 用户名、网站名、最大失败次数、通知器、错误信息
|
||||
- **输出**: 是否应该停止重试
|
||||
- **处理逻辑**:
|
||||
1. 增加失败次数
|
||||
2. 判断是否达到阈值
|
||||
3. 达到阈值则发送通知
|
||||
4. 发送通知后重置失败计数
|
||||
5. 返回是否停止重试
|
||||
|
||||
### 2.5 消息通知
|
||||
|
||||
#### 2.5.1 发送爱语飞飞通知
|
||||
- **功能描述**: 通过爱语飞飞 API 发送通知
|
||||
- **输入**: 令牌、标题、内容
|
||||
- **输出**: 是否发送成功(True/False)
|
||||
- **API 地址**: `https://iyuu.cn/{令牌}.send`
|
||||
- **请求方式**: GET
|
||||
- **请求参数**:
|
||||
- `text`: 通知标题
|
||||
- `desp`: 通知内容
|
||||
|
||||
### 2.6 主程序流程
|
||||
|
||||
#### 2.6.1 读取配置
|
||||
- **功能描述**: 读取配置文件
|
||||
- **输入**: 配置文件路径
|
||||
- **输出**: 配置字典
|
||||
- **配置文件**: `config.json`
|
||||
|
||||
#### 2.6.2 处理用户
|
||||
- **功能描述**: 处理单个用户的所有网站
|
||||
- **输入**: 用户配置
|
||||
- **输出**: 无
|
||||
- **处理逻辑**:
|
||||
1. 初始化 Cookie Cloud 客户端
|
||||
2. 初始化爱语飞飞通知器
|
||||
3. 初始化浏览器
|
||||
4. 遍历该用户的所有网站
|
||||
5. 调用处理网站功能
|
||||
6. 关闭浏览器
|
||||
|
||||
#### 2.6.3 处理网站
|
||||
- **功能描述**: 处理单个网站的监控
|
||||
- **输入**: 用户名、网站配置、Cookie Cloud 客户端、通知器、最大失败次数、浏览器
|
||||
- **输出**: 无
|
||||
- **处理流程**:
|
||||
1. **步骤 1**: 尝试使用本地 Cookie 登录
|
||||
- 读取本地 Cookie
|
||||
- 登录网站
|
||||
- 检测登录状态
|
||||
- 登录成功 → 跳转到步骤 3
|
||||
- 登录失败 → 跳转到步骤 2
|
||||
2. **步骤 2**: 从 Cookie Cloud 获取 Cookie 并重试
|
||||
- 从 Cookie Cloud 获取 Cookie
|
||||
- 登录网站
|
||||
- 检测登录状态
|
||||
- 登录成功 → 跳转到步骤 3
|
||||
- 登录失败 → 跳转到步骤 4
|
||||
3. **步骤 3**: 刷新页面并保存新 Cookie
|
||||
- 刷新页面
|
||||
- 获取最新 Cookie
|
||||
- 保存到 Cookie 文件
|
||||
- 重置失败计数
|
||||
- 结束
|
||||
4. **步骤 4**: 登录失败处理
|
||||
- 增加失败计数
|
||||
- 判断是否达到阈值
|
||||
- 达到阈值 → 发送通知 → 重置计数
|
||||
- 未达到阈值 → 记录日志
|
||||
|
||||
#### 2.6.4 运行监控
|
||||
- **功能描述**: 运行整个监控流程
|
||||
- **输入**: 无
|
||||
- **输出**: 无
|
||||
- **处理逻辑**:
|
||||
1. 读取配置文件
|
||||
2. 遍历所有用户
|
||||
3. 处理每个用户
|
||||
4. 记录成功和失败数量
|
||||
5. 输出日志
|
||||
|
||||
## 3. 配置管理功能
|
||||
|
||||
### 3.1 配置文件管理
|
||||
- 支持多用户配置
|
||||
- 支持多网站配置
|
||||
- 支持 Cookie Cloud 配置
|
||||
- 支持通知配置
|
||||
- 支持浏览器配置
|
||||
|
||||
### 3.2 状态文件管理
|
||||
- 记录失败次数
|
||||
- 持久化存储
|
||||
- 自动加载和保存
|
||||
|
||||
### 3.3 Cookie 文件管理
|
||||
- 统一管理所有 Cookie
|
||||
- 按用户和网站组织
|
||||
- 持久化存储
|
||||
|
||||
## 4. 日志功能
|
||||
|
||||
### 4.1 日志记录
|
||||
- 记录操作开始和结束
|
||||
- 记录用户和网站处理状态
|
||||
- 记录登录成功和失败
|
||||
- 记录 Cookie 更新
|
||||
- 记录通知发送
|
||||
- 记录异常信息
|
||||
|
||||
### 4.2 日志格式
|
||||
```
|
||||
[时间戳] 日志内容
|
||||
```
|
||||
|
||||
## 5. 异常处理
|
||||
|
||||
### 5.1 网络异常
|
||||
- Cookie Cloud 请求失败
|
||||
- 网站访问失败
|
||||
- 通知发送失败
|
||||
|
||||
### 5.2 浏览器异常
|
||||
- 浏览器启动失败
|
||||
- 元素查找失败
|
||||
- Cookie 设置失败
|
||||
|
||||
### 5.3 文件异常
|
||||
- 配置文件读取失败
|
||||
- Cookie 文件读写失败
|
||||
- 状态文件读写失败
|
||||
|
||||
### 5.4 加密异常
|
||||
- Cookie 解密失败
|
||||
- 密钥生成失败
|
||||
|
||||
## 6. 性能要求
|
||||
|
||||
- Cookie Cloud 请求超时:30 秒
|
||||
- 网站访问超时:10 秒
|
||||
- 元素查找超时:10 秒
|
||||
- 通知发送超时:10 秒
|
||||
|
||||
## 7. 安全要求
|
||||
|
||||
- Cookie Cloud 使用端到端加密
|
||||
- 配置文件包含敏感信息,需妥善保管
|
||||
- 爱语飞飞令牌不应泄露
|
||||
- Cookie 数据不应明文传输
|
||||
|
||||
## 8. 扩展功能
|
||||
|
||||
### 8.1 多用户支持
|
||||
- 每个用户独立的 Cookie Cloud 配置
|
||||
- 每个用户独立的爱语飞飞令牌
|
||||
- 每个用户独立的浏览器配置
|
||||
|
||||
### 8.2 多网站支持
|
||||
- 每个用户可以配置多个网站
|
||||
- 每个网站独立的登录检测
|
||||
- 每个网站独立的失败计数
|
||||
|
||||
### 8.3 浏览器配置
|
||||
- 支持 Chrome 和 Edge
|
||||
- 支持有头和无头模式
|
||||
- 每个用户可以独立配置浏览器
|
||||
|
||||
### 8.4 通知配置
|
||||
- 支持自定义最大失败次数
|
||||
- 支持自定义通知内容
|
||||
- 每个用户独立的通知配置
|
||||
|
||||
## 9. 非功能需求
|
||||
|
||||
### 9.1 可靠性
|
||||
- 程序异常不影响下次执行
|
||||
- 状态文件确保数据不丢失
|
||||
- 浏览器异常自动关闭
|
||||
|
||||
### 9.2 可维护性
|
||||
- 模块化设计
|
||||
- 清晰的日志输出
|
||||
- 详细的错误信息
|
||||
|
||||
### 9.3 可扩展性
|
||||
- 易于添加新用户
|
||||
- 易于添加新网站
|
||||
- 易于添加新通知方式
|
||||
|
||||
### 9.4 易用性
|
||||
- 配置文件简单明了
|
||||
- 日志输出清晰易懂
|
||||
- 通知信息详细准确
|
||||
@@ -1,173 +0,0 @@
|
||||
# Cookie 自动监控项目需求文档
|
||||
|
||||
## 1. 项目背景
|
||||
|
||||
本项目旨在实现一个自动化 Cookie 管理和监控系统,通过定时任务检查多个网站的登录状态,自动刷新和更新 Cookie,确保用户能够持续访问目标网站。当登录失败时,系统会自动从 Cookie Cloud 服务器获取最新的 Cookie 进行重试,并在连续失败达到阈值时通过爱语飞飞消息服务发送通知。
|
||||
|
||||
## 2. 项目目标
|
||||
|
||||
- 自动监控多个网站的登录状态
|
||||
- 自动刷新和更新 Cookie
|
||||
- 支持 Cookie Cloud 备份和恢复
|
||||
- 支持多用户、多网站配置
|
||||
- 支持多种浏览器(Chrome、Edge)
|
||||
- 支持有头/无头模式切换
|
||||
- 失败通知机制,避免消息过于频繁
|
||||
|
||||
## 3. 技术栈
|
||||
|
||||
- **编程语言**: Python 3.14.2
|
||||
- **浏览器控制**: DrissionPage
|
||||
- **HTTP 请求**: requests
|
||||
- **加密解密**: pycryptodome
|
||||
- **消息通知**: 爱语飞飞 API
|
||||
- **定时任务**: Windows 任务计划程序
|
||||
- **配置格式**: JSON
|
||||
|
||||
## 4. 运行环境
|
||||
|
||||
- **操作系统**: Windows 10/11
|
||||
- **Python 版本**: 3.14.2
|
||||
- **浏览器**: Chrome 或 Edge(系统自带)
|
||||
|
||||
## 5. 系统架构
|
||||
|
||||
### 5.1 模块划分
|
||||
|
||||
1. **Cookie Cloud 模块** (`cookie_cloud.py`)
|
||||
- 从 Cookie Cloud 服务器获取 Cookie 数据
|
||||
- 解密 Cookie 数据
|
||||
- 按域名组织 Cookie
|
||||
|
||||
2. **消息通知模块** (`notifier.py`)
|
||||
- 集成爱语飞飞 API
|
||||
- 失败次数追踪
|
||||
- 通知发送控制
|
||||
|
||||
3. **浏览器登录模块** (`browser_login.py`)
|
||||
- 使用 DrissionPage 控制浏览器
|
||||
- 检测登录状态
|
||||
- 管理 Cookie
|
||||
|
||||
4. **Cookie 管理器** (`browser_login.py`)
|
||||
- 统一管理所有 Cookie
|
||||
- 支持按用户和网站存储
|
||||
- 持久化存储
|
||||
|
||||
5. **主程序** (`monitor.py`)
|
||||
- 读取配置文件
|
||||
- 协调各模块工作
|
||||
- 处理异常和日志
|
||||
|
||||
### 5.2 数据流
|
||||
|
||||
```
|
||||
配置文件 → 主程序 → 浏览器登录模块 → 网站检测
|
||||
↓
|
||||
Cookie 管理器 ← Cookie Cloud 模块
|
||||
↓
|
||||
消息通知模块 ← 失败追踪器
|
||||
```
|
||||
|
||||
## 6. 核心流程
|
||||
|
||||
### 6.1 监控流程
|
||||
|
||||
1. 读取配置文件
|
||||
2. 遍历每个用户
|
||||
3. 遍历该用户的每个网站
|
||||
4. 尝试使用本地 Cookie 登录
|
||||
5. 登录失败 → 从 Cookie Cloud 获取 Cookie → 重试登录
|
||||
6. 登录成功 → 刷新页面 → 保存新 Cookie → 重置失败计数
|
||||
7. 登录失败 → 失败计数 +1
|
||||
8. 失败计数达到阈值 → 发送提醒 → 重置计数
|
||||
9. 保存状态到状态文件
|
||||
|
||||
### 6.2 失败重试策略
|
||||
|
||||
- 每个网站独立计数失败次数
|
||||
- 连续失败 3 次后发送通知
|
||||
- 发送通知后重置计数
|
||||
- 下次执行重新开始计数
|
||||
|
||||
## 7. 配置管理
|
||||
|
||||
### 7.1 配置文件结构
|
||||
|
||||
```json
|
||||
{
|
||||
"users": [
|
||||
{
|
||||
"name": "用户名",
|
||||
"cookie_cloud": {
|
||||
"uuid": "Cookie Cloud UUID",
|
||||
"password": "Cookie Cloud 密码",
|
||||
"api_url": "Cookie Cloud API 地址"
|
||||
},
|
||||
"notification": {
|
||||
"iyuu_token": "爱语飞飞令牌",
|
||||
"max_fail_count": 3
|
||||
},
|
||||
"browser": {
|
||||
"type": "edge/chrome",
|
||||
"headless": true/false
|
||||
},
|
||||
"websites": [
|
||||
{
|
||||
"name": "网站名称",
|
||||
"url": "网站地址",
|
||||
"login_check_selector": "登录检测选择器",
|
||||
"success_text": "登录成功文本"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 数据文件
|
||||
|
||||
- `config.json`: 配置文件
|
||||
- `cookies.json`: Cookie 存储文件
|
||||
- `state.json`: 失败计数状态文件
|
||||
|
||||
## 8. 定时任务
|
||||
|
||||
使用 Windows 任务计划程序设置定时任务:
|
||||
- 执行频率:每 30 分钟
|
||||
- 执行命令:`python G:\test\cookie\monitor.py`
|
||||
- 运行账户:SYSTEM
|
||||
|
||||
## 9. 依赖安装
|
||||
|
||||
```bash
|
||||
pip install DrissionPage requests pycryptodome
|
||||
```
|
||||
|
||||
## 10. 项目文件结构
|
||||
|
||||
```
|
||||
G:\test\cookie\
|
||||
├── config.json # 配置文件
|
||||
├── monitor.py # 主程序
|
||||
├── cookie_cloud.py # Cookie Cloud 模块
|
||||
├── notifier.py # 消息通知模块
|
||||
├── browser_login.py # 浏览器登录模块
|
||||
├── state.json # 失败计数状态文件
|
||||
└── cookies.json # Cookie 存储文件
|
||||
```
|
||||
|
||||
## 11. 安全考虑
|
||||
|
||||
- Cookie Cloud 使用端到端加密
|
||||
- 配置文件包含敏感信息,需妥善保管
|
||||
- 定时任务使用 SYSTEM 账户运行,需谨慎授权
|
||||
- 爱语飞飞令牌不应泄露
|
||||
|
||||
## 12. 扩展性
|
||||
|
||||
- 支持添加更多用户
|
||||
- 支持添加更多网站
|
||||
- 支持自定义失败阈值
|
||||
- 支持自定义通知消息内容
|
||||
- 支持切换浏览器类型和运行模式
|
||||
Reference in New Issue
Block a user