Files
weidian/templates/accounts.html
openclaw def06c6360 feat: 短信验证码登录 + v4 人机协作方案
核心改动:
- 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 模板:
  - 新增「短信登录」按钮
  - 确认弹窗提醒用户需要浏览器交互
2026-03-31 15:18:02 +08:00

169 lines
7.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 %}