709 lines
28 KiB
Python
709 lines
28 KiB
Python
|
|
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/<qrid>', 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/<account_id>')
|
|||
|
|
@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/<account_id>/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/<account_id>/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/<account_id>/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/<task_id>/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/<task_id>/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)
|