重构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