import os from flask import Flask, render_template, request, redirect, url_for, session, flash, jsonify from flask_session import Session import requests from functools import wraps from dotenv import load_dotenv from datetime import datetime load_dotenv() app = Flask(__name__) app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-secret-key') app.config['SESSION_TYPE'] = 'filesystem' Session(app) API_BASE_URL = os.getenv('API_BASE_URL', 'http://localhost:8000') AUTH_BASE_URL = os.getenv('AUTH_BASE_URL', 'http://localhost:8001') def get_headers(): """获取请求头,包含认证令牌""" headers = {'Content-Type': 'application/json'} if 'access_token' in session: headers['Authorization'] = f"Bearer {session['access_token']}" return headers def login_required(f): """登录验证装饰器""" @wraps(f) def decorated_function(*args, **kwargs): if 'user' not in session: flash('Please login first', 'warning') return redirect(url_for('login')) return f(*args, **kwargs) return decorated_function @app.route('/') def index(): if 'user' in session: return redirect(url_for('dashboard')) return redirect(url_for('login')) @app.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST': username = request.form.get('username') email = request.form.get('email') password = request.form.get('password') confirm_password = request.form.get('confirm_password') if password != confirm_password: flash('Passwords do not match', 'danger') return redirect(url_for('register')) try: response = requests.post( f'{AUTH_BASE_URL}/auth/register', json={'username': username, 'email': email, 'password': password}, timeout=10 ) if response.status_code == 201: data = response.json() session['user'] = data['user'] session['access_token'] = data['access_token'] session['refresh_token'] = data['refresh_token'] flash('Registration successful!', 'success') return redirect(url_for('dashboard')) else: error_data = response.json() flash(error_data.get('detail', 'Registration failed'), 'danger') except requests.RequestException as e: flash(f'Connection error: {str(e)}', 'danger') return render_template('register.html') @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': email = request.form.get('email') password = request.form.get('password') try: response = requests.post( f'{AUTH_BASE_URL}/auth/login', json={'email': email, 'password': password}, timeout=10 ) if response.status_code == 200: data = response.json() session['user'] = data['user'] session['access_token'] = data['access_token'] session['refresh_token'] = data['refresh_token'] flash('Login successful!', 'success') return redirect(url_for('dashboard')) else: error_data = response.json() flash(error_data.get('detail', 'Login failed'), 'danger') except requests.RequestException as e: flash(f'Connection error: {str(e)}', 'danger') return render_template('login.html') @app.route('/logout') def logout(): session.clear() flash('Logged out successfully', 'success') return redirect(url_for('login')) @app.route('/dashboard') @login_required def dashboard(): try: response = requests.get( f'{API_BASE_URL}/api/v1/accounts', headers=get_headers(), timeout=10 ) data = response.json() accounts = data.get('data', []) if data.get('success') else [] except requests.RequestException: accounts = [] flash('Failed to load accounts', 'warning') return render_template('dashboard.html', accounts=accounts, user=session.get('user')) @app.route('/accounts/new', methods=['GET', 'POST']) @login_required def add_account(): if request.method == 'POST': login_method = request.form.get('login_method', 'manual') if login_method == 'manual': weibo_user_id = request.form.get('weibo_user_id') cookie = request.form.get('cookie') remark = request.form.get('remark') try: response = requests.post( f'{API_BASE_URL}/api/v1/accounts', json={ 'weibo_user_id': weibo_user_id, 'cookie': cookie, 'remark': remark }, headers=get_headers(), timeout=10 ) data = response.json() if response.status_code == 200 and data.get('success'): flash('Account added successfully!', 'success') return redirect(url_for('dashboard')) else: flash(data.get('message', 'Failed to add account'), 'danger') except requests.RequestException as e: flash(f'Connection error: {str(e)}', 'danger') # 扫码授权功能总是启用(使用微博网页版接口) weibo_qrcode_enabled = True return render_template('add_account.html', weibo_qrcode_enabled=weibo_qrcode_enabled) @app.route('/api/weibo/qrcode/generate', methods=['POST']) @login_required def generate_weibo_qrcode(): """生成微博扫码登录二维码(模拟网页版)""" import uuid import time import traceback try: # 模拟微博网页版的二维码生成接口 # 实际接口:https://login.sina.com.cn/sso/qrcode/image # 生成唯一的 qrcode_id qrcode_id = str(uuid.uuid4()) # 调用微博的二维码生成接口 qr_api_url = 'https://login.sina.com.cn/sso/qrcode/image' params = { 'entry': 'weibo', 'size': '180', 'callback': f'STK_{int(time.time() * 1000)}' } # 添加浏览器请求头,模拟真实浏览器 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Referer': 'https://weibo.com/', 'Accept': '*/*', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'keep-alive' } print(f"[DEBUG] 请求微博 API: {qr_api_url}") response = requests.get(qr_api_url, params=params, headers=headers, timeout=10) print(f"[DEBUG] 响应状态码: {response.status_code}") print(f"[DEBUG] 响应内容: {response.text[:200]}") # 微博返回的是 JSONP 格式,需要解析 # 格式:STK_xxx({"retcode":20000000,"qrid":"xxx","image":"data:image/png;base64,xxx"}) import re import json match = re.search(r'\((.*)\)', response.text) if match: data = json.loads(match.group(1)) print(f"[DEBUG] 解析的数据: retcode={data.get('retcode')}, data={data.get('data')}") if data.get('retcode') == 20000000: # 微博返回的数据结构:{"retcode":20000000,"data":{"qrid":"...","image":"..."}} qr_data = data.get('data', {}) qrid = qr_data.get('qrid') qr_image_url = qr_data.get('image') if not qrid or not qr_image_url: print(f"[ERROR] 缺少 qrid 或 image: qrid={qrid}, image={qr_image_url}") return jsonify({'success': False, 'error': '二维码数据不完整'}), 500 # 如果 image 是相对 URL,补全为完整 URL if qr_image_url.startswith('//'): qr_image_url = 'https:' + qr_image_url print(f"[DEBUG] 二维码 URL: {qr_image_url}") # 存储二维码状态 if 'weibo_qrcodes' not in session: session['weibo_qrcodes'] = {} session['weibo_qrcodes'][qrid] = { 'status': 'waiting', 'created_at': str(datetime.now()), 'qrcode_id': qrcode_id } session.modified = True print(f"[DEBUG] 二维码生成成功: qrid={qrid}") return jsonify({ 'success': True, 'qrid': qrid, 'qr_image': qr_image_url, # 返回二维码图片 URL 'expires_in': 180 }) print("[DEBUG] 未能解析响应或 retcode 不正确") return jsonify({'success': False, 'error': '生成二维码失败'}), 500 except Exception as e: print(f"[ERROR] 生成二维码异常: {str(e)}") print(f"[ERROR] 堆栈跟踪:\n{traceback.format_exc()}") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/weibo/qrcode/check/', methods=['GET']) @login_required def check_weibo_qrcode(qrid): """检查微博扫码状态(模拟网页版)""" import time try: # 检查二维码是否存在 qrcodes = session.get('weibo_qrcodes', {}) if qrid not in qrcodes: return jsonify({'status': 'expired'}) # 调用微博的轮询接口 # 实际接口:https://login.sina.com.cn/sso/qrcode/check check_api_url = 'https://login.sina.com.cn/sso/qrcode/check' params = { 'entry': 'weibo', 'qrid': qrid, 'callback': f'STK_{int(time.time() * 1000)}' } # 添加浏览器请求头 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Referer': 'https://weibo.com/', 'Accept': '*/*', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Connection': 'keep-alive' } response = requests.get(check_api_url, params=params, headers=headers, timeout=10) print(f"[DEBUG] 检查状态 - qrid: {qrid}") print(f"[DEBUG] 检查状态 - 响应状态码: {response.status_code}") print(f"[DEBUG] 检查状态 - 响应内容: {response.text[:500]}") # 解析 JSONP 响应 import re match = re.search(r'\((.*)\)', response.text) if match: import json data = json.loads(match.group(1)) retcode = data.get('retcode') print(f"[DEBUG] 检查状态 - retcode: {retcode}, data: {data}") # 微博扫码状态码: # 20000000: 等待扫码 # 50050001: 已扫码,等待确认 # 20000001: 确认成功 # 50050002: 二维码过期 # 50050004: 取消授权 # 50114001: 未使用(等待扫码) # 50114004: 该二维码已登录(可能是成功状态) if retcode == 20000000 or retcode == 50114001: # 等待扫码 return jsonify({'status': 'waiting'}) elif retcode == 50050001: # 已扫码,等待确认 return jsonify({'status': 'scanned'}) elif retcode == 20000001 or retcode == 50114004: # 登录成功,获取跳转 URL # 50114004 也表示已登录成功 alt_url = data.get('alt') # 如果没有 alt 字段,尝试从 data 中获取 if not alt_url and data.get('data'): alt_url = data.get('data', {}).get('alt') print(f"[DEBUG] 登录成功 - retcode: {retcode}, alt_url: {alt_url}, full_data: {data}") # 如果没有 alt_url,尝试构造登录 URL if not alt_url: # 尝试使用 qrid 构造登录 URL # 微博可能使用不同的 URL 格式 possible_urls = [ f"https://login.sina.com.cn/sso/login.php?entry=weibo&qrid={qrid}", f"https://passport.weibo.com/sso/login?qrid={qrid}", f"https://login.sina.com.cn/sso/qrcode/login?qrid={qrid}" ] print(f"[DEBUG] 尝试构造登录 URL") for url in possible_urls: try: print(f"[DEBUG] 尝试 URL: {url}") test_response = requests.get(url, headers=headers, allow_redirects=False, timeout=5) print(f"[DEBUG] 响应状态码: {test_response.status_code}") if test_response.status_code in [200, 302, 301]: alt_url = url print(f"[DEBUG] 找到有效 URL: {alt_url}") break except Exception as e: print(f"[DEBUG] URL 失败: {str(e)}") continue if alt_url: # 添加浏览器请求头 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Referer': 'https://weibo.com/', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Connection': 'keep-alive' } print(f"[DEBUG] 访问跳转 URL: {alt_url}") # 访问跳转 URL 获取 Cookie cookie_response = requests.get(alt_url, headers=headers, allow_redirects=True, timeout=10) cookies = cookie_response.cookies print(f"[DEBUG] 获取到的 Cookies: {dict(cookies)}") # 构建 Cookie 字符串 cookie_str = '; '.join([f'{k}={v}' for k, v in cookies.items()]) if not cookie_str: print("[ERROR] 未获取到任何 Cookie") return jsonify({'status': 'error', 'error': '未获取到 Cookie'}) print(f"[DEBUG] Cookie 字符串长度: {len(cookie_str)}") # 获取用户信息 # 可以通过 Cookie 访问微博 API 获取 uid user_info_url = 'https://weibo.com/ajax/profile/info' user_headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Referer': 'https://weibo.com/', 'Accept': 'application/json, text/plain, */*', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8' } print(f"[DEBUG] 请求用户信息: {user_info_url}") user_response = requests.get(user_info_url, cookies=cookies, headers=user_headers, timeout=10) print(f"[DEBUG] 用户信息响应状态码: {user_response.status_code}") print(f"[DEBUG] 用户信息响应内容: {user_response.text[:500]}") user_data = user_response.json() if user_data.get('ok') == 1: user_info = user_data.get('data', {}).get('user', {}) weibo_uid = user_info.get('idstr', '') screen_name = user_info.get('screen_name', 'Weibo User') print(f"[DEBUG] 获取用户信息成功: uid={weibo_uid}, name={screen_name}") # 更新状态 session['weibo_qrcodes'][qrid]['status'] = 'success' session['weibo_qrcodes'][qrid]['cookie'] = cookie_str session['weibo_qrcodes'][qrid]['weibo_uid'] = weibo_uid session['weibo_qrcodes'][qrid]['screen_name'] = screen_name session.modified = True return jsonify({ 'status': 'success', 'weibo_uid': weibo_uid, 'screen_name': screen_name }) else: print(f"[ERROR] 获取用户信息失败: {user_data}") return jsonify({'status': 'error', 'error': '获取用户信息失败'}) else: print("[ERROR] 未获取到跳转 URL") return jsonify({'status': 'error', 'error': '获取登录信息失败'}) elif retcode == 50050002: return jsonify({'status': 'expired'}) elif retcode == 50050004: return jsonify({'status': 'cancelled'}) else: # 未知状态码,记录日志 print(f"[WARN] 未知的 retcode: {retcode}, msg: {data.get('msg')}") return jsonify({'status': 'waiting'}) # 默认继续等待 print("[DEBUG] 未能解析响应") return jsonify({'status': 'error', 'error': '检查状态失败'}) except Exception as e: print(f"[ERROR] 检查二维码状态异常: {str(e)}") import traceback print(f"[ERROR] 堆栈跟踪:\n{traceback.format_exc()}") return jsonify({'status': 'error', 'error': str(e)}) @app.route('/api/weibo/qrcode/add-account', methods=['POST']) @login_required def add_account_from_qrcode(): """从扫码结果添加账号""" try: data = request.json qrid = data.get('qrid') remark = data.get('remark', '') print(f"[DEBUG] 添加账号 - qrid: {qrid}") # 获取扫码结果 qrcodes = session.get('weibo_qrcodes', {}) qr_info = qrcodes.get(qrid) print(f"[DEBUG] 添加账号 - qr_info: {qr_info}") if not qr_info or qr_info.get('status') != 'success': print(f"[ERROR] 添加账号失败 - 二维码状态不正确: {qr_info.get('status') if qr_info else 'None'}") return jsonify({'success': False, 'message': '二维码未完成授权'}), 400 cookie = qr_info.get('cookie') weibo_uid = qr_info.get('weibo_uid') screen_name = qr_info.get('screen_name', 'Weibo User') print(f"[DEBUG] 添加账号 - uid: {weibo_uid}, name: {screen_name}, cookie_len: {len(cookie) if cookie else 0}") if not remark: remark = f"{screen_name} (扫码添加)" # 添加账号到系统 print(f"[DEBUG] 调用后端 API 添加账号: {API_BASE_URL}/api/v1/accounts") response = requests.post( f'{API_BASE_URL}/api/v1/accounts', json={ 'weibo_user_id': weibo_uid, 'cookie': cookie, 'remark': remark }, headers=get_headers(), timeout=10 ) print(f"[DEBUG] 后端响应状态码: {response.status_code}") print(f"[DEBUG] 后端响应内容: {response.text[:500]}") result = response.json() if response.status_code == 200 and result.get('success'): # 清除已使用的二维码 session['weibo_qrcodes'].pop(qrid, None) session.modified = True print(f"[DEBUG] 账号添加成功") return jsonify({ 'success': True, 'message': 'Account added successfully', 'account': result.get('data', {}) }) else: print(f"[ERROR] 后端返回失败: {result}") return jsonify({ 'success': False, 'message': result.get('message', 'Failed to add account') }), 400 except Exception as e: print(f"[ERROR] 添加账号异常: {str(e)}") import traceback print(f"[ERROR] 堆栈跟踪:\n{traceback.format_exc()}") return jsonify({'success': False, 'message': str(e)}), 500 @app.route('/accounts/') @login_required def account_detail(account_id): try: # 获取账号详情 response = requests.get( f'{API_BASE_URL}/api/v1/accounts/{account_id}', headers=get_headers(), timeout=10 ) account_data = response.json() account = account_data.get('data') if account_data.get('success') else None # 获取任务列表 tasks_response = requests.get( f'{API_BASE_URL}/api/v1/accounts/{account_id}/tasks', headers=get_headers(), timeout=10 ) tasks_data = tasks_response.json() tasks = tasks_data.get('data', []) if tasks_data.get('success') else [] # 获取签到日志 page = request.args.get('page', 1, type=int) logs_response = requests.get( f'{API_BASE_URL}/api/v1/accounts/{account_id}/signin-logs', params={'page': page, 'size': 20}, headers=get_headers(), timeout=10 ) logs_data = logs_response.json() logs = logs_data.get('data', {}) if logs_data.get('success') else {} if not account: flash('Account not found', 'danger') return redirect(url_for('dashboard')) return render_template( 'account_detail.html', account=account, tasks=tasks, logs=logs, user=session.get('user') ) except requests.RequestException as e: flash(f'Connection error: {str(e)}', 'danger') return redirect(url_for('dashboard')) @app.route('/accounts//edit', methods=['GET', 'POST']) @login_required def edit_account(account_id): if request.method == 'POST': remark = request.form.get('remark') cookie = request.form.get('cookie') try: data = {'remark': remark} if cookie: data['cookie'] = cookie response = requests.put( f'{API_BASE_URL}/api/v1/accounts/{account_id}', json=data, headers=get_headers(), timeout=10 ) result = response.json() if response.status_code == 200 and result.get('success'): flash('Account updated successfully!', 'success') return redirect(url_for('account_detail', account_id=account_id)) else: flash(result.get('message', 'Failed to update account'), 'danger') except requests.RequestException as e: flash(f'Connection error: {str(e)}', 'danger') try: response = requests.get( f'{API_BASE_URL}/api/v1/accounts/{account_id}', headers=get_headers(), timeout=10 ) data = response.json() account = data.get('data') if data.get('success') else None if not account: flash('Account not found', 'danger') return redirect(url_for('dashboard')) return render_template('edit_account.html', account=account) except requests.RequestException as e: flash(f'Connection error: {str(e)}', 'danger') return redirect(url_for('dashboard')) @app.route('/accounts//delete', methods=['POST']) @login_required def delete_account(account_id): try: response = requests.delete( f'{API_BASE_URL}/api/v1/accounts/{account_id}', headers=get_headers(), timeout=10 ) data = response.json() if response.status_code == 200 and data.get('success'): flash('Account deleted successfully!', 'success') else: flash(data.get('message', 'Failed to delete account'), 'danger') except requests.RequestException as e: flash(f'Connection error: {str(e)}', 'danger') return redirect(url_for('dashboard')) @app.route('/accounts//tasks/new', methods=['GET', 'POST']) @login_required def add_task(account_id): if request.method == 'POST': cron_expression = request.form.get('cron_expression') try: response = requests.post( f'{API_BASE_URL}/api/v1/accounts/{account_id}/tasks', json={'cron_expression': cron_expression}, headers=get_headers(), timeout=10 ) data = response.json() if response.status_code == 200 and data.get('success'): flash('Task created successfully!', 'success') return redirect(url_for('account_detail', account_id=account_id)) else: flash(data.get('message', 'Failed to create task'), 'danger') except requests.RequestException as e: flash(f'Connection error: {str(e)}', 'danger') return render_template('add_task.html', account_id=account_id) @app.route('/tasks//toggle', methods=['POST']) @login_required def toggle_task(task_id): is_enabled = request.form.get('is_enabled') == 'true' try: response = requests.put( f'{API_BASE_URL}/api/v1/tasks/{task_id}', json={'is_enabled': not is_enabled}, headers=get_headers(), timeout=10 ) data = response.json() if response.status_code == 200 and data.get('success'): flash('Task updated successfully!', 'success') else: flash(data.get('message', 'Failed to update task'), 'danger') except requests.RequestException as e: flash(f'Connection error: {str(e)}', 'danger') account_id = request.form.get('account_id') return redirect(url_for('account_detail', account_id=account_id)) @app.route('/tasks//delete', methods=['POST']) @login_required def delete_task(task_id): account_id = request.form.get('account_id') try: response = requests.delete( f'{API_BASE_URL}/api/v1/tasks/{task_id}', headers=get_headers(), timeout=10 ) data = response.json() if response.status_code == 200 and data.get('success'): flash('Task deleted successfully!', 'success') else: flash(data.get('message', 'Failed to delete task'), 'danger') except requests.RequestException as e: flash(f'Connection error: {str(e)}', 'danger') return redirect(url_for('account_detail', account_id=account_id)) @app.errorhandler(404) def not_found(error): return render_template('404.html'), 404 @app.errorhandler(500) def server_error(error): return render_template('500.html'), 500 if __name__ == '__main__': app.run(debug=True, port=5000)