879 lines
24 KiB
HTML
879 lines
24 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>WeChatPadPro 登录控制台</title>
|
||
<style>
|
||
:root {
|
||
--bg: #f3f6ff;
|
||
--card: #ffffff;
|
||
--line: #d9e1f6;
|
||
--text: #1f2a44;
|
||
--muted: #5f6b87;
|
||
--primary: #2356d8;
|
||
--primary-deep: #1b43ad;
|
||
--danger: #c9343f;
|
||
--ok: #14b86e;
|
||
--mono: "SFMono-Regular", Menlo, Consolas, monospace;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
|
||
color: var(--text);
|
||
background:
|
||
radial-gradient(1200px 500px at 80% -20%, #d9e7ff 0%, transparent 70%),
|
||
radial-gradient(800px 300px at 10% -10%, #eef4ff 0%, transparent 72%),
|
||
var(--bg);
|
||
min-height: 100vh;
|
||
padding: 18px;
|
||
}
|
||
|
||
.shell {
|
||
max-width: 1180px;
|
||
margin: 0 auto;
|
||
display: grid;
|
||
gap: 14px;
|
||
}
|
||
|
||
.top {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
align-items: center;
|
||
background: linear-gradient(120deg, #2448a5 0%, #2f66e3 100%);
|
||
border-radius: 14px;
|
||
padding: 16px 18px;
|
||
color: #fff;
|
||
box-shadow: 0 10px 24px rgba(35, 86, 216, 0.2);
|
||
}
|
||
|
||
.title {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.subtitle {
|
||
margin: 5px 0 0;
|
||
font-size: 13px;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.status-pill {
|
||
background: rgba(255, 255, 255, 0.16);
|
||
border: 1px solid rgba(255, 255, 255, 0.24);
|
||
border-radius: 999px;
|
||
padding: 7px 12px;
|
||
font-size: 13px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.dot {
|
||
width: 9px;
|
||
height: 9px;
|
||
border-radius: 50%;
|
||
background: #ffd25f;
|
||
box-shadow: 0 0 0 0 rgba(255, 210, 95, 0.6);
|
||
animation: pulse 1.5s infinite;
|
||
}
|
||
|
||
.dot.online {
|
||
background: var(--ok);
|
||
box-shadow: 0 0 0 0 rgba(20, 184, 110, 0.55);
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% {
|
||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.35);
|
||
}
|
||
70% {
|
||
box-shadow: 0 0 0 9px rgba(255, 255, 255, 0);
|
||
}
|
||
100% {
|
||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
|
||
}
|
||
}
|
||
|
||
.grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 14px;
|
||
}
|
||
|
||
.panel {
|
||
background: var(--card);
|
||
border: 1px solid var(--line);
|
||
border-radius: 14px;
|
||
padding: 14px;
|
||
box-shadow: 0 7px 20px rgba(30, 68, 163, 0.06);
|
||
min-height: 100%;
|
||
}
|
||
|
||
.panel h2 {
|
||
margin: 0 0 12px;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.field {
|
||
display: grid;
|
||
gap: 6px;
|
||
}
|
||
|
||
label {
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
select,
|
||
textarea {
|
||
width: 100%;
|
||
border-radius: 10px;
|
||
border: 1px solid #cbd7f7;
|
||
background: #fbfdff;
|
||
padding: 10px 11px;
|
||
color: var(--text);
|
||
font-size: 14px;
|
||
outline: none;
|
||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||
}
|
||
|
||
select:focus,
|
||
textarea:focus {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 3px rgba(35, 86, 216, 0.16);
|
||
}
|
||
|
||
.actions {
|
||
display: flex;
|
||
gap: 9px;
|
||
margin-top: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
button {
|
||
border: none;
|
||
border-radius: 10px;
|
||
padding: 9px 14px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: transform 0.06s ease, opacity 0.15s ease;
|
||
}
|
||
|
||
button:active {
|
||
transform: translateY(1px);
|
||
}
|
||
|
||
button:disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.58;
|
||
transform: none;
|
||
}
|
||
|
||
.btn-main {
|
||
color: #fff;
|
||
background: linear-gradient(120deg, var(--primary), var(--primary-deep));
|
||
}
|
||
|
||
.btn-danger {
|
||
color: #fff;
|
||
background: linear-gradient(120deg, #d73f4a, #b61e2a);
|
||
}
|
||
|
||
.qr-wrap {
|
||
border-radius: 12px;
|
||
border: 1px dashed #cad4f0;
|
||
min-height: 330px;
|
||
display: grid;
|
||
align-content: center;
|
||
justify-items: center;
|
||
gap: 8px;
|
||
background: #fcfdff;
|
||
padding: 12px;
|
||
}
|
||
|
||
.qr-img {
|
||
width: min(330px, 100%);
|
||
border: 1px solid #dde5f8;
|
||
border-radius: 12px;
|
||
background: #fff;
|
||
display: none;
|
||
}
|
||
|
||
.empty {
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
text-align: center;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.message-list {
|
||
display: grid;
|
||
gap: 9px;
|
||
max-height: 360px;
|
||
overflow: auto;
|
||
padding-right: 2px;
|
||
}
|
||
|
||
.message-card {
|
||
border-radius: 11px;
|
||
border: 1px solid #dbe4fa;
|
||
background: #fbfdff;
|
||
padding: 10px;
|
||
display: grid;
|
||
gap: 6px;
|
||
}
|
||
|
||
.message-meta {
|
||
font-family: var(--mono);
|
||
font-size: 12px;
|
||
color: #44506a;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.message-content {
|
||
font-size: 13px;
|
||
line-height: 1.42;
|
||
color: #0f172a;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
|
||
textarea {
|
||
min-height: 360px;
|
||
resize: vertical;
|
||
font-family: var(--mono);
|
||
font-size: 12px;
|
||
background: #f8fbff;
|
||
}
|
||
|
||
@media (max-width: 980px) {
|
||
.grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.top {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.status-pill {
|
||
align-self: flex-start;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main class="shell">
|
||
<section class="top">
|
||
<div>
|
||
<h1 class="title">WeChatPadPro 登录控制台</h1>
|
||
<p class="subtitle">只保留扫码登录,状态与消息自动轮询</p>
|
||
</div>
|
||
<div class="status-pill">
|
||
<span id="onlineDot" class="dot"></span>
|
||
<span id="onlineText">未连接</span>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="grid">
|
||
<section class="panel">
|
||
<h2>1. 登录配置</h2>
|
||
<div class="field">
|
||
<label for="qrEndpoint">二维码类型</label>
|
||
<select id="qrEndpoint">
|
||
<option value="GetLoginQrCodeNewX">NewX(默认)</option>
|
||
<option value="GetLoginQrCodePadX">PadX</option>
|
||
<option value="GetLoginQrCodeWin">Win</option>
|
||
<option value="GetLoginQrCodeMac">Mac</option>
|
||
<option value="GetLoginQrCodeAndroidPad">AndroidPad</option>
|
||
<option value="GetLoginQrCodeCar">Car</option>
|
||
<option value="GetLoginQrCodeA16">A16</option>
|
||
<option value="GetLoginQrCodeNew">New</option>
|
||
</select>
|
||
</div>
|
||
<div class="actions">
|
||
<button id="startLoginBtn" class="btn-main">生成授权二维码</button>
|
||
<button id="clearLoginBtn" class="btn-danger">清除登录状态</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel">
|
||
<h2>2. 扫码窗口</h2>
|
||
<div class="qr-wrap">
|
||
<img id="qrImg" class="qr-img" alt="二维码" />
|
||
<div id="qrHint" class="empty">点击“生成授权二维码”后在这里显示二维码</div>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
|
||
<section class="panel">
|
||
<h2>状态日志</h2>
|
||
<textarea id="statusView" readonly></textarea>
|
||
</section>
|
||
</main>
|
||
|
||
<script>
|
||
const state = {
|
||
baseUrl: "http://127.0.0.1:18238",
|
||
authKey: "",
|
||
qrData: null,
|
||
isOnline: false,
|
||
statusKnown: false,
|
||
statusTimer: null,
|
||
messageTimer: null,
|
||
lastStatusSig: "",
|
||
lastStatusAlertSig: "",
|
||
lastStatusReqError: "",
|
||
seenMessageKeys: new Set(),
|
||
persistTimer: null
|
||
};
|
||
|
||
const STATUS_INTERVAL_MS = 2000;
|
||
const MESSAGE_INTERVAL_MS = 2000;
|
||
const MESSAGE_COUNT = 30;
|
||
const QR_HINT_DEFAULT = "点击“生成授权二维码”后在这里显示二维码";
|
||
const QR_HINT_ONLINE = "当前账号已登录,二维码已隐藏。掉线或清除登录状态后可重新生成。";
|
||
const QR_HINT_CHECKING = "检测到已保存会话,正在校验登录状态,请稍候。";
|
||
const QR_ENDPOINT_PRESET = [
|
||
{ value: "GetLoginQrCodeNewX", label: "NewX(默认)" },
|
||
{ value: "GetLoginQrCodePadX", label: "PadX" },
|
||
{ value: "GetLoginQrCodeWin", label: "Win" },
|
||
{ value: "GetLoginQrCodeMac", label: "Mac" },
|
||
{ value: "GetLoginQrCodeAndroidPad", label: "AndroidPad" },
|
||
{ value: "GetLoginQrCodeCar", label: "Car" },
|
||
{ value: "GetLoginQrCodeA16", label: "A16" },
|
||
{ value: "GetLoginQrCodeNew", label: "New" }
|
||
];
|
||
|
||
const el = (id) => document.getElementById(id);
|
||
|
||
function log(line) {
|
||
const area = el("statusView");
|
||
const stamp = new Date().toLocaleTimeString();
|
||
area.value = `[${stamp}] ${line}\n` + area.value;
|
||
}
|
||
|
||
async function api(path, method = "GET", payload = null) {
|
||
const options = { method, headers: {} };
|
||
if (payload) {
|
||
options.headers["Content-Type"] = "application/json";
|
||
options.body = JSON.stringify(payload);
|
||
}
|
||
const resp = await fetch(path, options);
|
||
let data;
|
||
try {
|
||
data = await resp.json();
|
||
} catch (err) {
|
||
throw new Error(`响应不是 JSON: ${resp.status}`);
|
||
}
|
||
if (!resp.ok) {
|
||
throw new Error(data.error || `请求失败: ${resp.status}`);
|
||
}
|
||
return data;
|
||
}
|
||
|
||
function setOnline(isOnline) {
|
||
state.isOnline = Boolean(isOnline);
|
||
const dot = el("onlineDot");
|
||
const text = el("onlineText");
|
||
if (state.isOnline) {
|
||
dot.classList.add("online");
|
||
text.textContent = "已在线";
|
||
} else {
|
||
dot.classList.remove("online");
|
||
text.textContent = "未在线";
|
||
}
|
||
refreshLoginUiByState();
|
||
}
|
||
|
||
function hideQr(hintText = QR_HINT_DEFAULT) {
|
||
const img = el("qrImg");
|
||
const hint = el("qrHint");
|
||
img.src = "";
|
||
img.style.display = "none";
|
||
hint.textContent = hintText;
|
||
}
|
||
|
||
function refreshLoginUiByState() {
|
||
const btn = el("startLoginBtn");
|
||
if (!btn) {
|
||
return;
|
||
}
|
||
|
||
if (state.authKey && !state.statusKnown) {
|
||
btn.disabled = true;
|
||
btn.textContent = "状态检测中...";
|
||
hideQr(QR_HINT_CHECKING);
|
||
return;
|
||
}
|
||
|
||
if (state.isOnline) {
|
||
btn.disabled = true;
|
||
btn.textContent = "当前已登录";
|
||
hideQr(QR_HINT_ONLINE);
|
||
return;
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = "生成授权二维码";
|
||
if (state.qrData?.qrCodeBase64) {
|
||
renderQr(state.qrData);
|
||
} else {
|
||
hideQr(QR_HINT_DEFAULT);
|
||
}
|
||
}
|
||
|
||
function renderQr(qrData) {
|
||
const img = el("qrImg");
|
||
const hint = el("qrHint");
|
||
const base64 = qrData?.qrCodeBase64 || "";
|
||
if (!base64) {
|
||
hideQr(QR_HINT_DEFAULT);
|
||
return;
|
||
}
|
||
img.src = base64.startsWith("data:") ? base64 : `data:image/jpeg;base64,${base64}`;
|
||
img.style.display = "block";
|
||
hint.textContent = "请用微信扫码并在手机确认登录";
|
||
}
|
||
|
||
function clearMessageList() {
|
||
const list = el("messageList");
|
||
if (list) {
|
||
list.innerHTML = `<div class="empty">暂无消息</div>`;
|
||
}
|
||
state.seenMessageKeys.clear();
|
||
}
|
||
|
||
function appendMessages(items) {
|
||
if (!Array.isArray(items) || items.length === 0) {
|
||
return;
|
||
}
|
||
const list = el("messageList");
|
||
if (!list) {
|
||
return;
|
||
}
|
||
const firstEmpty = list.querySelector(".empty");
|
||
if (firstEmpty) {
|
||
firstEmpty.remove();
|
||
}
|
||
|
||
items.forEach((item) => {
|
||
const dedupeKey =
|
||
item.id && item.id !== "-"
|
||
? `id:${item.id}|type:${item.type ?? "-"}`
|
||
: [
|
||
item.id ?? "-",
|
||
item.type ?? "-",
|
||
item.time ?? "-",
|
||
item.from ?? "-",
|
||
item.to ?? "-",
|
||
String(item.content ?? "").slice(0, 120)
|
||
].join("|");
|
||
|
||
if (state.seenMessageKeys.has(dedupeKey)) {
|
||
return;
|
||
}
|
||
state.seenMessageKeys.add(dedupeKey);
|
||
if (state.seenMessageKeys.size > 3500) {
|
||
const first = state.seenMessageKeys.values().next().value;
|
||
state.seenMessageKeys.delete(first);
|
||
}
|
||
|
||
const card = document.createElement("article");
|
||
card.className = "message-card";
|
||
|
||
const meta = document.createElement("div");
|
||
meta.className = "message-meta";
|
||
meta.innerHTML =
|
||
`<span>id: ${item.id ?? "-"}</span>` +
|
||
`<span>type: ${item.type ?? "-"}</span>` +
|
||
`<span>from: ${item.from ?? "-"}</span>` +
|
||
`<span>to: ${item.to ?? "-"}</span>`;
|
||
|
||
const content = document.createElement("div");
|
||
content.className = "message-content";
|
||
content.textContent = item.content ?? "-";
|
||
|
||
card.appendChild(meta);
|
||
card.appendChild(content);
|
||
list.prepend(card);
|
||
});
|
||
}
|
||
|
||
function queuePersistSession() {
|
||
if (state.persistTimer) {
|
||
clearTimeout(state.persistTimer);
|
||
}
|
||
state.persistTimer = setTimeout(() => {
|
||
persistSession(true);
|
||
}, 250);
|
||
}
|
||
|
||
async function persistSession(silent = true) {
|
||
try {
|
||
await api("/api/session", "POST", {
|
||
baseUrl: state.baseUrl,
|
||
authKey: state.authKey,
|
||
qrData: state.qrData || {},
|
||
qrEndpoint: el("qrEndpoint").value
|
||
});
|
||
} catch (err) {
|
||
if (!silent) {
|
||
log(`保存会话失败: ${err.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
function ensureStatusPolling() {
|
||
if (state.statusTimer) {
|
||
return;
|
||
}
|
||
state.statusTimer = setInterval(() => {
|
||
checkStatus(false);
|
||
}, STATUS_INTERVAL_MS);
|
||
log(`已启动自动状态轮询(${STATUS_INTERVAL_MS / 1000}秒)。`);
|
||
}
|
||
|
||
function ensureMessagePolling() {
|
||
if (state.messageTimer) {
|
||
return;
|
||
}
|
||
state.messageTimer = setInterval(() => {
|
||
pollMessages(false);
|
||
}, MESSAGE_INTERVAL_MS);
|
||
log(`已启动自动消息轮询(${MESSAGE_INTERVAL_MS / 1000}秒)。`);
|
||
}
|
||
|
||
function stopMessagePolling(silent = false) {
|
||
if (!state.messageTimer) {
|
||
return;
|
||
}
|
||
clearInterval(state.messageTimer);
|
||
state.messageTimer = null;
|
||
if (!silent) {
|
||
log("已停止自动消息轮询。");
|
||
}
|
||
}
|
||
|
||
function buildStatusSig(data) {
|
||
const checkCode = data?.check?.Code;
|
||
const checkState = data?.check?.Data?.state ?? "-";
|
||
const onlineCode = data?.online?.Code;
|
||
const onlineText = data?.online?.Text || "-";
|
||
return `${checkCode}|${checkState}|${onlineCode}|${onlineText}`;
|
||
}
|
||
|
||
function logStatusIfChanged(data) {
|
||
const sig = buildStatusSig(data);
|
||
if (sig === state.lastStatusSig) {
|
||
return;
|
||
}
|
||
state.lastStatusSig = sig;
|
||
const checkCode = data?.check?.Code;
|
||
const checkText = data?.check?.Text || "-";
|
||
const checkState = data?.check?.Data?.state;
|
||
const onlineCode = data?.online?.Code;
|
||
const onlineText = data?.online?.Text || "-";
|
||
log(
|
||
`状态: CheckLoginStatus=${checkCode}, state=${checkState ?? "-"}(${checkText}) | ` +
|
||
`GetLoginStatus=${onlineCode}(${onlineText})`
|
||
);
|
||
}
|
||
|
||
function reportStatusAlerts(data) {
|
||
if (state.isOnline) {
|
||
state.lastStatusAlertSig = "";
|
||
return;
|
||
}
|
||
|
||
const alerts = [];
|
||
const checkCode = Number(data?.check?.Code);
|
||
const checkState = data?.check?.Data?.state;
|
||
const checkText = String(data?.check?.Text || "").trim();
|
||
const onlineCode = Number(data?.online?.Code);
|
||
const onlineText = String(data?.online?.Text || "").trim();
|
||
|
||
if (onlineCode !== 200 && onlineText) {
|
||
alerts.push(`账号未在线: ${onlineCode}(${onlineText})`);
|
||
}
|
||
if (onlineText.includes("版本过低")) {
|
||
alerts.push("检测到版本过低,请切换二维码类型(Win/Mac/AndroidPad)或升级后端组件。");
|
||
}
|
||
if (onlineText.includes("重新登录") || onlineText.includes("MMLoginStateNoLogin")) {
|
||
alerts.push("账号状态需要重新登录,请点击“生成授权二维码”重新扫码。");
|
||
}
|
||
if (checkCode === -3) {
|
||
alerts.push("当前流程需要验证码,若持续出现请改用支持验证码的登录流程。");
|
||
}
|
||
if (checkCode === 300 || checkCode === -2) {
|
||
alerts.push("授权或二维码状态异常(可能已失效),请重新生成授权二维码。");
|
||
}
|
||
if (checkCode < 0 && checkCode !== -2 && checkCode !== -3) {
|
||
alerts.push(`扫码状态异常: ${checkCode}(${checkText || "-"})`);
|
||
}
|
||
if (checkState === 4 && onlineCode !== 200) {
|
||
alerts.push("登录卡在新设备验证阶段,请在手机确认或换二维码类型重试。");
|
||
}
|
||
|
||
const sig = alerts.join("||");
|
||
if (!sig) {
|
||
state.lastStatusAlertSig = "";
|
||
return;
|
||
}
|
||
if (sig === state.lastStatusAlertSig) {
|
||
return;
|
||
}
|
||
state.lastStatusAlertSig = sig;
|
||
alerts.forEach((line) => log(`异常: ${line}`));
|
||
}
|
||
|
||
function rebuildQrEndpointOptions(serverEndpoints) {
|
||
const select = el("qrEndpoint");
|
||
const current = select.value;
|
||
const allowed =
|
||
Array.isArray(serverEndpoints) && serverEndpoints.length > 0
|
||
? new Set(serverEndpoints)
|
||
: null;
|
||
|
||
const options = [];
|
||
QR_ENDPOINT_PRESET.forEach((item) => {
|
||
if (!allowed || allowed.has(item.value)) {
|
||
options.push(item);
|
||
}
|
||
});
|
||
|
||
if (allowed) {
|
||
Array.from(allowed)
|
||
.sort()
|
||
.forEach((name) => {
|
||
if (!options.some((x) => x.value === name)) {
|
||
options.push({ value: name, label: name });
|
||
}
|
||
});
|
||
}
|
||
|
||
if (options.length === 0) {
|
||
options.push(...QR_ENDPOINT_PRESET);
|
||
}
|
||
|
||
select.innerHTML = "";
|
||
options.forEach((item) => {
|
||
const opt = document.createElement("option");
|
||
opt.value = item.value;
|
||
opt.textContent = item.label;
|
||
select.appendChild(opt);
|
||
});
|
||
|
||
if (current && options.some((x) => x.value === current)) {
|
||
select.value = current;
|
||
return;
|
||
}
|
||
if (options.some((x) => x.value === "GetLoginQrCodeNewX")) {
|
||
select.value = "GetLoginQrCodeNewX";
|
||
return;
|
||
}
|
||
select.value = options[0].value;
|
||
}
|
||
|
||
async function loadConfig() {
|
||
try {
|
||
const cfg = await api("/api/config");
|
||
if (cfg.baseUrl) {
|
||
state.baseUrl = cfg.baseUrl;
|
||
}
|
||
rebuildQrEndpointOptions(cfg.qrEndpoints);
|
||
|
||
if (cfg.hasDefaultAdminKey) {
|
||
log("已检测到后端默认 ADMIN_KEY,可直接生成授权二维码。");
|
||
} else {
|
||
log("后端没有默认 ADMIN_KEY,生成授权二维码会失败。请先在服务端 deploy/.env 配置。");
|
||
}
|
||
if (cfg.hasSavedSession) {
|
||
log("检测到已保存会话,正在恢复。");
|
||
}
|
||
} catch (err) {
|
||
log(`读取配置失败: ${err.message}`);
|
||
}
|
||
}
|
||
|
||
async function restoreSession() {
|
||
try {
|
||
const data = await api("/api/session");
|
||
const session = data.session || {};
|
||
if (session.baseUrl) {
|
||
state.baseUrl = session.baseUrl;
|
||
}
|
||
if (session.authKey) {
|
||
state.authKey = session.authKey;
|
||
state.statusKnown = false;
|
||
log("已恢复上次会话(authKey/baseUrl)。");
|
||
}
|
||
if (session.qrEndpoint) {
|
||
el("qrEndpoint").value = session.qrEndpoint;
|
||
}
|
||
if (session.qrData && typeof session.qrData === "object") {
|
||
state.qrData = session.qrData;
|
||
}
|
||
refreshLoginUiByState();
|
||
} catch (err) {
|
||
log(`恢复会话失败: ${err.message}`);
|
||
}
|
||
}
|
||
|
||
async function startLogin() {
|
||
if (state.isOnline) {
|
||
log("当前账号已在线,已禁止重复生成授权二维码。");
|
||
return;
|
||
}
|
||
try {
|
||
const payload = {
|
||
baseUrl: state.baseUrl,
|
||
qrEndpoint: el("qrEndpoint").value
|
||
};
|
||
const data = await api("/api/login/start", "POST", payload);
|
||
state.authKey = data.authKey || "";
|
||
state.qrData = data.qrData || null;
|
||
state.statusKnown = true;
|
||
if (data.qrEndpoint) {
|
||
el("qrEndpoint").value = data.qrEndpoint;
|
||
}
|
||
renderQr(state.qrData || {});
|
||
clearMessageList();
|
||
await persistSession(true);
|
||
log(`已生成 authKey: ${state.authKey}`);
|
||
ensureStatusPolling();
|
||
await checkStatus(true);
|
||
} catch (err) {
|
||
log(`生成授权二维码失败: ${err.message}`);
|
||
}
|
||
}
|
||
|
||
async function checkStatus(logError = true) {
|
||
if (!state.authKey) {
|
||
state.statusKnown = true;
|
||
state.lastStatusReqError = "";
|
||
setOnline(false);
|
||
stopMessagePolling(true);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const url =
|
||
`/api/login/status?authKey=${encodeURIComponent(state.authKey)}` +
|
||
`&baseUrl=${encodeURIComponent(state.baseUrl)}`;
|
||
const data = await api(url);
|
||
state.statusKnown = true;
|
||
state.lastStatusReqError = "";
|
||
setOnline(Boolean(data.isOnline));
|
||
logStatusIfChanged(data);
|
||
reportStatusAlerts(data);
|
||
queuePersistSession();
|
||
|
||
if (state.isOnline) {
|
||
if (state.qrData) {
|
||
state.qrData = null;
|
||
queuePersistSession();
|
||
}
|
||
ensureMessagePolling();
|
||
pollMessages(false);
|
||
} else {
|
||
stopMessagePolling(true);
|
||
}
|
||
} catch (err) {
|
||
const message = `查询状态失败: ${err.message}`;
|
||
if (logError || state.lastStatusReqError !== message) {
|
||
log(message);
|
||
}
|
||
state.lastStatusReqError = message;
|
||
state.statusKnown = false;
|
||
setOnline(false);
|
||
stopMessagePolling(true);
|
||
}
|
||
}
|
||
|
||
async function pollMessages(logError = true) {
|
||
if (!state.authKey || !state.isOnline) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const payload = {
|
||
baseUrl: state.baseUrl,
|
||
authKey: state.authKey,
|
||
count: MESSAGE_COUNT
|
||
};
|
||
const data = await api("/api/message/poll", "POST", payload);
|
||
if (data.received > 0) {
|
||
appendMessages(data.items);
|
||
log(`收到 ${data.received} 条消息。`);
|
||
}
|
||
} catch (err) {
|
||
if (logError) {
|
||
log(`自动拉取消息失败: ${err.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function clearLoginState() {
|
||
try {
|
||
const resp = await api("/api/login/clear", "POST", {
|
||
baseUrl: state.baseUrl,
|
||
authKey: state.authKey
|
||
});
|
||
const logout = resp.logout || {};
|
||
|
||
state.authKey = "";
|
||
state.qrData = null;
|
||
state.statusKnown = true;
|
||
state.lastStatusSig = "";
|
||
state.lastStatusAlertSig = "";
|
||
state.lastStatusReqError = "";
|
||
setOnline(false);
|
||
stopMessagePolling(true);
|
||
clearMessageList();
|
||
|
||
if (logout.attempted) {
|
||
if (logout.ok) {
|
||
log(`远端登出成功: ${logout.method} ${logout.path}`);
|
||
} else if (logout.error) {
|
||
log(`远端登出结果: ${logout.error}`);
|
||
}
|
||
}
|
||
log("已清除登录状态与持久化会话。");
|
||
} catch (err) {
|
||
log(`清除登录状态失败: ${err.message}`);
|
||
}
|
||
}
|
||
|
||
el("startLoginBtn").addEventListener("click", startLogin);
|
||
el("clearLoginBtn").addEventListener("click", clearLoginState);
|
||
el("qrEndpoint").addEventListener("change", queuePersistSession);
|
||
|
||
(async () => {
|
||
await loadConfig();
|
||
await restoreSession();
|
||
refreshLoginUiByState();
|
||
ensureStatusPolling();
|
||
if (state.authKey) {
|
||
await checkStatus(true);
|
||
} else {
|
||
state.statusKnown = true;
|
||
refreshLoginUiByState();
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|