-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathweb.go
More file actions
29 lines (27 loc) · 45.4 KB
/
Copy pathweb.go
File metadata and controls
29 lines (27 loc) · 45.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main
const loginHTML = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>登录 · QQ Bot Push</title><style>
:root{--ink:#101828;--muted:#667085;--line:#e4e7ec;--primary:#2563eb;--primary2:#4f7cff;--soft:#eff4ff}*{box-sizing:border-box}html,body{margin:0;min-height:100%;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;color:var(--ink)}body{background:#f8fafc}.login-shell{min-height:100vh;display:grid;grid-template-columns:minmax(420px,1.08fr) minmax(430px,.92fr)}.visual{position:relative;padding:52px clamp(42px,7vw,112px);overflow:hidden;background:linear-gradient(145deg,#f8fbff 0%,#f4f8ff 48%,#eaf2ff 100%);display:flex;flex-direction:column}.visual:before,.visual:after{content:"";position:absolute;border-radius:50%;filter:blur(2px)}.visual:before{width:430px;height:430px;right:-180px;bottom:-170px;background:radial-gradient(circle,rgba(72,128,255,.21),transparent 68%)}.visual:after{width:320px;height:320px;left:-170px;top:30%;background:radial-gradient(circle,rgba(103,164,255,.13),transparent 70%)}.brand{display:flex;align-items:center;gap:12px;position:relative;z-index:1}.brand-mark{width:40px;height:40px;display:grid;place-items:center;border-radius:11px;background:linear-gradient(135deg,#2563eb,#557df5);color:#fff;font-size:20px;font-weight:900;box-shadow:0 10px 24px rgba(37,99,235,.25)}.brand strong{display:block;font-size:17px}.brand small{display:block;margin-top:3px;color:#98a2b3;font-size:9px;font-weight:800;letter-spacing:1.5px}.hero{position:relative;z-index:1;margin:auto 0;max-width:590px}.eyebrow{color:#3269db;font-size:11px;font-weight:900;letter-spacing:1.7px}.hero h1{font-size:clamp(40px,5.2vw,67px);line-height:1.08;letter-spacing:-2.5px;margin:18px 0 22px}.hero h1 span{color:#2b6de8}.hero>p{font-size:15px;line-height:1.9;color:#667085;max-width:550px}.features{display:grid;gap:18px;margin-top:42px}.feature{display:flex;gap:13px;align-items:center}.feature-icon{width:34px;height:34px;display:grid;place-items:center;border-radius:9px;background:rgba(255,255,255,.75);border:1px solid rgba(37,99,235,.08);color:#2b6de8;box-shadow:0 7px 20px rgba(31,65,130,.07)}.feature strong{display:block;font-size:13px}.feature span{display:block;color:#8b95a6;font-size:11px;margin-top:4px}.copyright{position:relative;z-index:1;color:#a7b0bf;font-size:11px}.login-side{display:grid;place-items:center;padding:40px;background:rgba(255,255,255,.94)}.login-card{width:min(100%,430px);padding:42px;border:1px solid var(--line);border-radius:18px;background:#fff;box-shadow:0 24px 70px rgba(30,56,100,.1)}.label-top{color:#3269db;font-size:10px;font-weight:900;letter-spacing:1.7px}.login-card h2{font-size:28px;margin:15px 0 8px}.login-card .sub{color:#8a94a6;margin:0 0 30px;font-size:13px}.field{margin-bottom:18px}.field label{display:block;font-size:12px;font-weight:700;color:#475467;margin-bottom:8px}.input-wrap{height:46px;display:flex;align-items:center;border:1px solid #dce1e8;border-radius:9px;padding:0 13px;transition:.18s}.input-wrap:focus-within{border-color:#8aacfa;box-shadow:0 0 0 4px #eef4ff}.input-wrap span{color:#98a2b3;font-size:17px;margin-right:10px}.input-wrap input{width:100%;border:0;outline:0;font:inherit;color:#101828}.submit{width:100%;height:46px;border:0;border-radius:9px;background:linear-gradient(90deg,#2e7ae8,#3f91f3);color:#fff;font:inherit;font-weight:800;cursor:pointer;box-shadow:0 10px 20px rgba(46,122,232,.2);margin-top:5px}.submit:hover{filter:brightness(.97)}.submit:disabled{opacity:.6}.error{min-height:20px;margin-top:15px;color:#d92d20;font-size:12px;text-align:center}.security{text-align:center;color:#98a2b3;font-size:11px;margin:20px 0 0}@media(max-width:860px){.login-shell{grid-template-columns:1fr}.visual{display:none}.login-side{min-height:100vh;padding:24px}.login-card{padding:32px 26px}}
</style></head><body><div class="login-shell"><section class="visual"><div class="brand"><div class="brand-mark">Q</div><div><strong>QQ Bot Push</strong><small>MESSAGE WORKSPACE</small></div></div><div class="hero"><div class="eyebrow">MULTI-BOT MESSAGE DELIVERY</div><h1>让每条消息,<br><span>准确抵达。</span></h1><p>把多个 QQ 机器人接入同一个工作区,清晰管理机器人、推送目标与 API Key。</p><div class="features"><div class="feature"><div class="feature-icon">✓</div><div><strong>统一管理</strong><span>一个工作区连接多个 QQ 机器人</span></div></div><div class="feature"><div class="feature-icon">⌁</div><div><strong>连接清晰</strong><span>实时查看每个 Gateway 的运行状态</span></div></div><div class="feature"><div class="feature-icon">⚿</div><div><strong>安全推送</strong><span>登录密码与开放 API Key 完全分离</span></div></div></div></div><div class="copyright">© 2026 QQ Bot Push Workspace</div></section><section class="login-side"><form class="login-card" id="loginForm"><div class="label-top">WELCOME BACK</div><h2>欢迎回来</h2><p class="sub">登录后继续管理你的机器人与消息推送。</p><div class="field"><label for="username">用户名</label><div class="input-wrap"><span>♙</span><input id="username" autocomplete="username" placeholder="请输入用户名" required></div></div><div class="field"><label for="password">密码</label><div class="input-wrap"><span>♢</span><input id="password" type="password" autocomplete="current-password" placeholder="请输入密码" required></div></div><button class="submit" id="submitButton">登录工作区 →</button><div class="error" id="error"></div><p class="security">登录会话使用 HttpOnly Cookie,API 推送使用独立 API Key</p></form></section></div><script>const form=document.getElementById('loginForm'),button=document.getElementById('submitButton'),error=document.getElementById('error');form.addEventListener('submit',async event=>{event.preventDefault();button.disabled=true;error.textContent='';try{const response=await fetch('/api/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:username.value.trim(),password:password.value})});const data=await response.json().catch(()=>({}));if(!response.ok)throw Error(data.error||'登录失败');location.href='/'}catch(e){error.textContent=e.message}finally{button.disabled=false}});</script></body></html>`
const dashboardHTML = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>QQ Bot Push · 工作区</title><style>
:root{--ink:#101828;--muted:#667085;--subtle:#98a2b3;--line:#e5e9f0;--bg:#f7f9fc;--paper:#fff;--blue:#2563eb;--blue2:#3b82f6;--blue-soft:#eef4ff;--green:#16a36a;--green-soft:#ecfdf3;--red:#d92d20;--red-soft:#fff1f0;--amber:#d9820b;--shadow:0 12px 34px rgba(20,42,80,.065)}*{box-sizing:border-box}html{scroll-behavior:smooth}html,body{margin:0;min-height:100%;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;color:var(--ink);background:var(--bg);font-size:14px}button,input,select,textarea{font:inherit}.layout{display:flex;min-height:100vh}.sidebar{position:fixed;inset:0 auto 0 0;width:236px;background:#fff;border-right:1px solid var(--line);padding:27px 17px 18px;display:flex;flex-direction:column;z-index:20}.brand{display:flex;align-items:center;gap:11px;padding:0 9px}.brand-mark{width:37px;height:37px;border-radius:10px;background:linear-gradient(135deg,#2563eb,#4f7df4);display:grid;place-items:center;color:#fff;font-size:18px;font-weight:900;box-shadow:0 9px 18px rgba(37,99,235,.22)}.brand strong{display:block;font-size:16px}.brand small{display:block;font-size:8px;color:#98a2b3;letter-spacing:1.4px;margin-top:3px;font-weight:800}.nav-label{font-size:9px;color:#a8b0bd;letter-spacing:1.4px;font-weight:900;margin:39px 12px 10px}.nav{display:grid;gap:5px}.nav button{width:100%;border:0;background:transparent;color:#667085;border-radius:8px;padding:11px 12px;text-align:left;display:flex;align-items:center;gap:11px;font-weight:650;cursor:pointer}.nav button:hover,.nav button.active{background:var(--blue-soft);color:var(--blue)}.nav-icon{width:18px;height:18px;display:grid;place-items:center;font-size:16px}.side-bottom{margin-top:auto}.user-chip{padding:12px;border:1px solid var(--line);border-radius:9px;display:flex;align-items:center;gap:10px}.avatar{width:31px;height:31px;border-radius:8px;display:grid;place-items:center;background:#eef4ff;color:#2563eb;font-weight:850}.user-chip strong{display:block;font-size:12px}.user-chip span{display:block;color:#98a2b3;font-size:10px;margin-top:3px}.logout{width:100%;margin-top:8px;border:0;background:transparent;border-radius:8px;padding:10px 12px;color:#7d8999;text-align:left;cursor:pointer}.logout:hover{background:#fff3f2;color:var(--red)}.main{margin-left:236px;min-width:0;flex:1}.topbar{height:74px;padding:0 36px;background:rgba(255,255,255,.92);border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:10;backdrop-filter:blur(12px)}.breadcrumb{font-size:10px;color:#9ba5b4;letter-spacing:.7px;margin-bottom:5px}.topbar h1{font-size:18px;margin:0}.top-actions{display:flex;align-items:center;gap:9px}.icon-btn,.secondary,.primary,.danger{border-radius:8px;cursor:pointer;font-weight:700}.icon-btn{width:36px;height:36px;border:1px solid var(--line);background:#fff;color:#667085}.icon-btn:hover,.secondary:hover{border-color:#b9ccf7;color:var(--blue);background:var(--blue-soft)}.primary{border:0;background:var(--blue);color:#fff;padding:10px 15px;box-shadow:0 7px 16px rgba(37,99,235,.18)}.primary:hover{background:#1d55d1}.secondary{border:1px solid var(--line);background:#fff;color:#58677b;padding:9px 13px}.danger{border:1px solid #f1c7c3;background:#fff;color:var(--red);padding:8px 12px}.danger:hover{background:var(--red-soft)}.content{max-width:1420px;margin:0 auto;padding:34px 36px 55px}.view{display:none}.view.active{display:block}.hero{display:flex;align-items:end;justify-content:space-between;gap:24px;margin-bottom:27px}.eyebrow{font-size:9px;color:var(--blue);font-weight:900;letter-spacing:1.5px;margin-bottom:9px}.hero h2{font-size:29px;letter-spacing:-.5px;margin:0}.hero p{margin:10px 0 0;color:#7a8799;line-height:1.7}.live-pill{display:flex;align-items:center;gap:8px;padding:9px 12px;border:1px solid #cdebdc;background:#f5fcf8;color:#16845d;border-radius:8px;font-size:11px;font-weight:800}.dot{width:7px;height:7px;border-radius:50%;background:#aab4c2}.dot.online{background:#18ad75;box-shadow:0 0 0 4px #e3f7ee}.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-bottom:20px}.stat,.card{background:#fff;border:1px solid var(--line);border-radius:11px;box-shadow:var(--shadow)}.stat{padding:18px}.stat-head{display:flex;align-items:center;justify-content:space-between;color:#7f8b9d;font-size:11px;font-weight:750}.stat-icon{width:29px;height:29px;border-radius:7px;display:grid;place-items:center;background:#eef4ff;color:#2563eb}.stat-value{font-size:25px;font-weight:850;margin-top:16px}.stat-note{color:#98a2b3;font-size:10px;margin-top:6px}.section-grid{display:grid;grid-template-columns:1.45fr .85fr;gap:17px}.card-head{padding:20px 22px 0;display:flex;align-items:flex-start;justify-content:space-between;gap:15px}.card-title{font-size:14px;font-weight:850;margin:0}.card-desc{font-size:11px;color:#8b97a8;margin:6px 0 0;line-height:1.55}.card-body{padding:19px 22px 22px}.bot-health{display:grid;gap:10px}.health-row{display:grid;grid-template-columns:36px 1fr auto;align-items:center;gap:11px;padding:12px;border:1px solid #edf0f4;border-radius:9px}.bot-avatar{width:36px;height:36px;border-radius:9px;display:grid;place-items:center;background:#eff4ff;color:#2563eb;font-weight:850}.health-row strong{display:block;font-size:12px}.health-row small{display:block;color:#98a2b3;font-size:10px;margin-top:4px}.badge{display:inline-flex;align-items:center;gap:6px;border-radius:6px;padding:5px 8px;font-size:9px;font-weight:850}.badge.green{background:var(--green-soft);color:var(--green)}.badge.gray{background:#f2f4f7;color:#7b8797}.badge.blue{background:var(--blue-soft);color:var(--blue)}.badge.red{background:var(--red-soft);color:var(--red)}.empty{padding:30px 12px;text-align:center;color:#98a2b3;font-size:11px}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:17px}.toolbar p{margin:7px 0 0;color:#8591a2;font-size:11px}.table-card{overflow:hidden}.table-wrap{overflow:auto}.table{width:100%;border-collapse:collapse;min-width:720px}.table th{text-align:left;padding:11px 16px;border-bottom:1px solid var(--line);background:#fbfcfe;color:#8d98a8;font-size:9px;letter-spacing:.8px;text-transform:uppercase}.table td{padding:14px 16px;border-bottom:1px solid #eef1f5;font-size:11px;vertical-align:middle}.table tr:last-child td{border-bottom:0}.name{font-size:12px;font-weight:800}.meta{color:#98a2b3;font-size:10px;margin-top:4px}.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:#667085;max-width:260px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.row-actions{display:flex;justify-content:flex-end;gap:6px}.mini-btn{border:0;background:transparent;color:#7f8a9a;padding:5px 7px;border-radius:5px;cursor:pointer;font-size:10px}.mini-btn:hover{background:#f2f5f9;color:var(--blue)}.mini-btn.red:hover{background:var(--red-soft);color:var(--red)}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:15px}.field.full{grid-column:1/-1}.field label{display:block;color:#59687b;font-size:10px;font-weight:800;margin-bottom:7px}.field input,.field select,.field textarea{width:100%;border:1px solid #dce2ea;background:#fff;border-radius:8px;outline:0;color:#101828}.field input,.field select{height:41px;padding:0 11px}.field textarea{min-height:118px;padding:11px;resize:vertical;line-height:1.55}.field input:focus,.field select:focus,.field textarea:focus{border-color:#93b3fa;box-shadow:0 0 0 3px #eff4ff}.form-actions{grid-column:1/-1;display:flex;align-items:center;justify-content:flex-end;gap:9px;margin-top:3px}.notice{font-size:11px;min-height:16px;color:#7d8999}.notice.ok{color:var(--green)}.notice.error{color:var(--red)}.push-layout{display:grid;grid-template-columns:minmax(0,1.02fr) minmax(410px,.98fr);gap:17px;align-items:start}.sticky-card{position:sticky;top:94px}.api-doc{display:grid;gap:14px}.endpoint{display:flex;align-items:center;gap:11px;border:1px solid var(--line);border-radius:9px;background:#fbfcfe;padding:11px}.method{padding:4px 8px;border-radius:5px;background:#29b84a;color:#fff;font-size:10px;font-weight:900}.endpoint code,.code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;color:#233a63}.doc-grid{display:grid;grid-template-columns:1fr 1fr;gap:11px}.doc-box{border:1px solid var(--line);border-radius:9px;padding:14px;background:#fff}.doc-box strong{display:block;font-size:11px;margin-bottom:8px}.doc-box p{font-size:10px;color:#68778b;line-height:1.65;margin:0}.header-code{margin-top:10px;border:1px solid #dfe5ed;background:#fbfcfe;border-radius:7px;padding:9px;display:flex;justify-content:space-between;gap:10px}.params{width:100%;border-collapse:collapse}.params th,.params td{text-align:left;border-bottom:1px solid #edf0f4;padding:9px 8px;font-size:9px}.params th{color:#8d98a8}.required{color:#ef4444;border:1px solid #fecaca;border-radius:4px;padding:2px 5px}.code-box{position:relative;border:1px solid var(--line);border-radius:9px;background:#fbfcfe;overflow:hidden}.code-head{padding:10px 12px;border-bottom:1px solid var(--line);font-size:10px;font-weight:800}.code-box pre{margin:0;padding:14px;white-space:pre-wrap;word-break:break-word;line-height:1.6}.copy{position:absolute;right:9px;top:7px;border:0;background:transparent;color:#6d83aa;cursor:pointer;font-size:10px}.api-key-once{border:1px solid #b9d0ff;background:#f3f7ff;border-radius:9px;padding:13px;margin-top:13px}.api-key-once strong{font-size:11px}.api-key-once code{display:block;margin-top:8px;word-break:break-all}.modal-backdrop{position:fixed;inset:0;background:rgba(16,24,40,.38);display:none;align-items:center;justify-content:center;padding:20px;z-index:50;backdrop-filter:blur(3px)}.modal-backdrop.open{display:flex}.modal{width:min(100%,560px);background:#fff;border-radius:14px;border:1px solid var(--line);box-shadow:0 24px 80px rgba(14,30,60,.2)}.modal-head{padding:20px 22px 0;display:flex;justify-content:space-between;align-items:start}.modal-head h3{margin:0;font-size:16px}.modal-close{border:0;background:transparent;color:#8b96a6;font-size:20px;cursor:pointer}.modal-body{padding:20px 22px 22px}.secret-note{font-size:10px;color:#98a2b3;line-height:1.6}.mobile-menu{display:none}@media(max-width:1080px){.stats{grid-template-columns:repeat(2,1fr)}.section-grid,.push-layout{grid-template-columns:1fr}.sticky-card{position:static}.doc-grid{grid-template-columns:1fr}}@media(max-width:760px){.sidebar{transform:translateX(-100%);transition:.2s}.sidebar.open{transform:translateX(0);box-shadow:15px 0 40px rgba(20,40,80,.14)}.main{margin-left:0}.mobile-menu{display:block}.topbar{height:68px;padding:0 16px}.content{padding:25px 16px 42px}.hero{display:block}.live-pill{display:inline-flex;margin-top:15px}.hero h2{font-size:25px}.stats{gap:9px}.stat{padding:15px}.form-grid{grid-template-columns:1fr}.field.full,.form-actions{grid-column:1}.toolbar{align-items:flex-start}.toolbar>div:last-child{display:flex;gap:7px}.topbar .primary{display:none}}
</style></head><body><div class="layout"><aside class="sidebar" id="sidebar"><div class="brand"><div class="brand-mark">Q</div><div><strong>QQ Bot Push</strong><small>MESSAGE WORKSPACE</small></div></div><div class="nav-label">工作区</div><nav class="nav"><button data-view="overview" class="active"><span class="nav-icon">⌂</span>概览</button><button data-view="bots"><span class="nav-icon">◉</span>机器人 Bot</button><button data-view="push"><span class="nav-icon">➤</span>消息推送</button><button data-view="account"><span class="nav-icon">⚙</span>账户设置</button></nav><div class="side-bottom"><div class="user-chip"><div class="avatar" id="avatar">A</div><div><strong id="sideUsername">管理员</strong><span>工作区管理员</span></div></div><button class="logout" onclick="location.href='/logout'">↪ 退出登录</button></div></aside><div class="main"><header class="topbar"><div style="display:flex;align-items:center;gap:12px"><button class="icon-btn mobile-menu" id="mobileMenu">☰</button><div><div class="breadcrumb" id="breadcrumb">QQ BOT PUSH / 概览</div><h1 id="pageTitle">概览</h1></div></div><div class="top-actions"><button class="icon-btn" title="刷新" onclick="refreshCurrent()">↻</button><button class="primary" onclick="openBotModal()">+ 添加机器人</button></div></header><main class="content"><section class="view active" id="view-overview"><div class="hero"><div><div class="eyebrow">WORKSPACE OVERVIEW</div><h2>机器人运行概览</h2><p>统一查看已接入机器人与实时 Gateway 运行状态。</p></div><div class="live-pill"><span class="dot online"></span>服务运行中</div></div><div class="stats"><div class="stat"><div class="stat-head"><span>已接入机器人</span><span class="stat-icon">◉</span></div><div class="stat-value" id="statBots">--</div><div class="stat-note">工作区内全部机器人</div></div><div class="stat"><div class="stat-head"><span>运行中的机器人</span><span class="stat-icon">⌁</span></div><div class="stat-value" id="statRunning">--</div><div class="stat-note">Gateway 当前已连接</div></div><div class="stat"><div class="stat-head"><span>推送目标</span><span class="stat-icon">◎</span></div><div class="stat-value" id="statTargets">--</div><div class="stat-note">已发现或手动添加的目标</div></div><div class="stat"><div class="stat-head"><span>服务运行时间</span><span class="stat-icon">◷</span></div><div class="stat-value" id="statUptime">--</div><div class="stat-note">当前进程持续运行时间</div></div></div><div class="section-grid"><article class="card"><div class="card-head"><div><h3 class="card-title">机器人连接状态</h3><p class="card-desc">每个机器人独立维护 QQ WebSocket Gateway。</p></div><span class="badge blue">实时状态</span></div><div class="card-body"><div class="bot-health" id="overviewBots"></div></div></article><article class="card"><div class="card-head"><div><h3 class="card-title">快速开始</h3><p class="card-desc">完成以下步骤即可通过 API 推送消息。</p></div></div><div class="card-body"><div class="health-row"><div class="bot-avatar">1</div><div><strong>添加机器人</strong><small>配置 AppID 与 AppSecret</small></div></div><div class="health-row"><div class="bot-avatar">2</div><div><strong>绑定目标</strong><small>将 OpenID 与 QQ 号对应</small></div></div><div class="health-row"><div class="bot-avatar">3</div><div><strong>创建 API Key</strong><small>使用独立密钥调用推送接口</small></div></div></div></article></div></section><section class="view" id="view-bots"><div class="toolbar"><div><div class="eyebrow">BOT MANAGEMENT</div><h2 style="margin:0;font-size:25px">机器人管理</h2><p>添加并管理多个 QQ 官方机器人,每个机器人拥有独立连接状态。</p></div><button class="primary" onclick="openBotModal()">+ 添加机器人</button></div><article class="card table-card"><div class="table-wrap"><table class="table"><thead><tr><th>机器人</th><th>AppID</th><th>连接状态</th><th>推送目标</th><th>创建时间</th><th></th></tr></thead><tbody id="botsBody"></tbody></table><div class="empty" id="botsEmpty" hidden>还没有机器人,点击“添加机器人”开始接入。</div></div></article></section><section class="view" id="view-push"><div class="toolbar"><div><div class="eyebrow">MESSAGE DELIVERY</div><h2 style="margin:0;font-size:25px">消息推送</h2><p>选择 API Key、机器人和已绑定 QQ 号的推送目标。</p></div><button class="secondary" onclick="openTargetModal()">+ 添加推送目标</button></div><div class="push-layout"><div><article class="card"><div class="card-head"><div><h3 class="card-title">发送测试消息</h3><p class="card-desc">后台测试发送使用当前登录会话;业务系统调用下方 API 时必须使用 API Key。</p></div><span class="badge blue">官方 API</span></div><div class="card-body"><div class="form-grid"><div class="field"><label>现有 API Key</label><select id="pushKey"></select></div><div class="field"><label>选择机器人</label><select id="pushBot" onchange="loadPushTargets()"></select></div><div class="field full"><label>选择推送目标</label><select id="pushTarget" onchange="updateCurl()"></select></div><div class="field full"><label>消息内容</label><textarea id="pushContent" placeholder="输入要推送的消息内容"></textarea></div><div class="form-actions"><span class="notice" id="pushNotice" style="margin-right:auto"></span><button class="primary" id="sendButton" onclick="sendMessage()">发送消息 →</button></div></div></div></article><article class="card" style="margin-top:17px"><div class="card-head"><div><h3 class="card-title">推送目标与 QQ 号绑定</h3><p class="card-desc">QQ 官方 API 使用 OpenID;这里维护便于识别和 API 调用的 QQ 号映射。</p></div><button class="secondary" onclick="openTargetModal()">添加目标</button></div><div class="table-wrap" style="margin-top:12px"><table class="table" style="min-width:660px"><thead><tr><th>目标</th><th>机器人</th><th>QQ 号绑定</th><th>OpenID</th><th></th></tr></thead><tbody id="targetsBody"></tbody></table><div class="empty" id="targetsEmpty" hidden>暂无推送目标。用户给机器人发送消息后可自动发现 OpenID,再编辑绑定 QQ 号。</div></div></article></div><aside class="card sticky-card"><div class="card-head"><div><div class="eyebrow">OPENAPI</div><h3 class="card-title">私聊推送 API</h3><p class="card-desc">在业务服务中调用接口,把消息发送到已绑定的目标。</p></div><span class="badge green">稳定版</span></div><div class="card-body api-doc"><div class="endpoint"><span class="method">POST</span><code id="endpointURL">/api/openapi/v1/push/private</code></div><div class="doc-grid"><div class="doc-box"><strong>⚿ 请求鉴权</strong><p>使用账户设置中创建的 API Key,通过请求头传入。密钥只在创建时完整显示。</p><div class="header-code"><code>X-Notice-OpenAPI-Key</code><span style="color:#98a2b3;font-size:9px">API Key</span></div></div><div class="doc-box"><strong>➤ 请求格式</strong><p>请求体使用 JSON。HTTP 成功不代表平台一定发送成功,请同时检查返回字段。</p><div class="header-code"><code>application/json</code><span style="color:#98a2b3;font-size:9px">MessageLog</span></div></div></div><div class="code-box"><div class="code-head">请求参数</div><table class="params"><tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr><tr><td><code>bot_id</code></td><td>String</td><td><span class="required">是</span></td><td>目标机器人 ID</td></tr><tr><td><code>target</code></td><td>String</td><td><span class="required">是</span></td><td>目标 ID、绑定 QQ 号或 OpenID</td></tr><tr><td><code>content</code></td><td>String</td><td><span class="required">是</span></td><td>要推送的消息内容</td></tr></table></div><div class="code-box"><button class="copy" onclick="copyCurl()">复制</button><div class="code-head">调用示例 · cURL</div><pre class="code" id="curlCode"></pre></div><div class="code-box"><div class="code-head">成功响应 · HTTP 200</div><pre class="code">{
"code": 0,
"message": "success",
"data": {
"status": "SUCCESS",
"bot_id": "bot-id",
"target": "123456789"
}
}</pre></div></div></aside></div></section><section class="view" id="view-account"><div class="toolbar"><div><div class="eyebrow">ACCOUNT & SECURITY</div><h2 style="margin:0;font-size:25px">账户设置</h2><p>管理网页登录密码和业务推送使用的 API Key。</p></div></div><div class="section-grid"><article class="card"><div class="card-head"><div><h3 class="card-title">API Key 管理</h3><p class="card-desc">推送接口使用 API Key,不使用网页登录密码。密钥只在创建时显示一次。</p></div><button class="primary" onclick="createAPIKey()">+ 创建 API Key</button></div><div class="card-body"><div id="newKeyBox"></div><div class="table-wrap"><table class="table" style="min-width:560px"><thead><tr><th>名称</th><th>Key 前缀</th><th>创建时间</th><th>最后使用</th><th></th></tr></thead><tbody id="keysBody"></tbody></table><div class="empty" id="keysEmpty" hidden>尚未创建 API Key。</div></div></div></article><article class="card"><div class="card-head"><div><h3 class="card-title">修改登录密码</h3><p class="card-desc">修改后所有网页登录会话会失效,需要重新登录。</p></div></div><div class="card-body"><div class="form-grid"><div class="field full"><label>当前密码</label><input id="currentPassword" type="password" autocomplete="current-password"></div><div class="field full"><label>新密码</label><input id="newPassword" type="password" autocomplete="new-password" placeholder="至少 8 个字符"></div><div class="field full"><label>确认新密码</label><input id="confirmPassword" type="password" autocomplete="new-password"></div><div class="form-actions"><span class="notice" id="passwordNotice" style="margin-right:auto"></span><button class="primary" onclick="changePassword()">保存新密码</button></div></div></div></article></div></section></main></div></div><div class="modal-backdrop" id="botModal"><div class="modal"><div class="modal-head"><div><h3>添加机器人</h3><p class="card-desc">凭据只保存在服务端数据文件中。</p></div><button class="modal-close" onclick="closeModal('botModal')">×</button></div><div class="modal-body"><div class="form-grid"><div class="field full"><label>机器人名称</label><input id="botName" placeholder="例如:通知机器人"></div><div class="field full"><label>AppID</label><input id="botAppID" autocomplete="off"></div><div class="field full"><label>AppSecret</label><input id="botSecret" type="password" autocomplete="new-password"></div><div class="form-actions"><span class="notice" id="botNotice" style="margin-right:auto"></span><button class="secondary" onclick="closeModal('botModal')">取消</button><button class="primary" onclick="addBot()">保存并连接</button></div></div></div></div></div><div class="modal-backdrop" id="targetModal"><div class="modal"><div class="modal-head"><div><h3 id="targetModalTitle">添加推送目标</h3><p class="card-desc">将 QQ Bot OpenID 与便于识别的 QQ 号绑定。</p></div><button class="modal-close" onclick="closeModal('targetModal')">×</button></div><div class="modal-body"><input id="editTargetID" type="hidden"><div class="form-grid"><div class="field"><label>机器人</label><select id="targetBot"></select></div><div class="field"><label>目标类型</label><select id="targetKind"><option value="c2c">QQ 私聊</option><option value="group">QQ群聊</option></select></div><div class="field"><label>显示名称</label><input id="targetName" placeholder="例如:管理员"></div><div class="field"><label>绑定 QQ 号 / 群号</label><input id="targetQQ" placeholder="例如:123456789"></div><div class="field full"><label>OpenID</label><input id="targetOpenID" placeholder="QQ Gateway 事件中获取的 OpenID"></div><div class="field full"><label>OneBot ID(可选)</label><input id="targetOneBot" placeholder="兼容 OneBot v11 的 user_id / group_id"></div><div class="form-actions"><span class="notice" id="targetNotice" style="margin-right:auto"></span><button class="secondary" onclick="closeModal('targetModal')">取消</button><button class="primary" onclick="saveTarget()">保存目标</button></div></div></div></div></div><script>
const $=id=>document.getElementById(id),state={bots:[],targets:[],keys:[],status:null,account:null,currentView:'overview'};const esc=value=>String(value??'').replace(/[&<>"']/g,ch=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));const api=async(path,options={})=>{const response=await fetch(path,{...options,headers:{'Content-Type':'application/json',...(options.headers||{})}});if(response.status===401){location.href='/login';throw Error('登录已失效')}if(response.status===204)return null;const data=await response.json().catch(()=>({}));if(!response.ok)throw Error(data.error||data.message||response.status);return data};const fmt=value=>value?new Date(value).toLocaleString('zh-CN'):'—';function notice(id,text,type=''){const node=$(id);node.textContent=text;node.className='notice '+type}document.querySelectorAll('[data-view]').forEach(button=>button.onclick=()=>showView(button.dataset.view));function showView(name){state.currentView=name;document.querySelectorAll('.view').forEach(v=>v.classList.remove('active'));$('view-'+name).classList.add('active');document.querySelectorAll('[data-view]').forEach(b=>b.classList.toggle('active',b.dataset.view===name));const titles={overview:'概览',bots:'机器人 Bot',push:'消息推送',account:'账户设置'};$('pageTitle').textContent=titles[name];$('breadcrumb').textContent='QQ BOT PUSH / '+titles[name];$('sidebar').classList.remove('open');refreshCurrent()}$('mobileMenu').onclick=()=>$('sidebar').classList.toggle('open');function openModal(id){$(id).classList.add('open')}function closeModal(id){$(id).classList.remove('open')}function openBotModal(){notice('botNotice','');$('botName').value='';$('botAppID').value='';$('botSecret').value='';openModal('botModal')}async function init(){await Promise.all([loadStatus(),loadAccount(),loadTargets()]);renderAll()}async function loadStatus(){const data=await api('/api/status');state.status=data;state.bots=data.bots||[]}async function loadAccount(){const data=await api('/api/account');state.account=data;state.keys=data.api_keys||[];$('sideUsername').textContent=data.username||'管理员';$('avatar').textContent=(data.username||'A').slice(0,1).toUpperCase()}async function loadTargets(){const data=await api('/api/targets');state.targets=data.targets||[]}async function refreshCurrent(){try{if(state.currentView==='account')await loadAccount();else await Promise.all([loadStatus(),loadTargets()]);renderAll()}catch(e){console.error(e)}}function renderAll(){renderOverview();renderBots();renderSelects();renderTargets();renderKeys();updateCurl()}function renderOverview(){const s=state.status||{};$('statBots').textContent=s.bot_count??state.bots.length;$('statRunning').textContent=s.running_bot_count??0;$('statTargets').textContent=s.targets??state.targets.length;$('statUptime').textContent=s.uptime||'--';$('overviewBots').innerHTML=state.bots.map(bot=>{const online=!!bot.gateway?.connected;return '<div class="health-row"><div class="bot-avatar">'+esc((bot.name||'Q').slice(0,1))+'</div><div><strong>'+esc(bot.name)+'</strong><small>AppID '+esc(bot.app_id)+' · '+bot.targets+' 个目标</small></div><span class="badge '+(online?'green':'gray')+'"><span class="dot '+(online?'online':'')+'"></span>'+(online?'运行中':'未连接')+'</span></div>'}).join('')||'<div class="empty">还没有接入机器人</div>'}function renderBots(){$('botsBody').innerHTML=state.bots.map(bot=>{const online=!!bot.gateway?.connected;return '<tr><td><div class="name">'+esc(bot.name)+'</div><div class="meta">ID '+esc(bot.id)+'</div></td><td class="mono">'+esc(bot.app_id)+'</td><td><span class="badge '+(online?'green':'gray')+'">'+(online?'运行中':'未连接')+'</span><div class="meta">'+esc(bot.gateway?.last_error||'Gateway 正常')+'</div></td><td>'+bot.targets+'</td><td>'+fmt(bot.created_at)+'</td><td><div class="row-actions"><button class="mini-btn" onclick="toggleBot(\''+esc(bot.id)+'\','+(!bot.enabled)+')">'+(bot.enabled?'停用':'启用')+'</button><button class="mini-btn red" onclick="deleteBot(\''+esc(bot.id)+'\')">删除</button></div></td></tr>'}).join('');$('botsEmpty').hidden=state.bots.length>0}function renderSelects(){const botOptions=state.bots.map(bot=>'<option value="'+esc(bot.id)+'">'+esc(bot.name)+' · '+esc(bot.app_id)+'</option>').join('');const previous=$('pushBot').value;$('pushBot').innerHTML=botOptions||'<option value="">暂无机器人</option>';$('targetBot').innerHTML=botOptions||'<option value="">暂无机器人</option>';if(previous&&state.bots.some(b=>b.id===previous))$('pushBot').value=previous;$('pushKey').innerHTML=state.keys.filter(k=>!k.revoked).map(k=>'<option value="'+esc(k.id)+'">'+esc(k.name)+' · '+esc(k.prefix)+'••••</option>').join('')||'<option value="">请先在账户设置创建 API Key</option>';loadPushTargets()}function loadPushTargets(){const botID=$('pushBot').value;const targets=state.targets.filter(t=>t.bot_id===botID);$('pushTarget').innerHTML=targets.map(t=>'<option value="'+esc(t.id)+'">'+esc(t.name)+' · '+esc(t.qq_number||'未绑定 QQ 号')+' · '+(t.kind==='group'?'群聊':'私聊')+'</option>').join('')||'<option value="">该机器人暂无目标</option>';updateCurl()}function botName(id){return state.bots.find(b=>b.id===id)?.name||'未知机器人'}function renderTargets(){$('targetsBody').innerHTML=state.targets.map(t=>'<tr><td><div class="name">'+esc(t.name||'未命名')+'</div><div class="meta">'+(t.kind==='group'?'QQ群聊':'QQ 私聊')+'</div></td><td>'+esc(botName(t.bot_id))+'</td><td><span class="badge '+(t.qq_number?'green':'red')+'">'+esc(t.qq_number||'未绑定')+'</span></td><td class="mono" title="'+esc(t.openid)+'">'+esc(t.openid)+'</td><td><div class="row-actions"><button class="mini-btn" onclick="editTarget(\''+esc(t.id)+'\')">编辑绑定</button><button class="mini-btn red" onclick="deleteTarget(\''+esc(t.id)+'\')">删除</button></div></td></tr>').join('');$('targetsEmpty').hidden=state.targets.length>0}function renderKeys(){$('keysBody').innerHTML=state.keys.map(k=>'<tr><td><div class="name">'+esc(k.name)+'</div></td><td class="mono">'+esc(k.prefix)+'••••••••</td><td>'+fmt(k.created_at)+'</td><td>'+fmt(k.last_used_at)+'</td><td><div class="row-actions">'+(k.revoked?'<span class="badge gray">已撤销</span>':'<button class="mini-btn red" onclick="revokeKey(\''+esc(k.id)+'\')">撤销</button>')+'</div></td></tr>').join('');$('keysEmpty').hidden=state.keys.length>0}async function addBot(){notice('botNotice','保存中...');try{await api('/api/bots',{method:'POST',body:JSON.stringify({name:$('botName').value,app_id:$('botAppID').value,app_secret:$('botSecret').value})});closeModal('botModal');await loadStatus();renderAll()}catch(e){notice('botNotice',e.message,'error')}}async function toggleBot(id,enabled){try{await api('/api/bots/'+id,{method:'PATCH',body:JSON.stringify({enabled})});await loadStatus();renderAll()}catch(e){alert(e.message)}}async function deleteBot(id){if(!confirm('确定删除这个机器人?机器人存在目标时不能删除。'))return;try{await api('/api/bots/'+id,{method:'DELETE'});await Promise.all([loadStatus(),loadTargets()]);renderAll()}catch(e){alert(e.message)}}function openTargetModal(){$('targetModalTitle').textContent='添加推送目标';$('editTargetID').value='';$('targetBot').disabled=false;$('targetKind').disabled=false;$('targetName').value='';$('targetQQ').value='';$('targetOpenID').value='';$('targetOpenID').disabled=false;$('targetOneBot').value='';notice('targetNotice','');renderSelects();openModal('targetModal')}function editTarget(id){const t=state.targets.find(x=>x.id===id);if(!t)return;$('targetModalTitle').textContent='编辑 QQ 号绑定';$('editTargetID').value=t.id;$('targetBot').value=t.bot_id;$('targetBot').disabled=true;$('targetKind').value=t.kind;$('targetKind').disabled=true;$('targetName').value=t.name||'';$('targetQQ').value=t.qq_number||'';$('targetOpenID').value=t.openid;$('targetOpenID').disabled=true;$('targetOneBot').value=t.onebot_id||'';notice('targetNotice','');openModal('targetModal')}async function saveTarget(){const id=$('editTargetID').value;notice('targetNotice','保存中...');try{if(id){await api('/api/targets/'+id,{method:'PATCH',body:JSON.stringify({name:$('targetName').value,qq_number:$('targetQQ').value,onebot_id:$('targetOneBot').value})})}else{await api('/api/targets',{method:'POST',body:JSON.stringify({bot_id:$('targetBot').value,kind:$('targetKind').value,name:$('targetName').value,qq_number:$('targetQQ').value,openid:$('targetOpenID').value,onebot_id:$('targetOneBot').value})})}closeModal('targetModal');await loadTargets();renderAll()}catch(e){notice('targetNotice',e.message,'error')}}async function deleteTarget(id){if(!confirm('确定删除这个推送目标?'))return;try{await api('/api/targets/'+id,{method:'DELETE'});await loadTargets();renderAll()}catch(e){alert(e.message)}}async function sendMessage(){const botID=$('pushBot').value,targetID=$('pushTarget').value,content=$('pushContent').value.trim();if(!$('pushKey').value){notice('pushNotice','请先创建 API Key','error');return}if(!botID||!targetID||!content){notice('pushNotice','请选择机器人、目标并填写内容','error');return}$('sendButton').disabled=true;notice('pushNotice','正在发送...');try{await api('/api/push',{method:'POST',body:JSON.stringify({bot_id:botID,target_id:targetID,content})});notice('pushNotice','推送成功','ok');$('pushContent').value=''}catch(e){notice('pushNotice',e.message,'error')}finally{$('sendButton').disabled=false}}async function createAPIKey(){const name=prompt('请输入 API Key 名称,例如:生产环境');if(name===null)return;try{const data=await api('/api/account/keys',{method:'POST',body:JSON.stringify({name})});$('newKeyBox').innerHTML='<div class="api-key-once"><strong>请立即复制并妥善保存,关闭后无法再次查看:</strong><code id="newKeyValue">'+esc(data.key)+'</code><button class="secondary" style="margin-top:10px" onclick="navigator.clipboard.writeText($(\'newKeyValue\').textContent)">复制 API Key</button></div>';await loadAccount();renderAll()}catch(e){alert(e.message)}}async function revokeKey(id){if(!confirm('撤销后使用该 Key 的调用会立即失败,确定继续?'))return;try{await api('/api/account/keys/'+id,{method:'DELETE'});await loadAccount();renderAll()}catch(e){alert(e.message)}}async function changePassword(){const next=$('newPassword').value;if(next!==$('confirmPassword').value){notice('passwordNotice','两次输入的新密码不一致','error');return}try{const data=await api('/api/account/password',{method:'POST',body:JSON.stringify({current_password:$('currentPassword').value,new_password:next})});if(data.relogin)location.href='/login'}catch(e){notice('passwordNotice',e.message,'error')}}function updateCurl(){const bot=$('pushBot').value||'BOT_ID',target=state.targets.find(t=>t.id===$('pushTarget').value);const targetValue=target?.qq_number||target?.id||'TARGET';const content=$('pushContent').value.trim()||'服务已恢复';$('curlCode').textContent='curl -X POST "'+location.origin+'/api/openapi/v1/push/private" \\\n -H "X-Notice-OpenAPI-Key: YOUR_API_KEY" \\\n -H "Content-Type: application/json" \\\n --data '+JSON.stringify(JSON.stringify({bot_id:bot,target:targetValue,content}))}async function copyCurl(){await navigator.clipboard.writeText($('curlCode').textContent)}$('pushContent').addEventListener('input',updateCurl);document.querySelectorAll('.modal-backdrop').forEach(modal=>modal.addEventListener('click',e=>{if(e.target===modal)closeModal(modal.id)}));init().catch(e=>console.error(e));
</script><style>
.gateway-option{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:14px 15px;border:1px solid var(--line);border-radius:7px;background:#f8fafc;cursor:pointer}.gateway-option strong{display:block;font-size:13px}.gateway-option small{display:block;margin-top:4px;color:var(--muted);line-height:1.5}.gateway-switch{position:relative;flex:0 0 38px;width:38px;height:22px}.gateway-switch input{position:absolute;opacity:0;pointer-events:none}.gateway-switch span{position:absolute;inset:0;border-radius:11px;background:#cbd5e1;transition:.18s}.gateway-switch span:after{content:'';position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 3px #0002;transition:.18s}.gateway-switch input:checked+span{background:var(--primary)}.gateway-switch input:checked+span:after{transform:translateX(16px)}
</style><script>
const botFormActions=document.querySelector('#botModal .form-actions');
if(botFormActions){const option=document.createElement('label');option.className='gateway-option field full';option.innerHTML='<div><strong>启动 Gateway</strong><small>接收 QQ 私聊事件,自动保存目标并回复用户 OpenID。关闭后仍可使用 HTTP API 推送。</small></div><span class="gateway-switch"><input id="botGateway" type="checkbox" checked><span></span></span>';botFormActions.before(option);const desc=document.querySelector('#botModal .modal-head .card-desc');if(desc)desc.textContent='保存前会校验 QQ 官方凭据,可选择是否连接 Gateway。';const button=document.querySelector('#botModal .form-actions .primary');if(button)button.textContent='校验并添加'}
openBotModal=function(){notice('botNotice','');$('botName').value='';$('botAppID').value='';$('botSecret').value='';if($('botGateway'))$('botGateway').checked=true;openModal('botModal')};
addBot=async function(){notice('botNotice','正在校验 QQ 凭据...');try{await api('/api/bots',{method:'POST',body:JSON.stringify({name:$('botName').value,app_id:$('botAppID').value,app_secret:$('botSecret').value,gateway_enabled:$('botGateway')?.checked??true})});closeModal('botModal');await loadStatus();renderAll()}catch(e){notice('botNotice',e.message,'error')}};
toggleGateway=async function(id,gatewayEnabled){try{await api('/api/bots/'+id,{method:'PATCH',body:JSON.stringify({gateway_enabled:gatewayEnabled})});await loadStatus();renderAll()}catch(e){alert(e.message)}};
renderOverview=function(){const s=state.status||{};$('statBots').textContent=s.bot_count??state.bots.length;$('statRunning').textContent=s.running_bot_count??0;$('statTargets').textContent=s.targets??state.targets.length;$('statUptime').textContent=s.uptime||'--';$('overviewBots').innerHTML=state.bots.map(bot=>{const online=!!bot.gateway?.connected,off=!bot.gateway_enabled;const label=!bot.enabled?'未启用':off?'HTTP 推送':online?'Gateway 已连接':'Gateway 连接中';const badge=online?'green':off&&bot.enabled?'blue':'gray';return '<div class="health-row"><div class="bot-avatar">'+esc((bot.name||'Q').slice(0,1))+'</div><div><strong>'+esc(bot.name)+'</strong><small>AppID '+esc(bot.app_id)+' · '+bot.targets+' 个目标</small></div><span class="badge '+badge+'"><span class="dot '+(online?'online':'')+'"></span>'+label+'</span></div>'}).join('')||'<div class="empty">还没有接入机器人</div>'};
renderBots=function(){$('botsBody').innerHTML=state.bots.map(bot=>{const online=!!bot.gateway?.connected,off=!bot.gateway_enabled;const status=!bot.enabled?'机器人未启用':off?'Gateway 已关闭':online?'Gateway 已连接':'Gateway 未连接';const badge=online?'green':off&&bot.enabled?'blue':'gray';const detail=off&&bot.enabled?'HTTP 推送可用':online?'接收消息并自动回复 OpenID':(bot.gateway?.last_error||'正在建立连接');return '<tr><td><div class="name">'+esc(bot.name)+'</div><div class="meta">ID '+esc(bot.id)+'</div></td><td class="mono">'+esc(bot.app_id)+'</td><td><span class="badge '+badge+'">'+status+'</span><div class="meta">'+esc(detail)+'</div></td><td>'+bot.targets+'</td><td>'+fmt(bot.created_at)+'</td><td><div class="row-actions"><button class="mini-btn" onclick="toggleGateway(\''+esc(bot.id)+'\','+(!bot.gateway_enabled)+')">'+(bot.gateway_enabled?'关闭 Gateway':'启动 Gateway')+'</button><button class="mini-btn" onclick="toggleBot(\''+esc(bot.id)+'\','+(!bot.enabled)+')">'+(bot.enabled?'停用机器人':'启用机器人')+'</button><button class="mini-btn red" onclick="deleteBot(\''+esc(bot.id)+'\')">删除</button></div></td></tr>'}).join('');$('botsEmpty').hidden=state.bots.length>0};
</script></body></html>`