zcbot/web/static/wechat_bind.html

153 lines
6.0 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>绑定微信 · zcbot</title>
<style>
:root { --fg:#1f2328; --muted:#656d76; --line:#d0d7de; --accent:#a01e1e; --bg:#f6f8fa; }
* { box-sizing: border-box; }
body { font: 15px/1.6 system-ui, "Segoe UI", "Microsoft YaHei", sans-serif;
color: var(--fg); margin: 0; background: var(--bg); }
.wrap { max-width: 520px; margin: 40px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid var(--line); border-radius: 12px;
padding: 24px; box-shadow: 0 1px 2px rgba(0,0,0,.04); }
h1 { font-size: 20px; margin: 0 0 4px; }
.sub { color: var(--muted); margin: 0 0 20px; font-size: 13px; }
button { font: inherit; cursor: pointer; border: 1px solid var(--line);
background: #fff; border-radius: 8px; padding: 8px 16px; }
button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
button:disabled { opacity: .5; cursor: default; }
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 14px; }
#qrbox { text-align: center; margin: 18px 0; }
#qrbox img { width: 240px; height: 240px; border: 1px solid var(--line); border-radius: 8px; }
.hint { color: var(--muted); font-size: 13px; margin-top: 8px; }
.status { padding: 10px 12px; border-radius: 8px; font-size: 14px; margin-top: 14px; }
.ok { background: #e6f4ea; color: #1a7f37; }
.err { background: #ffebe9; color: #cf222e; }
.wait { background: #fff8c5; color: #7d4e00; }
code { background: var(--bg); padding: 1px 5px; border-radius: 4px; font-size: 12px; }
.steps { font-size: 13px; color: var(--muted); margin: 14px 0 0; padding-left: 18px; }
</style>
</head>
<body>
<div class="wrap"><div class="card">
<h1>绑定微信(ClawBot)</h1>
<p class="sub">绑定后可在个人微信「微信 ClawBot」里直接和 zcbot 对话;近 24h 内有互动时,定时简报 / 结果也能主动推给你。</p>
<div id="state" class="status wait">加载中…</div>
<div class="row">
<button id="btn-bind" class="primary">绑定 / 重新绑定</button>
<button id="btn-test" disabled>发送测试消息</button>
<button id="btn-unbind" disabled>解绑</button>
</div>
<div id="qrbox" hidden>
<img id="qrimg" alt="微信扫码二维码">
<div class="hint" id="qrhint">用手机微信「扫一扫」上面的二维码,在手机上确认授权。</div>
</div>
<ol class="steps">
<li>需个人微信 8.0.70+ 且已灰度到「ClawBot」插件(设置→插件)。</li>
<li>绑定成功后,先在微信里给「微信 ClawBot」发一句话,主动推送才会开启(24h 窗口)。</li>
</ol>
</div></div>
<script>
const LS_TOKEN = "zcbot.token";
const token = () => localStorage.getItem(LS_TOKEN) || "";
const $ = (id) => document.getElementById(id);
let polling = false;
async function api(path, opts = {}) {
opts.headers = Object.assign({ "Authorization": "Bearer " + token() }, opts.headers || {});
const r = await fetch(path, opts);
if (r.status === 204) return null;
const body = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(body.detail || ("HTTP " + r.status));
return body;
}
function setState(cls, msg) {
const el = $("state");
el.className = "status " + cls;
el.textContent = msg;
}
async function refresh() {
if (!token()) { setState("err", "未登录:请先在 zcbot 主页登录,再打开本页。"); return; }
try {
const b = await api("/v1/wechat/bind");
if (b.bound) {
const push = b.can_push ? "可主动推送" : "需在微信里发条消息以开启主动推送";
setState("ok", "已绑定" + (b.user_im_id ? "" : "(待首次开口)") + " · " + push);
$("btn-test").disabled = false;
$("btn-unbind").disabled = false;
} else {
setState("wait", "尚未绑定。点「绑定」生成二维码,用手机微信扫。");
$("btn-test").disabled = true;
$("btn-unbind").disabled = true;
}
} catch (e) { setState("err", "查询失败:" + e.message); }
}
async function bindFlow() {
if (polling) return;
polling = true;
$("btn-bind").disabled = true;
try {
while (polling) {
const q = await api("/v1/wechat/bind/qrcode", { method: "POST" });
$("qrimg").src = q.qr_png;
$("qrbox").hidden = false;
setState("wait", "等待扫码…(二维码约 1 分钟过期,会自动换新)");
// 轮询该二维码状态
let expired = false;
while (polling && !expired) {
let s;
try { s = await api("/v1/wechat/bind/status?qrcode_id=" + encodeURIComponent(q.qrcode_id)); }
catch (e) { await sleep(2000); continue; }
if (s.status === "confirmed") {
$("qrbox").hidden = true;
polling = false;
setState("ok", "绑定成功!现在去微信「微信 ClawBot」发一句话试试。");
await refresh();
return;
}
if (s.status === "expired") { expired = true; break; }
await sleep(1000);
}
}
} catch (e) {
setState("err", "绑定出错:" + e.message);
} finally {
polling = false;
$("btn-bind").disabled = false;
}
}
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
$("btn-bind").onclick = bindFlow;
$("btn-unbind").onclick = async () => {
polling = false;
if (!confirm("确定解绑微信?")) return;
try { await api("/v1/wechat/bind", { method: "DELETE" }); $("qrbox").hidden = true; await refresh(); }
catch (e) { setState("err", "解绑失败:" + e.message); }
};
$("btn-test").onclick = async () => {
$("btn-test").disabled = true;
try {
const r = await api("/v1/wechat/test", { method: "POST" });
if (r.ok) setState("ok", "测试消息已发送,去微信查收。");
else setState("err", "推送未送达(" + r.reason + ")。多半是超过 24h 没在微信互动:发条消息再试。");
} catch (e) { setState("err", "测试失败:" + e.message); }
finally { $("btn-test").disabled = false; }
};
refresh();
</script>
</body>
</html>