diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..bf6239f --- /dev/null +++ b/backend/app.py @@ -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) diff --git a/backend/config/__init__.py b/backend/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/config/settings.py b/backend/config/settings.py new file mode 100644 index 0000000..84ce5f4 --- /dev/null +++ b/backend/config/settings.py @@ -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' diff --git a/backend/instance/cookie_monitor.db b/backend/instance/cookie_monitor.db new file mode 100644 index 0000000..e2064a9 Binary files /dev/null and b/backend/instance/cookie_monitor.db differ diff --git a/backend/models/__init__.py b/backend/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/models/database.py b/backend/models/database.py new file mode 100644 index 0000000..1e600a3 --- /dev/null +++ b/backend/models/database.py @@ -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) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..bd190aa --- /dev/null +++ b/backend/requirements.txt @@ -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 diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/routes/admin.py b/backend/routes/admin.py new file mode 100644 index 0000000..515f91d --- /dev/null +++ b/backend/routes/admin.py @@ -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('/') +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)}) diff --git a/backend/routes/cookies.py b/backend/routes/cookies.py new file mode 100644 index 0000000..77affa8 --- /dev/null +++ b/backend/routes/cookies.py @@ -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/', 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/', 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('/', 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('/', 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/', 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 diff --git a/backend/routes/detections.py b/backend/routes/detections.py new file mode 100644 index 0000000..db12cce --- /dev/null +++ b/backend/routes/detections.py @@ -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('/', 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 diff --git a/backend/routes/notifications.py b/backend/routes/notifications.py new file mode 100644 index 0000000..4af123a --- /dev/null +++ b/backend/routes/notifications.py @@ -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 diff --git a/backend/routes/users.py b/backend/routes/users.py new file mode 100644 index 0000000..7052dc5 --- /dev/null +++ b/backend/routes/users.py @@ -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('/', methods=['GET']) +def get_user(user_id): + """获取单个用户""" + user = User.query.get_or_404(user_id) + return jsonify(user.to_dict()) + + +@user_bp.route('/', 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('/', 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 diff --git a/backend/routes/websites.py b/backend/routes/websites.py new file mode 100644 index 0000000..5b48155 --- /dev/null +++ b/backend/routes/websites.py @@ -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/', 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('/', methods=['GET']) +def get_website(website_id): + """获取单个网站""" + website = Website.query.get_or_404(website_id) + return jsonify(website.to_dict()) + + +@website_bp.route('/', 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('/', 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 diff --git a/backend/static/css/style.css b/backend/static/css/style.css new file mode 100644 index 0000000..e551f70 --- /dev/null +++ b/backend/static/css/style.css @@ -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); +} diff --git a/backend/static/js/app.js b/backend/static/js/app.js new file mode 100644 index 0000000..10ad649 --- /dev/null +++ b/backend/static/js/app.js @@ -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'); diff --git a/backend/templates/index.html b/backend/templates/index.html new file mode 100644 index 0000000..1a1912c --- /dev/null +++ b/backend/templates/index.html @@ -0,0 +1,398 @@ +{% raw %} + + + + + Cookie监控管理系统 + + + + + + + +
+
+ + +
+

Cookie监控管理系统

+ 返回监控首页 +
+
+
+ + {{ countdown }}秒后自动刷新 +
+ 刷新 + 登录后台 + + 后台管理 + + +
+
+ + + +
+
+

网站监控仪表板

+
+ + + + + + + + +
+
+ +
+
+

{{ user.name }}

+ + {{ user.websites.filter(w => w.loginStatus === 'success').length }}/{{ user.websites.length }} 在线 + +
+ + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ 返回监控首页 +

网站管理 - {{ currentWebsite?.name }}

+
+ + + + 添加Cookie + 刷新 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 保存设置 + + + + + 刷新 + + + + + + + + + +
+ + +
+

系统设置

+ + + 添加用户 + + + + + + + + + + + + + +
+ + + + + 添加网站 +
+ + + + + + + + + + + + + +
+ + + 刷新 + + + + + + + + + + + + + + + 刷新 + + + + + + + + + + + + + + + +
+
+ + +
+
+ 返回设置 +

网站管理 - {{ currentUser?.name }}

+ 添加网站 +
+ + + + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + Cookie Cloud 设置 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +{% endraw %} diff --git a/browser_manager.py b/browser_manager.py deleted file mode 100644 index 6ca2103..0000000 --- a/browser_manager.py +++ /dev/null @@ -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 diff --git a/config_manager.py b/config_manager.py deleted file mode 100644 index fa71447..0000000 --- a/config_manager.py +++ /dev/null @@ -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", "") - } diff --git a/cookie_cloud.py b/cookie_cloud.py deleted file mode 100644 index 0941306..0000000 --- a/cookie_cloud.py +++ /dev/null @@ -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 diff --git a/cookie_extractor.py b/cookie_extractor.py deleted file mode 100644 index 474acff..0000000 --- a/cookie_extractor.py +++ /dev/null @@ -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() diff --git a/cookiecloud_client/README.md b/cookiecloud_client/README.md deleted file mode 100644 index 896ad85..0000000 --- a/cookiecloud_client/README.md +++ /dev/null @@ -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客户端功能 -- 独立模块,无外部依赖 -- 完善的异常处理 -- 详细的文档和示例 diff --git a/cookiecloud_client/__init__.py b/cookiecloud_client/__init__.py deleted file mode 100644 index 9342f33..0000000 --- a/cookiecloud_client/__init__.py +++ /dev/null @@ -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__' -] diff --git a/cookiecloud_client/client.py b/cookiecloud_client/client.py deleted file mode 100644 index 87ba2f1..0000000 --- a/cookiecloud_client/client.py +++ /dev/null @@ -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 diff --git a/cookiecloud_client/examples.py b/cookiecloud_client/examples.py deleted file mode 100644 index 9240475..0000000 --- a/cookiecloud_client/examples.py +++ /dev/null @@ -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() diff --git a/cookiecloud_client/exceptions.py b/cookiecloud_client/exceptions.py deleted file mode 100644 index 5204dbd..0000000 --- a/cookiecloud_client/exceptions.py +++ /dev/null @@ -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) diff --git a/cookiecloud_client/models.py b/cookiecloud_client/models.py deleted file mode 100644 index c0a877b..0000000 --- a/cookiecloud_client/models.py +++ /dev/null @@ -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()) diff --git a/cookiecloud_client/requirements.txt b/cookiecloud_client/requirements.txt deleted file mode 100644 index a1561de..0000000 --- a/cookiecloud_client/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -# CookieCloud客户端依赖项 -# 此模块完全独立,仅使用Python标准库 - -# Python版本要求 -Python>=3.7 - -# 无外部依赖 -# 仅使用Python标准库模块: -# - json -# - urllib -# - dataclasses -# - typing -# - datetime -# - unittest (用于测试) diff --git a/cookiecloud_client/setup.py b/cookiecloud_client/setup.py deleted file mode 100644 index f95f740..0000000 --- a/cookiecloud_client/setup.py +++ /dev/null @@ -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", - }, -) diff --git a/cookiecloud_client/tests.py b/cookiecloud_client/tests.py deleted file mode 100644 index 6612326..0000000 --- a/cookiecloud_client/tests.py +++ /dev/null @@ -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() diff --git a/cookiecloud_client/version.py b/cookiecloud_client/version.py deleted file mode 100644 index a7edd13..0000000 --- a/cookiecloud_client/version.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -版本信息 -""" - -__version__ = '1.0.0' -__author__ = 'CookieManager Team' -__email__ = 'support@example.com' -__license__ = 'MIT' diff --git a/cookies.json b/cookies.json deleted file mode 100644 index 28417cf..0000000 --- a/cookies.json +++ /dev/null @@ -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" - } -] \ No newline at end of file diff --git a/frontend/__init__.py b/frontend/__init__.py new file mode 100644 index 0000000..b43039b --- /dev/null +++ b/frontend/__init__.py @@ -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 diff --git a/frontend/client.py b/frontend/client.py new file mode 100644 index 0000000..a1cdcd8 --- /dev/null +++ b/frontend/client.py @@ -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() diff --git a/frontend/config/config.py b/frontend/config/config.py new file mode 100644 index 0000000..b4b4678 --- /dev/null +++ b/frontend/config/config.py @@ -0,0 +1,15 @@ +""" +配置文件 +""" +# 后台服务地址 +BACKEND_URL = "http://localhost:5000" + +# 浏览器配置 +BROWSER_TYPE = "edge" # edge 或 chrome +HEADLESS_MODE = False # 是否无头模式 + +# 检测间隔(秒) +CHECK_INTERVAL = 30 + +# 页面加载超时时间(秒) +PAGE_LOAD_TIMEOUT = 30 diff --git a/logger.py b/logger.py deleted file mode 100644 index 9395894..0000000 --- a/logger.py +++ /dev/null @@ -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) diff --git a/monitor.py b/monitor.py deleted file mode 100644 index 49e8d1d..0000000 --- a/monitor.py +++ /dev/null @@ -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() diff --git a/monitor.spec b/monitor.spec deleted file mode 100644 index dba6602..0000000 --- a/monitor.spec +++ /dev/null @@ -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', -) diff --git a/notification.py b/notification.py deleted file mode 100644 index a0fa744..0000000 --- a/notification.py +++ /dev/null @@ -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 diff --git a/notify_state.json b/notify_state.json deleted file mode 100644 index 3b704e8..0000000 --- a/notify_state.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "彭峰(个人)": { - "date": "2026-03-08", - "hour": 9 - }, - "彭峰(公司)": { - "date": "2026-03-08", - "hour": 9 - }, - "小余": { - "date": "2026-03-08", - "hour": 8 - } -} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2a1686f..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -DrissionPage -requests -pycryptodome \ No newline at end of file diff --git a/update_cookie_cloud.py b/update_cookie_cloud.py new file mode 100644 index 0000000..4f956b1 --- /dev/null +++ b/update_cookie_cloud.py @@ -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() diff --git a/功能需求文档.md b/功能需求文档.md deleted file mode 100644 index 00476a2..0000000 --- a/功能需求文档.md +++ /dev/null @@ -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 易用性 -- 配置文件简单明了 -- 日志输出清晰易懂 -- 通知信息详细准确 \ No newline at end of file diff --git a/项目需求文档.md b/项目需求文档.md deleted file mode 100644 index 2f37000..0000000 --- a/项目需求文档.md +++ /dev/null @@ -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. 扩展性 - -- 支持添加更多用户 -- 支持添加更多网站 -- 支持自定义失败阈值 -- 支持自定义通知消息内容 -- 支持切换浏览器类型和运行模式 \ No newline at end of file