fix:bug
This commit is contained in:
3
.env
Normal file
3
.env
Normal file
@@ -0,0 +1,3 @@
|
||||
PORT=3000
|
||||
WECHAT_UPSTREAM_BASE_URL=http://113.44.162.180:7006
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
PORT=3000
|
||||
WECHAT_UPSTREAM_BASE_URL=http://your-wechat-server-host:port
|
||||
WECHAT_UPSTREAM_BASE_URL=http://113.44.162.180:7006
|
||||
|
||||
|
||||
# KEY = HBpEnbtj9BJZ
|
||||
# password = 408449830
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine AS build
|
||||
FROM docker.m.daocloud.io/library/node:20-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -8,10 +8,11 @@ RUN npm install
|
||||
|
||||
COPY tsconfig.json ./tsconfig.json
|
||||
COPY src ./src
|
||||
COPY public ./public
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine AS runtime
|
||||
FROM docker.m.daocloud.io/library/node:20-alpine AS runtime
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -19,9 +20,10 @@ ENV NODE_ENV=production
|
||||
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
COPY --from=build /app/dist ./dist
|
||||
COPY .env.example ./ ./
|
||||
COPY --from=build /app/public ./public
|
||||
COPY .env.example ./
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD [\"node\", \"dist/server.js\"]
|
||||
CMD ["node", "dist/server.js"]
|
||||
|
||||
|
||||
918
public/index.html
Normal file
918
public/index.html
Normal file
@@ -0,0 +1,918 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>微信登录管理面板</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--bg: #0f172a;
|
||||
--card: #020617;
|
||||
--border: #1e293b;
|
||||
--accent: #22c55e;
|
||||
--accent-soft: rgba(34, 197, 94, 0.12);
|
||||
--text: #e5e7eb;
|
||||
--muted: #9ca3af;
|
||||
--danger: #ef4444;
|
||||
--radius: 16px;
|
||||
--shadow: 0 18px 45px rgba(15, 23, 42, 0.7);
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(circle at top, #1e293b 0, #020617 55%, #000 100%);
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'SF Pro Text',
|
||||
'Segoe UI', sans-serif;
|
||||
color: var(--text);
|
||||
}
|
||||
.shell {
|
||||
width: 100%;
|
||||
max-width: 1080px;
|
||||
margin: 32px;
|
||||
border-radius: 28px;
|
||||
background: radial-gradient(circle at top left, #22c55e1f 0, #020617 48%, #000 100%);
|
||||
padding: 1px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.shell-inner {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
padding: 26px 26px 26px 26px;
|
||||
border-radius: 27px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(52, 211, 153, 0.09), transparent 55%),
|
||||
radial-gradient(circle at bottom, rgba(59, 130, 246, 0.09), transparent 55%),
|
||||
#020617f2;
|
||||
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||
backdrop-filter: blur(26px);
|
||||
}
|
||||
@media (max-width: 880px) {
|
||||
.shell-inner {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.title-main {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.pill {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border: 1px solid rgba(148, 163, 184, 0.4);
|
||||
color: var(--muted);
|
||||
}
|
||||
.title-sub {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 999px;
|
||||
background: #22c55e;
|
||||
box-shadow: 0 0 0 6px rgba(34, 197, 94, 0.25);
|
||||
}
|
||||
.status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.status-row strong {
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
.card {
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: radial-gradient(circle at top, rgba(15, 23, 42, 0.9), #020617);
|
||||
padding: 18px 18px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 12px 14px;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.form-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.field label {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.field label span {
|
||||
color: var(--danger);
|
||||
margin-left: 2px;
|
||||
}
|
||||
input,
|
||||
select {
|
||||
font: inherit;
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(51, 65, 85, 0.9);
|
||||
padding: 8px 11px;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease,
|
||||
transform 0.08s ease;
|
||||
}
|
||||
input::placeholder {
|
||||
color: rgba(148, 163, 184, 0.8);
|
||||
}
|
||||
input:focus,
|
||||
select:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px rgba(34, 197, 94, 0.55);
|
||||
background: rgba(15, 23, 42, 0.99);
|
||||
transform: translateY(-0.5px);
|
||||
}
|
||||
.checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.checkbox-row input {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
button {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: transform 0.08s ease, box-shadow 0.12s ease, background 0.12s ease,
|
||||
opacity 0.12s ease;
|
||||
}
|
||||
button.primary {
|
||||
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||
color: #022c22;
|
||||
box-shadow: 0 10px 24px rgba(34, 197, 94, 0.35);
|
||||
}
|
||||
button.secondary {
|
||||
background: rgba(15, 23, 42, 0.98);
|
||||
color: var(--text);
|
||||
border: 1px solid rgba(148, 163, 184, 0.4);
|
||||
}
|
||||
button.danger {
|
||||
background: rgba(127, 29, 29, 0.96);
|
||||
color: #fee2e2;
|
||||
border: 1px solid rgba(248, 113, 113, 0.7);
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: wait;
|
||||
}
|
||||
button:not(:disabled):hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.54);
|
||||
}
|
||||
button:not(:disabled):active {
|
||||
transform: translateY(0px) scale(0.99);
|
||||
box-shadow: 0 8px 16px rgba(15, 23, 42, 0.55);
|
||||
}
|
||||
.qr-card {
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid rgba(30, 64, 175, 0.8);
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(59, 130, 246, 0.35), transparent 60%),
|
||||
rgba(15, 23, 42, 0.98);
|
||||
padding: 18px 16px 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.qr-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.98);
|
||||
border: 1px solid rgba(148, 163, 184, 0.6);
|
||||
color: var(--muted);
|
||||
}
|
||||
.qr-box {
|
||||
margin-top: 4px;
|
||||
border-radius: 18px;
|
||||
border: 1px dashed rgba(148, 163, 184, 0.7);
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(148, 163, 184, 0.23), transparent 55%),
|
||||
radial-gradient(circle at bottom, rgba(15, 23, 42, 0.95), #020617);
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
min-height: 220px;
|
||||
}
|
||||
.qr-img {
|
||||
width: 190px;
|
||||
height: 190px;
|
||||
border-radius: 18px;
|
||||
background: #020617;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(15, 23, 42, 0.9),
|
||||
0 15px 32px rgba(15, 23, 42, 0.9);
|
||||
}
|
||||
.qr-img img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.qr-placeholder {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
.hint {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
.hint strong {
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
.log-card {
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(55, 65, 81, 0.9);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(34, 197, 94, 0.16), transparent 55%),
|
||||
rgba(15, 23, 42, 0.98);
|
||||
padding: 14px 13px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-height: 120px;
|
||||
}
|
||||
.log-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
}
|
||||
.log-body {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
max-height: 120px;
|
||||
overflow: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.log-line {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.log-line.ok {
|
||||
color: #4ade80;
|
||||
}
|
||||
.log-line.warn {
|
||||
color: #facc15;
|
||||
}
|
||||
.log-line.error {
|
||||
color: #f97373;
|
||||
}
|
||||
.tag-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.tag {
|
||||
padding: 1px 7px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(55, 65, 81, 0.9);
|
||||
background: rgba(15, 23, 42, 0.92);
|
||||
}
|
||||
.small-label {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.login-state {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.login-state.ok {
|
||||
color: #4ade80;
|
||||
}
|
||||
.login-state.pending {
|
||||
color: #facc15;
|
||||
}
|
||||
.login-state.offline {
|
||||
color: #f97373;
|
||||
}
|
||||
.login-state.unknown {
|
||||
color: var(--muted);
|
||||
}
|
||||
.result-card {
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(55, 65, 81, 0.9);
|
||||
background: rgba(15, 23, 42, 0.98);
|
||||
padding: 12px 12px 10px;
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.result-body {
|
||||
max-height: 120px;
|
||||
overflow: auto;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border-radius: 10px;
|
||||
padding: 6px 8px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.result-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.result-actions button {
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.slider-card {
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(59, 130, 246, 0.8);
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(59, 130, 246, 0.25), transparent 60%),
|
||||
rgba(15, 23, 42, 0.98);
|
||||
padding: 10px 10px 8px;
|
||||
margin-top: 12px;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.slider-frame {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(30, 64, 175, 0.8);
|
||||
background: #020617;
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="shell-inner">
|
||||
<section>
|
||||
<header>
|
||||
<div class="title">
|
||||
<div class="title-main">
|
||||
微信登录控制台
|
||||
<span class="pill">Admin · iPad / Mac 直登</span>
|
||||
</div>
|
||||
<div class="title-sub">
|
||||
按文档流程封装的可视化工具:生成二维码 · 轮询扫码 · 查看在线状态 · 一键退出登录
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="status-row">
|
||||
<span class="status-dot" id="health-dot"></span>
|
||||
<span>后端状态:<strong id="health-text">检测中...</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">登录参数</div>
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label for="key">
|
||||
账号唯一标识 <span>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="key"
|
||||
placeholder="例如:your-account-key"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="proxy">代理(可选)</label>
|
||||
<input
|
||||
id="proxy"
|
||||
placeholder="socks5://username:password@ipv4:port"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="device">登录设备类型</label>
|
||||
<select id="device">
|
||||
<option value="">自动</option>
|
||||
<option value="ipad">iPad</option>
|
||||
<option value="mac">Mac</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label> </label>
|
||||
<div class="checkbox-row">
|
||||
<input type="checkbox" id="check-proxy" />
|
||||
<label for="check-proxy">修改代理时自动检测可用性</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary" id="btn-qrcode">
|
||||
获取登录二维码
|
||||
</button>
|
||||
<button class="secondary" id="btn-check-scan">
|
||||
检测扫码状态
|
||||
</button>
|
||||
<button class="secondary" id="btn-online">
|
||||
获取在线状态
|
||||
</button>
|
||||
<button class="danger" id="btn-logout">
|
||||
退出当前账号
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tag-row">
|
||||
<span class="tag">通过 /api/auth/qrcode 代理 swagger 登录接口</span>
|
||||
<span class="tag">无需直接暴露 8069 服务</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="log-card" style="margin-top: 12px">
|
||||
<div class="log-header">
|
||||
<span>操作日志</span>
|
||||
<button class="secondary" id="btn-clear-log" style="padding: 3px 10px; font-size: 11px">
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
<div class="log-body" id="log-body"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside>
|
||||
<div class="qr-card">
|
||||
<div class="qr-header">
|
||||
<span>扫码登录二维码</span>
|
||||
<span class="badge" id="login-state-badge">未创建</span>
|
||||
</div>
|
||||
<div class="qr-box">
|
||||
<div class="qr-img" id="qr-img-box">
|
||||
<div class="qr-placeholder" id="qr-placeholder">
|
||||
点击左侧「获取登录二维码」后,将在这里显示二维码图片或登录链接信息。
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint" id="qr-hint">
|
||||
请使用微信客户端扫描二维码完成登录。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height: 12px"></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="small-label">当前账号在线状态</div>
|
||||
<div class="login-state unknown" id="login-state-text">未知</div>
|
||||
<div class="small-label" id="login-extra">尚未查询</div>
|
||||
</div>
|
||||
|
||||
<div class="result-card" id="result-card" style="display: none">
|
||||
<div class="result-actions">
|
||||
<span class="small-label">最近一次扫码状态返回数据</span>
|
||||
<button class="secondary" id="btn-copy-result">复制结果到剪贴板</button>
|
||||
</div>
|
||||
<div class="result-body" id="result-body"></div>
|
||||
</div>
|
||||
|
||||
<div class="slider-card" id="slider-card">
|
||||
<div class="small-label">新设备滑块验证(内嵌窗口)</div>
|
||||
<iframe
|
||||
id="slider-frame"
|
||||
class="slider-frame"
|
||||
src=""
|
||||
referrerpolicy="no-referrer"
|
||||
></iframe>
|
||||
<div class="small-label">
|
||||
如 iframe 无法加载,可在新标签页打开:
|
||||
<a href="http://113.44.162.180:7765/?key=408449830" target="_blank" style="color: #60a5fa">
|
||||
滑块验证页面
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
const SLIDER_VERIFY_URL = 'http://113.44.162.180:7765/?key=408449830';
|
||||
|
||||
const state = {
|
||||
pollingScan: null,
|
||||
pollingOnline: null,
|
||||
sliderOpened: false,
|
||||
};
|
||||
|
||||
function log(message, level = 'info') {
|
||||
const logBody = $('log-body');
|
||||
const line = document.createElement('div');
|
||||
line.className = 'log-line ' + (level === 'error' ? 'error' : level === 'warn' ? 'warn' : '');
|
||||
const ts = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
||||
line.textContent = `[${ts}] ${message}`;
|
||||
logBody.appendChild(line);
|
||||
logBody.scrollTop = logBody.scrollHeight;
|
||||
}
|
||||
|
||||
async function checkHealth() {
|
||||
try {
|
||||
const res = await fetch('/health', { cache: 'no-store' });
|
||||
if (!res.ok) throw new Error('status ' + res.status);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
$('health-dot').style.background = '#22c55e';
|
||||
$('health-text').textContent = '在线';
|
||||
log('后端健康检查成功: ' + JSON.stringify(data || {}));
|
||||
} catch (e) {
|
||||
$('health-dot').style.background = '#ef4444';
|
||||
$('health-text').textContent = '不可用';
|
||||
log('后端健康检查失败: ' + (e && e.message ? e.message : e), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(loading) {
|
||||
$('btn-qrcode').disabled = loading;
|
||||
$('btn-check-scan').disabled = loading;
|
||||
$('btn-online').disabled = loading;
|
||||
$('btn-logout').disabled = loading;
|
||||
}
|
||||
|
||||
function getCommonPayload() {
|
||||
const key = $('key').value.trim();
|
||||
const proxy = $('proxy').value.trim();
|
||||
const device = $('device').value;
|
||||
const check = $('check-proxy').checked;
|
||||
if (!key) {
|
||||
alert('请先填写账号唯一标识(key)');
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
key,
|
||||
proxy: proxy || undefined,
|
||||
ipadOrMac: device,
|
||||
check,
|
||||
};
|
||||
}
|
||||
|
||||
function updateLoginState(text, status = 'unknown', extra = '') {
|
||||
const el = $('login-state-text');
|
||||
el.textContent = text;
|
||||
el.className = 'login-state ' + status;
|
||||
|
||||
$('login-extra').textContent = extra || '';
|
||||
|
||||
const badge = $('login-state-badge');
|
||||
if (status === 'ok') {
|
||||
badge.textContent = '已登录 / 在线';
|
||||
} else if (status === 'pending') {
|
||||
badge.textContent = '等待扫码 / 确认中';
|
||||
} else if (status === 'offline') {
|
||||
badge.textContent = '已离线 / 未登录';
|
||||
} else {
|
||||
badge.textContent = '未知';
|
||||
}
|
||||
}
|
||||
|
||||
function renderQrFromResponse(data) {
|
||||
const box = $('qr-img-box');
|
||||
box.innerHTML = '';
|
||||
|
||||
let qrText = null;
|
||||
|
||||
if (data && typeof data === 'object') {
|
||||
if (data.qrUrl || data.qrcodeUrl || data.qr || data.url) {
|
||||
qrText = data.qrUrl || data.qrcodeUrl || data.qr || data.url;
|
||||
} else if (data.qrcode || data.qr_code) {
|
||||
qrText = data.qrcode || data.qr_code;
|
||||
}
|
||||
if (!qrText) {
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
const pre = document.createElement('pre');
|
||||
pre.textContent = json;
|
||||
pre.style.fontSize = '10px';
|
||||
pre.style.padding = '8px';
|
||||
pre.style.whiteSpace = 'pre-wrap';
|
||||
pre.style.wordBreak = 'break-all';
|
||||
box.appendChild(pre);
|
||||
$('qr-hint').textContent = '返回结果中未找到二维码字符串,请根据具体字段名在前端进行适配。';
|
||||
return;
|
||||
}
|
||||
} else if (typeof data === 'string') {
|
||||
qrText = data;
|
||||
}
|
||||
|
||||
const qrContainer = document.createElement('div');
|
||||
qrContainer.id = 'qr-code';
|
||||
box.appendChild(qrContainer);
|
||||
|
||||
try {
|
||||
// 使用前端 QRCode 库将登录链接渲染为二维码
|
||||
// @ts-ignore
|
||||
new QRCode(qrContainer, {
|
||||
text: qrText,
|
||||
width: 190,
|
||||
height: 190,
|
||||
});
|
||||
$('qr-hint').textContent = '请使用微信客户端扫描上方二维码完成登录。';
|
||||
} catch (e) {
|
||||
const fallback = document.createElement('pre');
|
||||
fallback.textContent = qrText || '';
|
||||
fallback.style.fontSize = '10px';
|
||||
fallback.style.padding = '8px';
|
||||
fallback.style.whiteSpace = 'pre-wrap';
|
||||
fallback.style.wordBreak = 'break-all';
|
||||
box.appendChild(fallback);
|
||||
$('qr-hint').textContent = '二维码生成失败,请复制链接手动生成二维码。';
|
||||
}
|
||||
}
|
||||
|
||||
async function callApi(path, options = {}) {
|
||||
const url = '/api' + path.replace(/^\/api/, '');
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers || {}),
|
||||
},
|
||||
});
|
||||
let body = null;
|
||||
try {
|
||||
body = await res.json();
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
if (!res.ok) {
|
||||
const msg = (body && (body.error || body.message)) || res.statusText || '请求失败';
|
||||
throw new Error(msg);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
async function onGetQrCode() {
|
||||
const payload = getCommonPayload();
|
||||
if (!payload) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
log('请求登录二维码...');
|
||||
const body = {
|
||||
Proxy: payload.proxy || '',
|
||||
IpadOrmac: payload.ipadOrMac || '',
|
||||
Check: !!payload.check,
|
||||
};
|
||||
const data = await callApi('/auth/qrcode', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key: payload.key, ...body }),
|
||||
});
|
||||
log('获取二维码成功');
|
||||
renderQrFromResponse(data);
|
||||
updateLoginState('等待扫码 / 确认中', 'pending', '请在 60 秒内使用微信扫码。');
|
||||
} catch (e) {
|
||||
log('获取二维码失败: ' + e.message, 'error');
|
||||
updateLoginState('二维码获取失败', 'offline', e.message || '');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onCheckScanStatus(silent = false) {
|
||||
const payload = getCommonPayload();
|
||||
if (!payload) return;
|
||||
if (!silent) setLoading(true);
|
||||
try {
|
||||
if (!silent) log('手动检测扫码状态...');
|
||||
const data = await callApi(
|
||||
`/auth/scan-status?key=${encodeURIComponent(payload.key)}`,
|
||||
);
|
||||
const text = JSON.stringify(data);
|
||||
log('扫码状态: ' + text);
|
||||
|
||||
const lower = text.toLowerCase();
|
||||
|
||||
// 显示最近一次返回的数据,方便复制
|
||||
const resultCard = $('result-card');
|
||||
const resultBody = $('result-body');
|
||||
if (resultCard && resultBody) {
|
||||
resultBody.textContent = text;
|
||||
resultCard.style.display = 'flex';
|
||||
}
|
||||
|
||||
// 自动识别「新设备验证」,展示滑块 iframe
|
||||
if (!state.sliderOpened && (text.includes('新设备') || lower.includes('new device'))) {
|
||||
state.sliderOpened = true;
|
||||
log('检测到新设备验证,打开内嵌滑块验证窗口。', 'warn');
|
||||
const sliderCard = $('slider-card');
|
||||
const sliderFrame = $('slider-frame');
|
||||
if (sliderCard && sliderFrame) {
|
||||
sliderFrame.src = SLIDER_VERIFY_URL;
|
||||
sliderCard.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
if (lower.includes('\"state\":2') || /\"state\"\\s*:\\s*2/.test(lower)) {
|
||||
updateLoginState('登录成功', 'ok', text);
|
||||
if (state.pollingScan) {
|
||||
clearInterval(state.pollingScan);
|
||||
state.pollingScan = null;
|
||||
}
|
||||
} else if (lower.includes('expired') || lower.includes('timeout')) {
|
||||
updateLoginState('二维码已过期,请重新获取', 'offline', text);
|
||||
if (state.pollingScan) {
|
||||
clearInterval(state.pollingScan);
|
||||
state.pollingScan = null;
|
||||
}
|
||||
} else {
|
||||
updateLoginState('扫码状态更新', 'pending', text);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!silent) log('检测扫码状态失败: ' + e.message, 'error');
|
||||
} finally {
|
||||
if (!silent) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onGetOnlineStatus() {
|
||||
const payload = getCommonPayload();
|
||||
if (!payload) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
log('查询账号在线状态...');
|
||||
const data = await callApi(`/auth/status?key=${encodeURIComponent(payload.key)}`);
|
||||
const text = JSON.stringify(data);
|
||||
log('在线状态: ' + text);
|
||||
const lower = text.toLowerCase();
|
||||
if (lower.includes('online') || lower.includes('true') || lower.includes('登录')) {
|
||||
updateLoginState('当前账号在线', 'ok', text);
|
||||
} else {
|
||||
updateLoginState('当前账号离线或未登录', 'offline', text);
|
||||
}
|
||||
} catch (e) {
|
||||
log('获取在线状态失败: ' + e.message, 'error');
|
||||
updateLoginState('在线状态查询失败', 'unknown', e.message || '');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onLogout() {
|
||||
const payload = getCommonPayload();
|
||||
if (!payload) return;
|
||||
if (!confirm('确定要退出该账号的微信登录吗?')) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
log('正在退出登录...');
|
||||
const data = await callApi('/auth/logout', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key: payload.key }),
|
||||
});
|
||||
log('退出登录完成: ' + JSON.stringify(data));
|
||||
updateLoginState('已退出登录', 'offline');
|
||||
if (state.pollingScan) {
|
||||
clearInterval(state.pollingScan);
|
||||
state.pollingScan = null;
|
||||
}
|
||||
} catch (e) {
|
||||
log('退出登录失败: ' + e.message, 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
$('btn-qrcode').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
onGetQrCode();
|
||||
});
|
||||
$('btn-check-scan').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
onCheckScanStatus(false);
|
||||
});
|
||||
$('btn-online').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
onGetOnlineStatus();
|
||||
});
|
||||
$('btn-logout').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
onLogout();
|
||||
});
|
||||
$('btn-clear-log').addEventListener('click', () => {
|
||||
$('log-body').innerHTML = '';
|
||||
});
|
||||
const copyBtn = $('btn-copy-result');
|
||||
if (copyBtn) {
|
||||
copyBtn.addEventListener('click', async () => {
|
||||
const body = $('result-body');
|
||||
if (!body || !body.textContent) {
|
||||
alert('当前没有可复制的结果。');
|
||||
return;
|
||||
}
|
||||
const text = body.textContent;
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
log('扫码状态结果已复制到剪贴板。', 'ok');
|
||||
alert('结果已复制到剪贴板,可直接粘贴使用。');
|
||||
} catch (e) {
|
||||
log('复制结果到剪贴板失败: ' + e.message, 'error');
|
||||
alert('复制失败,请手动选择文本复制。');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bindEvents();
|
||||
checkHealth();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,6 +2,7 @@ import express from 'express';
|
||||
import cors from 'cors';
|
||||
import morgan from 'morgan';
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
|
||||
import { authRouter } from './wechatAuth';
|
||||
|
||||
@@ -13,6 +14,9 @@ app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(morgan('dev'));
|
||||
|
||||
const publicDir = path.join(__dirname, '../public');
|
||||
app.use(express.static(publicDir));
|
||||
|
||||
app.get('/health', (_req, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user