核心改动: - weidian_sso_login_v4.py: 全新人机协作登录方案 - Playwright 打开页面 + 自动填手机号 - 人拖滑块(唯一需要人做的事) - 脚本自动拦截 ticket → 发短信 - 人输入验证码 → 自动提交 → 保存 auth - 反检测: 隐藏 webdriver 标记、模拟 iPhone 设备、逐字输入 - 多 selector 兼容(微店不同版本 DOM 结构) - 自动截图 debug(失败时) - auth_service.py: 重写,集成 v4 方案 - login_with_password(): 密码登录(全自动) - login_with_sms(): 短信登录(人机协作) - 保存 Playwright storage_state + 精简 cookies JSON - accounts.py 路由: 新增 /login_sms/<id> 接口 - 密码登录和短信登录两条路径 - 状态轮询支持新的交互状态 - accounts.html 模板: - 新增「短信登录」按钮 - 确认弹窗提醒用户需要浏览器交互
169 lines
7.4 KiB
HTML
169 lines
7.4 KiB
HTML
{% extends "base.html" %}
|
||
{% block content %}
|
||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
<h4 class="mb-0">账号管理</h4>
|
||
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addModal">
|
||
<i class="bi bi-plus-lg"></i> 添加账号
|
||
</button>
|
||
</div>
|
||
<div class="card">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover mb-0">
|
||
<thead><tr>
|
||
<th>ID</th><th>名称</th><th>手机号</th><th>登录状态</th><th>更新时间</th><th>操作</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
{% for a in accounts %}
|
||
<tr>
|
||
<td>{{ a.id }}</td>
|
||
<td>{{ a.name }}</td>
|
||
<td>{{ a.phone[:3] }}****{{ a.phone[-4:] if a.phone|length > 4 else '' }}</td>
|
||
<td>
|
||
{% if a.is_logged_in %}
|
||
<span class="badge bg-success">已登录</span>
|
||
{% elif a.login_msg == '登录中...' %}
|
||
<span class="badge bg-warning">
|
||
<span class="spinner-border spinner-border-sm" style="width:.7rem;height:.7rem"></span> 登录中...
|
||
</span>
|
||
{% else %}
|
||
<span class="badge bg-secondary" title="{{ a.login_msg or '' }}">未登录</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>{{ a.updated_at }}</td>
|
||
<td>
|
||
<button class="btn btn-outline-primary btn-sm" onclick="doLogin({{ a.id }}, this)">
|
||
<i class="bi bi-key"></i> 密码登录
|
||
</button>
|
||
<button class="btn btn-outline-info btn-sm" onclick="doSmsLogin({{ a.id }}, this)">
|
||
<i class="bi bi-chat-dots"></i> 短信登录
|
||
</button>
|
||
<button class="btn btn-outline-danger btn-sm" onclick="deleteAccount({{ a.id }})">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
{% if not accounts %}
|
||
<tr><td colspan="6" class="text-center text-muted py-4">暂无账号,点击右上角添加</td></tr>
|
||
{% endif %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<div class="modal fade" id="addModal" tabindex="-1">
|
||
<div class="modal-dialog"><div class="modal-content">
|
||
<div class="modal-header"><h5 class="modal-title">添加微店账号</h5></div>
|
||
<div class="modal-body">
|
||
<form id="addForm">
|
||
<div class="mb-3">
|
||
<label class="form-label">备注名称</label>
|
||
<input type="text" class="form-control" name="name" required placeholder="如:主号、小号1">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">手机号</label>
|
||
<input type="tel" class="form-control" name="phone" required placeholder="微店登录手机号">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">密码</label>
|
||
<input type="password" class="form-control" name="password" required placeholder="微店登录密码">
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button class="btn btn-primary" onclick="addAccount()">添加</button>
|
||
</div>
|
||
</div></div>
|
||
</div>
|
||
{% endblock %}
|
||
{% block scripts %}
|
||
<script>
|
||
function addAccount() {
|
||
var form = document.getElementById('addForm');
|
||
var data = new FormData(form);
|
||
var btn = event.target;
|
||
btn.disabled = true;
|
||
btn.textContent = '添加中...';
|
||
fetch('/accounts/add', { method: 'POST', body: data })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d.success) {
|
||
bootstrap.Modal.getInstance(document.getElementById('addModal')).hide();
|
||
form.reset();
|
||
location.reload();
|
||
} else {
|
||
alert(d.msg);
|
||
btn.disabled = false;
|
||
btn.textContent = '添加';
|
||
}
|
||
})
|
||
.catch(function() { btn.disabled = false; btn.textContent = '添加'; });
|
||
}
|
||
function doLogin(id, btn) {
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> 登录中...';
|
||
var badge = btn.closest('tr').querySelector('.badge');
|
||
if (badge) { badge.className = 'badge bg-warning'; badge.textContent = '登录中...'; }
|
||
fetch('/accounts/login/' + id, { method: 'POST' })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function() { pollStatus(id, btn); })
|
||
.catch(function() { btn.disabled = false; btn.innerHTML = '<i class="bi bi-key"></i> 密码登录'; });
|
||
}
|
||
function doSmsLogin(id, btn) {
|
||
if (!confirm('短信登录需要您在弹出的浏览器中拖动滑块,并在服务器终端输入验证码。确定继续?')) return;
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> 等待交互...';
|
||
var badge = btn.closest('tr').querySelector('.badge');
|
||
if (badge) { badge.className = 'badge bg-info'; badge.textContent = '等待人机交互...'; }
|
||
fetch('/accounts/login_sms/' + id, { method: 'POST' })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d.success) {
|
||
alert(d.msg);
|
||
pollStatus(id, btn);
|
||
} else {
|
||
alert(d.msg);
|
||
btn.disabled = false;
|
||
btn.innerHTML = '<i class="bi bi-chat-dots"></i> 短信登录';
|
||
}
|
||
})
|
||
.catch(function() { btn.disabled = false; btn.innerHTML = '<i class="bi bi-chat-dots"></i> 短信登录'; });
|
||
}
|
||
function pollStatus(id, btn) {
|
||
var interval = setInterval(function() {
|
||
fetch('/accounts/status/' + id)
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d.done) {
|
||
clearInterval(interval);
|
||
var row = btn ? btn.closest('tr') : null;
|
||
if (row) {
|
||
var badge = row.querySelector('.badge');
|
||
if (d.is_logged_in) {
|
||
badge.className = 'badge bg-success';
|
||
badge.textContent = '已登录';
|
||
} else {
|
||
badge.className = 'badge bg-danger';
|
||
badge.textContent = '失败';
|
||
badge.title = d.login_msg;
|
||
}
|
||
}
|
||
if (btn) {
|
||
btn.disabled = false;
|
||
btn.innerHTML = btn.textContent.includes('短信') ?
|
||
'<i class="bi bi-chat-dots"></i> 短信登录' :
|
||
'<i class="bi bi-key"></i> 密码登录';
|
||
}
|
||
}
|
||
});
|
||
}, 2000);
|
||
}
|
||
function deleteAccount(id) {
|
||
if (!confirm('确定删除此账号?相关任务也会被删除。')) return;
|
||
fetch('/accounts/delete/' + id, { method: 'POST' })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) { if (d.success) location.reload(); });
|
||
}
|
||
</script>
|
||
{% endblock %}
|