diff --git a/pyproject.toml b/pyproject.toml index a8e75a5..4f06a2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ dependencies = [ "cryptography>=50.0,<51.0", "qrcode>=7.0,<8.3", "python-multipart>=0.0.9", - "jinja2>=3.1,<4.0", "filetype>=1.2.0", "aria2p>=0.12.1", "mutagen>=1.48.1", diff --git a/static/site_auth.js b/static/site_auth.js deleted file mode 100644 index 75b3dbc..0000000 --- a/static/site_auth.js +++ /dev/null @@ -1,1434 +0,0 @@ -/* ============================================================ - * 云村小助手 - 站点账号系统前端逻辑(按次计费版) - * 未经授权不得复制、修改或用于商业用途 - * ============================================================ */ -(function () { - 'use strict'; - - /* Promise.finally 老内核 polyfill(与 index.html 同一份标准实现—— - index.html 的脚本先加载,这里再兜底一次确保独立引用时也安全) */ - if (!Promise.prototype.finally) { - Promise.prototype.finally = function (cb) { - var P = this.constructor || Promise; - return this.then( - function (v) { return P.resolve(cb()).then(function () { return v; }); }, - function (e) { return P.resolve(cb()).then(function () { throw e; }); } - ); - }; - } - - var SITE_VERSION = '2.4.0'; // 与后端 APP_VERSION、index.html CHANGELOG 对齐 - var CHANGELOG = [ - { v: '2.4.0', date: '2026-09-21', items: ['修复:老内核浏览器(Chrome 57-62)操作按钮卡加载的问题', '安全:登录失败次数过多临时锁定;公网错误信息不再暴露内部细节', '优化:用户列表不再携带明文网易云 Cookie(按需单独查看)'] }, - { v: '2.1.0', date: '2026-08-28', items: ['计费改为按次计费:一首歌 1 次,歌单打包按歌曲数量扣减', '免费试用改为 3 次(3 首歌),注册页领取,每设备限一次', '卡密系统升级为次数卡,后台可管理用户次数'] }, - { v: '2.0.0', date: '2026-08-28', items: ['新增站点账号系统:强制登录、邮箱验证码注册', '新增管理后台:用户管理、全局Cookie、SMTP、日志、卡密、公告、数据库备份', '新增个人中心:剩余次数、卡密充值、公告查看'] }, - { v: '1.2.0', date: '2026-03-29', items: ['修复部分歌曲解析失败的问题', '优化界面交互体验'] } - ]; - - function $(s, root) { return (root || document).querySelector(s); } - function $$(s, root) { return Array.prototype.slice.call((root || document).querySelectorAll(s)); } - - /* ---------------- toast ---------------- */ - var toastWrap = null; - function toast(msg, type) { - if (!toastWrap) { - toastWrap = document.createElement('div'); - toastWrap.style.cssText = 'position:fixed;top:18px;left:50%;transform:translateX(-50%);z-index:99999;display:flex;flex-direction:column;gap:8px;align-items:center;pointer-events:none;'; - document.body.appendChild(toastWrap); - } - var t = document.createElement('div'); - t.textContent = msg; - t.style.cssText = 'padding:10px 18px;border-radius:10px;font-size:13px;color:#fff;background:' + - (type === 'error' ? 'linear-gradient(135deg,#ff5f57,#e0483f)' : - type === 'success' ? 'linear-gradient(135deg,#34c759,#2aa64c)' : 'rgba(30,30,30,.92)') + - ';box-shadow:0 6px 24px rgba(0,0,0,.25);opacity:0;transform:translateY(-8px);transition:all .25s;max-width:80vw;'; - toastWrap.appendChild(t); - requestAnimationFrame(function () { t.style.opacity = '1'; t.style.transform = 'translateY(0)'; }); - setTimeout(function () { - t.style.opacity = '0'; t.style.transform = 'translateY(-8px)'; - setTimeout(function () { t.remove(); }, 300); - }, 2600); - } - - /* ---------------- 请求 ---------------- */ - function api(url, params) { - var body = Object.keys(params || {}).map(function (k) { - return encodeURIComponent(k) + '=' + encodeURIComponent(params[k] == null ? '' : params[k]); - }).join('&'); - // 30 秒超时:服务端挂起时不再无限等待(登录含省份查询约 5s 上限,留足余量; - // 无 AbortController 的老内核优雅降级为无超时) - var ctl = (typeof AbortController !== 'undefined') ? new AbortController() : null; - var timer = null; - if (ctl) timer = setTimeout(function () { try { ctl.abort(); } catch (e) {} }, 30000); - return fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, - body: body, - credentials: 'same-origin', - signal: ctl ? ctl.signal : undefined - }).then(function (r) { - // 双回调解包(不用 Promise.finally:兼容 Chromium 57+ 老内核) - return r.json().then(function (d) { if (timer) clearTimeout(timer); return d; }, - function (e) { if (timer) clearTimeout(timer); throw e; }); - }).catch(function () { - // 默认失败兜底——网络断/超时/非 JSON 响应不再产生 unhandledrejection - // 噪音(喂 fe_error 上报通道),统一转为标准失败对象走调用方既有的 toast - if (timer) clearTimeout(timer); - return { status: 0, success: false, message: '网络异常,请检查连接后重试' }; - }); - } - function apiGet(url) { - var ctl = (typeof AbortController !== 'undefined') ? new AbortController() : null; - var timer = null; - if (ctl) timer = setTimeout(function () { try { ctl.abort(); } catch (e) {} }, 15000); - // 注意:apiGet 不做默认失败兜底——/auth/me 的调用方依赖"失败保持原登录态" - //(拒绝会跳过 then 体,不清空已有 user),转成 resolved 失败对象会让 UI 掉成游客 - return fetch(url, { credentials: 'same-origin', signal: ctl ? ctl.signal : undefined }) - .then(function (r) { - return r.json().then(function (d) { if (timer) clearTimeout(timer); return d; }, - function (e) { if (timer) clearTimeout(timer); throw e; }); - }); - } - -/* ---------------- 设备标识 ---------------- */ -function getDeviceId() { - /* 与 index.html 的 getDeviceId 统一为同一键/同一值—— - 注册指纹与下载任务/心跳的设备号不再各走一套; - 内存缓存兜底 localStorage 不可写时同一页面内保持稳定。 */ - if (window.__siteDeviceIdCache) return window.__siteDeviceIdCache; - var KEY = 'netease_device_id'; - var d = ''; - try { d = localStorage.getItem(KEY) || ''; } catch (e) { } - if (!d) { - d = 'dev-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10); - try { localStorage.setItem(KEY, d); } catch (e) { } - } - window.__siteDeviceIdCache = d; - return d; -} - - /* ---------------- 状态 ---------------- */ - var user = null; - var durationText = ''; - var ann = null; - var smtpOk = false; // fail-closed——/auth/me 返回前不显示注册入口(防止未配置 SMTP 的窗口期误开) - var registerCredits = 3; // 注册赠送次数由 /auth/me 下发,文案不再硬编码 - - /* ---------------- 弹窗 ---------------- */ - function openModal(id) { var m = $('#' + id); if (m) m.classList.add('show'); } - function closeModal(id) { var m = $('#' + id); if (m) m.classList.remove('show'); } - - /* ---------------- 导航栏 ---------------- */ - function renderAuthArea() { - // 同步全局登录状态,供主脚本 requireLogin() 使用 - window.SITE_LOGGED_IN = !!user; - // 未开放邮箱注册(SMTP 未配置)时,隐藏所有注册入口,避免用户注册卡死 - var showRegister = !user && smtpOk; - var regBtn = $('#btn-open-register'); - if (regBtn) regBtn.hidden = !showRegister; - ['goto-register', 'goto-register2'].forEach(function (id) { - var a = $('#' + id); if (a) a.hidden = !showRegister; - }); - // 两个弹窗(登录/注册)里的试用提示都要受管——原 querySelector 只命中第一个 - $$('.auth-trial-box').forEach(function (box) { box.hidden = !showRegister; }); - $$('.reg-gift-num').forEach(function (el) { el.textContent = registerCredits; }); - var guest = $('#auth-guest-area'), uarea = $('#auth-user-area'); - if (guest && uarea) { - if (user) { - guest.hidden = true; uarea.hidden = false; - var pill = $('#auth-duration-pill'); - if (pill) pill.textContent = '⏳ ' + (durationText || '0 次'); - var av = $('#user-avatar'); - if (av) { - av.textContent = (user.username || 'U').charAt(0).toUpperCase(); - av.title = user.username + (user.role === 'admin' ? '(管理员)' : ''); - } - } else { - guest.hidden = false; uarea.hidden = true; - } - } - var ucTab = $('#tab-usercenter'), uaTab = $('#tab-useradmin'); - if (ucTab) ucTab.hidden = !user; - if (uaTab) uaTab.hidden = !(user && user.role === 'admin'); - // 全局账号歌单仅管理员可见 - var gTab = $('#tab-global'); - if (gTab) gTab.hidden = !(user && user.role === 'admin'); - } - - /* ---------------- 会话 ---------------- */ - function refreshSession() { - return apiGet('/auth/me').then(function (data) { - var d = data.data || {}; - user = d.user || null; - durationText = d.duration_text || ''; - ann = d.announcement || null; - smtpOk = !!d.smtp_configured; - // 注册赠送次数动态下发(/auth/me),文案不再硬编码 - if (typeof d.register_credits === 'number') registerCredits = d.register_credits; - renderAuthArea(); - renderUserCenter(); - maybeShowAnnouncement(); - // 确认登录后再恢复下载任务(游客调任务接口会 401 误弹登录框) - if (user && typeof window.SITE_TASKS_BOOTSTRAP === 'function') { - try { window.SITE_TASKS_BOOTSTRAP(); } catch (e) {} - } - return user; - }).catch(function () { return null; }); - } - - // 全局暴露给主脚本 apiPost 的 401 统一处理(同一提示 5 秒内不重复弹出) - var _lastAuthToast = 0; - function authToastOnce(msg, type) { - var now = Date.now(); - if (now - _lastAuthToast < 5000) return; - _lastAuthToast = now; - toast(msg, type || 'error'); - } - window.handleAuthExpired = function (msg, expired) { - if (expired) { - authToastOnce(msg || '下载次数已用尽,请充值卡密'); - showExpiredModal(); - return; - } - // 会话失效时清掉内存里的旧用户状态再引导登录 - //(原实现保留旧 user,页面继续显示"已登录"却处处 401) - user = null; - durationText = ''; - renderAuthArea(); - renderUserCenter(); - authToastOnce(msg || '登录已失效,请重新登录'); - // 登录弹窗已打开且正处于找回/两步验证视图时,不复位视图—— - // 未登录期间后台任务轮询/心跳会持续吃 401 反复进入本函数,若每次都把 - // 弹窗拽回登录视图,管理员正在输入的验证码会被打断、票据被清空 - var _lm = document.getElementById('login-modal'); - var _fv = document.getElementById('forgot-view'); - var _a2v = document.getElementById('admin2fa-view'); - var _authViewOpen = _lm && _lm.classList.contains('show') && - ((_fv && !_fv.hidden) || (_a2v && !_a2v.hidden)); - if (!_authViewOpen) { - hideForgotView(); - hideAdmin2faView(); - } - // 会话失效弹登录窗前,先关掉可能叠开的注册弹窗(双遮罩错乱), - // 已 show 则短路——401 风暴(1.5s 轮询)期间不反复重开 - var _reg = document.getElementById('register-modal'); - if (_reg && _reg.classList.contains('show')) closeModal('register-modal'); - if (_lm && _lm.classList.contains('show')) return; - openModal('login-modal'); - }; - // 暴露给主脚本:扣费/任务结束后刷新顶栏剩余次数与登录态 - window.refreshSITECredits = refreshSession; - window.renderSITEUserCenter = renderUserCenter; - // 暴露当前剩余下载次数给主脚本预检(未登录 -1,不限 999999) - window.getSITECredits = function () { - if (!user) return -1; - var c = (typeof user.duration_seconds === 'number') ? user.duration_seconds : 0; - return c < 0 ? 999999 : c; - }; - - /* 次数用尽弹窗(下载触发,带「去充值」入口) */ - function showExpiredModal() { - // 先移除可能残留的旧实例(此前被 Escape 隐藏但未删 DOM 时,旧实现会直接 - // return,导致该弹窗从此再也弹不出来) - var old = $('#site-expired-modal'); - if (old) old.remove(); - var mask = document.createElement('div'); - mask.className = 'modal-mask show'; - mask.id = 'site-expired-modal'; - mask.setAttribute('data-dynamic', '1'); // Escape 关闭时连同 DOM 一并移除 - mask.innerHTML = - ''; - document.body.appendChild(mask); - mask.addEventListener('click', function (e) { if (e.target === mask) mask.remove(); }); - var go = $('#site-expired-go'), close = $('#site-expired-close'); - if (go) go.addEventListener('click', function () { - mask.remove(); - var t = $('#tab-usercenter'); - if (t) t.click(); - }); - if (close) close.addEventListener('click', function () { mask.remove(); }); - } - - function afterAuth(data) { - var d = data.data || {}; - user = d.user || null; - durationText = d.duration_text || ''; - renderAuthArea(); - renderUserCenter(); - // 登录成功后立即恢复本设备的进行中任务 - if (user && typeof window.SITE_TASKS_BOOTSTRAP === 'function') { - try { window.SITE_TASKS_BOOTSTRAP(); } catch (e) {} - } - } - - /* ---------------- 登录 / 注册弹窗 ---------------- */ - // 注册唯一入口——导航按钮与登录弹窗两处链接(全站仅有的 3 条打开 - // 路径)统一经此:未同意协议 → cookieGate('register') 弹「隐私与协议设置」 - // 注册模式(16 秒强制阅读计时)并中止;已同意 → 直接进注册弹窗。 - // 提交侧 doRegister 另有 fail-closed 兜底闸,程序化打开也绕不过。 - function openRegisterFlow() { - closeModal('login-modal'); // 先关登录弹窗,避免协议窗被压在下方 - var gate = window.SITE_COOKIE_GATE; - if (typeof gate !== 'function') { - // fail-closed:协议闸未就绪(主脚本异常)时不放行注册 - toast('页面协议组件未就绪,请刷新页面后重试', 'error'); - return; - } - if (!gate('register')) return; // 未同意:闸内已弹协议窗并提示 - openModal('register-modal'); - } - function bindAuthModal() { - var lo = $('#btn-open-login'), ro = $('#btn-open-register'); - if (lo) lo.addEventListener('click', function () { closeModal('register-modal'); hideForgotView(); hideAdmin2faView(); openModal('login-modal'); }); - if (ro) ro.addEventListener('click', function () { closeModal('login-modal'); openRegisterFlow(); }); - ['login-close', 'register-close', 'announce-close'].forEach(function (id) { - var b = $('#' + id); - if (b) b.addEventListener('click', function () { - if (id.indexOf('login') === 0 && window.resetAuthViews) window.resetAuthViews(); // 关闭侧复位 - closeModal(id.indexOf('login') === 0 ? 'login-modal' : id.indexOf('register') === 0 ? 'register-modal' : 'announce-modal'); - }); - }); - [['login-modal', 'login-close'], ['register-modal', 'register-close']].forEach(function (p) { - var m = $('#' + p[0]); - if (m) m.addEventListener('click', function (e) { - if (e.target === m) { - if (p[0] === 'login-modal' && window.resetAuthViews) window.resetAuthViews(); // 关闭侧复位 - closeModal(p[0]); - } - }); - }); - var g1 = $('#goto-register'), g2 = $('#goto-register2'); - if (g1) g1.addEventListener('click', function () { closeModal('login-modal'); openRegisterFlow(); }); - if (g2) g2.addEventListener('click', function () { closeModal('login-modal'); openRegisterFlow(); }); - var gl1 = $('#goto-login'); - if (gl1) gl1.addEventListener('click', function () { closeModal('register-modal'); hideForgotView(); hideAdmin2faView(); openModal('login-modal'); }); - // 找回密码入口/返回(只交付 DOM 漏了绑定) - var gf = $('#goto-forgot'); - if (gf) gf.addEventListener('click', showForgotView); - var fbl = $('#forgot-back-login'); - if (fbl) fbl.addEventListener('click', hideForgotView); - // 管理员两步验证视图的返回链接 - var a2b = $('#a2fa-back'); - if (a2b) a2b.addEventListener('click', hideAdmin2faView); - - // 注册即送 3 次,无需 Tab 切换 - } - - /* ---------------- 登录 ---------------- */ - function doLogin() { - var btn = $('#login-submit'); - var username = ($('#login-username') || {}).value || ''; - var password = ($('#login-password') || {}).value || ''; - if (!username || !password) { toast('请输入用户名/邮箱和密码', 'error'); return; } - btn.disabled = true; btn.textContent = '登录中...'; - api('/auth/login', { username: username, password: password }) - .then(function (data) { - if (data.status !== 200) throw new Error(data.message || '登录失败'); - // 管理员密码验证通过——转两步验证视图(服务端未建会话) - if (data.data && data.data.admin_2fa) { - showAdmin2faView(data.data.ticket, data.data.email_masked); - toast('密码验证通过,请完成邮箱验证', 'success'); - return; - } - afterAuth(data); - closeModal('login-modal'); - toast('登录成功,欢迎回来,' + (user ? user.username : ''), 'success'); - if (data.data && data.data.redirect === 'admin') { - var uaTab = $('#tab-useradmin'); - if (uaTab) uaTab.click(); - } else { - var ucTab = $('#tab-usercenter'); - if (ucTab) ucTab.click(); - } - }) - .catch(function (e) { toast(e.message || '登录失败', 'error'); }) - .finally(function () { btn.disabled = false; btn.textContent = '登 录'; }); - } - - /* ---------------- 注册 ---------------- */ - var _REG_EMAIL_RE = /^[\w.%+-]+@[\w.-]+\.[A-Za-z]{2,}$/; - var _REG_NAME_RE = /^[A-Za-z0-9_\u4e00-\u9fa5]{3,20}$/; - // 注册弹窗内常驻错误行(长文案 2.6s toast 看不完) - function regShowError(msg) { - toast(msg, 'error'); - var line = $('#reg-error-line'); - if (line) { line.textContent = msg; line.hidden = false; } - } - function regClearError() { - var line = $('#reg-error-line'); - if (line) { line.textContent = ''; line.hidden = true; } - } - function sendCode() { - var btn = $('#reg-send-code'); - var email = ($('#reg-email') || {}).value || ''; - if (!_REG_EMAIL_RE.test(email.trim())) { toast('邮箱格式不正确', 'error'); return; } - regClearError(); - btn.disabled = true; btn.textContent = '发送中...'; - api('/auth/send_code', { email: email, purpose: 'register' }) - .then(function (data) { - if (data.status !== 200) throw new Error(data.message || '发送失败'); - toast('验证码已发送,请查收邮箱', 'success'); - var left = 60; - window.__regCodeTimer = setInterval(function () { - left--; btn.textContent = left + 's'; - if (left <= 0) { clearInterval(window.__regCodeTimer); btn.textContent = '发送验证码'; btn.disabled = false; } - }, 1000); - }) - .catch(function (e) { - toast(e.message || '发送失败', 'error'); - btn.disabled = false; btn.textContent = '发送验证码'; - }); - } - - /* ---------------- 找回密码 ---------------- */ - // 只交付了找回视图 DOM 未实现 JS——此处补齐全链路。 - // 后端 /auth/forgot_password 已就绪:验证码原子消费、防枚举 - // 统一文案、login_seq 踢全会话、5 次/分/IP 限流;发码复用 /auth/send_code。 - function forgotShowError(msg) { - toast(msg, 'error'); - var line = $('#forgot-error-line'); - if (line) { line.textContent = msg; line.hidden = false; } - } - function forgotClearError() { - var line = $('#forgot-error-line'); - if (line) { line.textContent = ''; line.hidden = true; } - } - function forgotStopCodeTimer() { - clearInterval(window.__forgotCodeTimer || 0); - var btn = $('#forgot-send-code'); - if (btn && btn.disabled) { btn.disabled = false; btn.textContent = '发送验证码'; } - } - function showForgotView() { - var form = document.querySelector('#login-modal .auth-form'); - var fv = $('#forgot-view'); - if (!form || !fv) return; - form.classList.add('forgot-mode'); - fv.hidden = false; - forgotClearError(); - var em = $('#forgot-email'); - if (em) { try { em.focus(); } catch (e) {} } - } - function hideForgotView() { - var form = document.querySelector('#login-modal .auth-form'); - var fv = $('#forgot-view'); - if (form) form.classList.remove('forgot-mode'); - if (fv && !fv.hidden) fv.hidden = true; - forgotClearError(); - forgotStopCodeTimer(); - } - function doForgotSendCode() { - var btn = $('#forgot-send-code'); - var email = ($('#forgot-email') || {}).value || ''; - if (!_REG_EMAIL_RE.test(email.trim())) { forgotShowError('邮箱格式不正确'); return; } - forgotClearError(); - btn.disabled = true; btn.textContent = '发送中...'; - api('/auth/send_code', { email: email, purpose: 'reset' }) - .then(function (data) { - if (data.status !== 200) throw new Error(data.message || '发送失败'); - toast('验证码已发送,请查收邮箱', 'success'); - var left = 60; - window.__forgotCodeTimer = setInterval(function () { - left--; btn.textContent = left + 's'; - if (left <= 0) { clearInterval(window.__forgotCodeTimer); btn.textContent = '发送验证码'; btn.disabled = false; } - }, 1000); - }) - .catch(function (e) { - forgotShowError(e.message || '发送失败'); - btn.disabled = false; btn.textContent = '发送验证码'; - }); - } - function doForgotReset() { - var btn = $('#forgot-submit'); - var email = ($('#forgot-email') || {}).value || ''; - var code = ($('#forgot-code') || {}).value || ''; - var password = ($('#forgot-password') || {}).value || ''; - forgotClearError(); - if (!email || !code || !password) { forgotShowError('请填写完整找回信息'); return; } - if (!_REG_EMAIL_RE.test(email.trim())) { forgotShowError('邮箱格式不正确'); return; } - if (password.length < 8 || password.length > 128) { forgotShowError('新密码长度需在 8-128 位之间'); return; } - btn.disabled = true; btn.textContent = '重置中...'; - api('/auth/forgot_password', { email: email, code: code, password: password }) - .then(function (data) { - if (data.status !== 200) throw new Error(data.message || '重置失败'); - var fe = $('#forgot-email'), fc = $('#forgot-code'), fp = $('#forgot-password'); - if (fe) fe.value = ''; if (fc) fc.value = ''; if (fp) fp.value = ''; - forgotStopCodeTimer(); - hideForgotView(); - toast('密码已重置,请用新密码登录', 'success'); - }) - .catch(function (e) { forgotShowError(e.message || '重置失败'); }) - .finally(function () { btn.disabled = false; btn.textContent = '重置密码'; }); - } - - /* ---------------- 管理员两步验证 ---------------- - * Step1 用户名+密码 → 服务端签发票据(不建会话);本视图完成 - * Step2 绑定邮箱比对(触发发码)与 Step3 验证码登录。普通用户不经过此流程。 */ - var _a2faTicket = ''; - function a2faShowError(msg) { - toast(msg, 'error'); - var line = $('#a2fa-error-line'); - if (line) { line.textContent = msg; line.hidden = false; } - } - function a2faClearError() { - var line = $('#a2fa-error-line'); - if (line) { line.textContent = ''; line.hidden = true; } - } - function a2faStopTimer() { - clearInterval(window.__a2faTimer || 0); - var btn = $('#a2fa-send'); - if (btn && btn.disabled) { btn.disabled = false; btn.textContent = '发送验证码'; } - } - function showAdmin2faView(ticket, emailMask) { - var form = document.querySelector('#login-modal .auth-form'); - var v = $('#admin2fa-view'); - if (!form || !v) return; - _a2faTicket = ticket || ''; - var mask = $('#a2fa-email-mask'); - if (mask) mask.textContent = emailMask || '-'; - // 清掉上次会话残留的输入(错码/旧邮箱不再被原样重复提交) - var ae = $('#a2fa-email'), ac = $('#a2fa-code'); - if (ae) ae.value = ''; - if (ac) ac.value = ''; - form.classList.add('admin2fa-mode'); - v.hidden = false; - a2faClearError(); - a2faStopTimer(); - var em = $('#a2fa-email'); - if (em) { try { em.focus(); } catch (e) {} } - } - function hideAdmin2faView() { - var form = document.querySelector('#login-modal .auth-form'); - var v = $('#admin2fa-view'); - if (form) form.classList.remove('admin2fa-mode'); - if (v && !v.hidden) v.hidden = true; - _a2faTicket = ''; - a2faClearError(); - a2faStopTimer(); - } - // 关闭侧统一复位——登录弹窗的 X/遮罩/Escape/doLogout 关闭路径都调用, - // 不再只依赖"打开侧纪律";同时供 index.html 的 Escape 通用分支使用 - window.resetAuthViews = function () { hideForgotView(); hideAdmin2faView(); }; - function doA2faSendCode() { - var btn = $('#a2fa-send'); - var email = ($('#a2fa-email') || {}).value || ''; - if (!email.trim()) { a2faShowError('请输入管理员绑定邮箱'); return; } - a2faClearError(); - btn.disabled = true; btn.textContent = '发送中...'; - api('/auth/admin_login/email', { ticket: _a2faTicket, email: email }) - .then(function (data) { - if (data.status === 401) { - // 先复位视图再提示——错误行随视图隐藏,此处 toast 是唯一可达提示 - hideAdmin2faView(); - a2faShowError(data.message || '请重新登录'); - return; - } - if (data.status !== 200) throw new Error(data.message || '发送失败'); - toast('验证码已发送至管理员邮箱,请查收', 'success'); - var ci = $('#a2fa-code'); - if (ci) { try { ci.focus(); } catch (e) {} } - var left = 60; - window.__a2faTimer = setInterval(function () { - left--; btn.textContent = left + 's'; - if (left <= 0) { clearInterval(window.__a2faTimer); btn.textContent = '发送验证码'; btn.disabled = false; } - }, 1000); - }) - .catch(function (e) { - a2faShowError(e.message || '发送失败'); - btn.disabled = false; btn.textContent = '发送验证码'; - }); - } - function doA2faVerify() { - var btn = $('#a2fa-submit'); - var email = ($('#a2fa-email') || {}).value || ''; - var code = ($('#a2fa-code') || {}).value || ''; - a2faClearError(); - if (!email.trim()) { a2faShowError('请输入管理员绑定邮箱'); return; } - if (!code.trim()) { a2faShowError('请先获取并输入邮箱验证码'); return; } - btn.disabled = true; btn.textContent = '验证中...'; - api('/auth/admin_login/verify', { ticket: _a2faTicket, code: code }) - .then(function (data) { - if (data.status === 401) { - // 先复位视图再提示(同上,错误行随视图隐藏) - hideAdmin2faView(); - a2faShowError(data.message || '请重新登录'); - return; - } - if (data.status !== 200) throw new Error(data.message || '验证失败'); - a2faClearError(); - afterAuth(data); - closeModal('login-modal'); - toast('登录成功,欢迎回来,' + (user ? user.username : ''), 'success'); - var uaTab = $('#tab-useradmin'); - if (uaTab) uaTab.click(); - }) - .catch(function (e) { a2faShowError(e.message || '验证失败'); }) - .finally(function () { btn.disabled = false; btn.textContent = '验证并登录'; }); - } - - function doRegister() { - var btn = $('#reg-submit'); - var username = ($('#reg-username') || {}).value || ''; - var password = ($('#reg-password') || {}).value || ''; - var email = ($('#reg-email') || {}).value || ''; - var code = ($('#reg-code') || {}).value || ''; - regClearError(); - // 提交侧兜底闸(入口已统一走 openRegisterFlow,此处防任何程序化 - // 打开路径绕过)。fail-closed:协议闸缺失视为未同意。注册语境只查协议同意 - // (功能性 Cookie 不拦注册,拦解析/下载时另有提示);未同意时闸内已自动 - // 弹「隐私与协议设置」注册模式并 toast,此处不再重复提示 - if (typeof window.SITE_COOKIE_GATE !== 'function') { - toast('页面协议组件未就绪,请刷新页面后重试', 'error'); - return; - } - if (!window.SITE_COOKIE_GATE('register')) return; - // 前端即时校验(口径与后端一致,省一次网络往返) - if (!username || !password || !email || !code) { regShowError('请填写完整注册信息'); return; } - if (!_REG_NAME_RE.test(username)) { regShowError('用户名需 3-20 位,仅支持中文、字母、数字、下划线,且不能纯下划线'); return; } - if (!_REG_EMAIL_RE.test(email.trim())) { regShowError('邮箱格式不正确'); return; } - if (password.length < 8 || password.length > 128) { regShowError('密码长度需在 8-128 位之间'); return; } - btn.disabled = true; btn.textContent = '注册中...'; - api('/auth/register', { - username: username, password: password, email: email, code: code, - device_id: getDeviceId(), agree: 1 - }) - .then(function (data) { - if (data.status !== 200) throw new Error(data.message || '注册失败'); - regClearError(); - afterAuth(data); - closeModal('register-modal'); - toast('注册成功,已自动登录,赠送的下载次数可在个人中心查看', 'success'); - var ucTab = $('#tab-usercenter'); - if (ucTab) ucTab.click(); - }) - .catch(function (e) { regShowError(e.message || '注册失败'); }) - .finally(function () { btn.disabled = false; btn.textContent = '注 册'; }); - } - - /* ---------------- 退出登录 ---------------- */ - function doLogout() { - api('/auth/logout', {}).then(function () { - user = null; durationText = ''; - renderAuthArea(); - renderUserCenter(); // 同步个人中心 gate/content 显隐,与其他登出路径一致 - if (window.resetAuthViews) window.resetAuthViews(); // 登出复位登录弹窗视图 - closeModal('register-modal'); closeModal('login-modal'); - // 退出连带清除个人网易云登录(index.html 注册的钩子: - // 清 localStorage 的个人 Cookie/解析账号选择并复位账号栏) - if (typeof window.SITE_ON_LOGOUT === 'function') { - try { window.SITE_ON_LOGOUT(); } catch (e) {} - } - var ex = $('#site-expired-modal'); if (ex) ex.remove(); - // 切回搜索页,避免停留在个人中心 / 管理后台 / 管理员歌单 - var search = document.querySelector('.tab[data-tab="search"]') || $('#tab-search'); - if (search) search.click(); - toast('已退出登录', 'success'); - }); - } - - /* ---------------- 个人中心 ---------------- */ - function fmtTime(ts) { - if (!ts) return '-'; - var d = new Date(ts * 1000); - return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + - String(d.getDate()).padStart(2, '0'); - } - - function renderUserCenter() { - var el; - // 登录态切换:隐藏 🔒 占位、显示个人中心内容 - var gate = $('#uc-gate'), content = $('#uc-content'); - if (gate && content) { - gate.hidden = !!user; - content.hidden = !user; - } - if (!user) return; - if ((el = $('#uc-avatar'))) el.textContent = (user.username || 'U').charAt(0).toUpperCase(); - if ((el = $('#uc-username'))) el.textContent = user ? user.username : '-'; - if ((el = $('#uc-role'))) el.textContent = user ? (user.role === 'admin' ? '管理员 · 不限次数' - : (typeof user.duration_seconds === 'number' && user.duration_seconds < 0 ? 'VIP · 不限次数' : '普通用户')) : '-'; - if ((el = $('#uc-duration'))) el.textContent = user ? (durationText || '0 次') : '-'; - if ((el = $('#uc-email'))) el.textContent = (user && user.email) || '未绑定'; - if ((el = $('#uc-created'))) el.textContent = user ? fmtTime(user.created_at) : '-'; - var cl = $('#uc-changelog'); - if (cl) { - cl.innerHTML = CHANGELOG.map(function (v) { - return '
v' + v.v + ' ' + - v.date + '
' + - v.items.map(function (i) { return '· ' + i; }).join('
') + '
'; - }).join('') + '
当前版本 v' + SITE_VERSION + - '
'; - } - } - - function doRedeem() { - var input = $('#uc-card-input'); - var code = (input && input.value || '').trim(); - if (!code) { toast('请输入卡密', 'error'); return; } - var btn = $('#uc-card-redeem'); - btn.disabled = true; - api('/auth/redeem', { code: code }) - .then(function (data) { - if (data.status !== 200) throw new Error(data.message || '兑换失败'); - durationText = (data.data && data.data.duration_text) || durationText; - // 同步更新内存余额(批量下载预检读的是 user.duration_seconds, - // 原实现只改显示文字,预检仍用充值前的旧值) - if (user && data.data && typeof data.data.duration_seconds === 'number') { - user.duration_seconds = data.data.duration_seconds; - return null; - } - return refreshSession(); - }) - .then(function () { - renderAuthArea(); renderUserCenter(); - if (input) input.value = ''; - toast('充值成功', 'success'); - }) - .catch(function (e) { toast(e.message || '兑换失败', 'error'); }) - .finally(function () { btn.disabled = false; }); - } - - - /* ---------------- 公告 ---------------- */ - function maybeShowAnnouncement(force) { - // 手动"查看公告"(force=true)无条件展示; - // 自动弹出仍按 popup_mode 与 once 已读版本判断 - if (!force && (!ann || !ann.content || ann.popup_mode === 'off')) return; - if (!force && ann && ann.popup_mode === 'once') { - // once 模式的已读判断:登录用户比对服务端 ann_seen; - // 游客比对本地 localStorage(原实现只查 user,游客每次刷新都重复弹窗) - if (user && user.ann_seen && String(user.ann_seen) === String(ann.updated_at)) return; - if (!user) { - var guestSeen = ''; - try { guestSeen = localStorage.getItem('netease_ann_once_seen') || ''; } catch (e) {} - if (guestSeen === String(ann.updated_at)) return; - } - } - if (!ann || !ann.content) { toast('当前没有公告内容', 'info'); return; } - var t = $('#announce-title'), c = $('#announce-content'); - if (t) t.textContent = ann.title || '系统公告'; - if (c) c.innerHTML = String(ann.content).split('\n').map(function (l) { return escapeHtml(l); }).join('
'); - // 首访 Cookie 协议窗显示中不叠加公告弹窗(双遮罩错乱); - // 跳过时不记已读,下次进入页面再弹 - var _cm = document.getElementById('cookie-modal'); - if (_cm && _cm.classList.contains('show')) return; - openModal('announce-modal'); - if (user) api('/auth/ann_seen', {}); - else { try { localStorage.setItem('netease_ann_once_seen', String(ann.updated_at)); } catch (e) {} } - } - - function escapeHtml(s) { - return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { - return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; - }); - } - - /* ---------------- 管理后台 ---------------- */ - var uaLoaded = false; - var uaLogsType = 'success'; - - function uaShowGate() { - var gate = $('#ua-gate'), content = $('#ua-content'); - if (!gate || !content) return; - var isAdmin = !!(user && user.role === 'admin'); - gate.hidden = isAdmin; - content.hidden = !isAdmin; - if (isAdmin && !uaLoaded) { uaLoaded = true; uaLoadUsers(); uaLoadCookieStatus(); uaLoadSmtp(); uaLoadLogs(); uaLoadCards(); uaLoadAnn(); uaLoadDb(); } - } - - function uaBindSide() { - $$('#admin-side .admin-side-item[data-view]').forEach(function (item) { - // 侧栏项补键盘语义(同型——tabindex/role + Enter/空格激活) - item.setAttribute('tabindex', '0'); - item.setAttribute('role', 'button'); - item.addEventListener('click', function () { - $$('#admin-side .admin-side-item').forEach(function (i) { i.classList.remove('active'); }); - item.classList.add('active'); - $$('.admin-view').forEach(function (v) { v.classList.toggle('active', v.getAttribute('data-aview') === item.getAttribute('data-view')); }); - }); - item.addEventListener('keydown', function (e) { - if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); item.click(); } - }); - }); - var back = $('#ua-back'); - if (back) back.addEventListener('click', function () { - var t = $('#tab-useradmin'); if (t) t.classList.remove('active'); - var search = document.querySelector('.tab[data-tab="search"]'); - if (search) search.click(); - }); - var lo = $('#ua-logout'); - if (lo) lo.addEventListener('click', doLogout); - // 返回/退出两个无 data-view 的侧栏项同样补键盘语义 - [back, lo].forEach(function (el) { - if (!el) return; - el.setAttribute('tabindex', '0'); - el.setAttribute('role', 'button'); - el.addEventListener('keydown', function (e) { - if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); el.click(); } - }); - }); - /* ---- 播放 API 密钥 ---- */ - var pkShow = $('#ua-playkey-show'); - var pkInput = $('#ua-playkey-value'); - function pkCopyToClipboard(text) { - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(text).then(function () { toast('密钥已复制', 'success'); }, - function () { toast('复制失败,请手动选择文本复制', 'error'); }); - } else { - toast('复制失败,请点击「显示密钥」后手动复制', 'error'); - } - } - if (pkShow) pkShow.addEventListener('click', function () { - if (pkShow.dataset.state === 'shown') { - pkInput.value = ''; - pkInput.placeholder = '点击「显示密钥」查看'; - pkShow.textContent = '显示密钥'; - pkShow.dataset.state = ''; - return; - } - api('/admin/site/play_key/get', {}).then(function (d) { - if (d.status !== 200) { toast(d.message || '获取失败', 'error'); return; } - pkInput.value = d.data.key; - pkShow.textContent = '隐藏'; - pkShow.dataset.state = 'shown'; - }); - }); - var pkCopy = $('#ua-playkey-copy'); - if (pkCopy) pkCopy.addEventListener('click', function () { - api('/admin/site/play_key/get', {}).then(function (d) { - if (d.status !== 200) { toast(d.message || '获取失败', 'error'); return; } - pkCopyToClipboard(d.data.key); - }); - }); - var pkRotate = $('#ua-playkey-rotate'); - if (pkRotate) pkRotate.addEventListener('click', function () { - if (!confirm('刷新后旧密钥立即全部失效(外部旧地址将打不开),确定继续?')) return; - api('/admin/site/play_key/rotate', {}).then(function (d) { - if (d.status !== 200) { toast(d.message || '刷新失败', 'error'); return; } - pkInput.value = d.data.key; - pkShow.textContent = '隐藏'; - pkShow.dataset.state = 'shown'; - toast('密钥已刷新,旧密钥已全部失效', 'success'); - }); - }); - } - - /* ---- 用户管理 ---- */ - function uaInjectUserToolbar() { - var refresh = $('#ua-users-refresh'); - if (!refresh || $('#ua-user-create')) return; - var btn = document.createElement('button'); - btn.className = 'btn btn-sm btn-primary'; - btn.id = 'ua-user-create'; - btn.type = 'button'; - btn.textContent = '➕ 添加用户'; - btn.addEventListener('click', uaCreateUserModal); - refresh.parentNode.insertBefore(btn, refresh); - } - - function uaCreateUserModal() { - var old = $('#site-create-user-modal'); - if (old) old.remove(); - var mask = document.createElement('div'); - mask.className = 'modal-mask show'; - mask.id = 'site-create-user-modal'; - mask.setAttribute('data-dynamic', '1'); // Escape 关闭时连同 DOM 一并移除 - mask.innerHTML = - ''; - document.body.appendChild(mask); - mask.addEventListener('click', function (e) { if (e.target === mask) mask.remove(); }); - var cancel = $('#cnu-cancel'); - if (cancel) cancel.addEventListener('click', function () { mask.remove(); }); - var submit = $('#cnu-submit'); - if (submit) submit.addEventListener('click', function () { - var body = { - username: ($('#cnu-username') || {}).value || '', - password: ($('#cnu-password') || {}).value || '', - email: ($('#cnu-email') || {}).value || '', - count: ($('#cnu-count') || {}).value || 0 - }; - if (!body.username || body.password.length < 8) { toast('请填写用户名和至少 8 位密码', 'error'); return; } - submit.disabled = true; - api('/admin/site/user/create', body).then(function (d) { - toast(d.message || (d.status === 200 ? '创建成功' : '创建失败'), d.status === 200 ? 'success' : 'error'); - if (d.status === 200) { mask.remove(); uaLoadUsers(); } - }).finally(function () { submit.disabled = false; }); - }); - } - - function uaLoadUsers() { - uaInjectUserToolbar(); - api('/admin/site/users', {}).then(function (data) { - var body = $('#ua-users-body'); - if (!body) return; - if (data.status !== 200) { body.innerHTML = '' + escapeHtml(data.message || '加载失败') + ''; return; } - var users = (data.data && data.data.users) || []; - var cnt = $('#ua-users-count'); - if (cnt) cnt.textContent = users.length; - if (!users.length) { body.innerHTML = '暂无用户'; return; } - body.innerHTML = users.map(function (u) { - var isAdmin = u.role === 'admin'; - var disabled = (u.status === 0); - var banned = (u.status === 2); - // 数值字段同过转义(纵深防御——后端口径若变为字符串 - // 不再形成管理后台存储型 XSS 面,与同表 username/email 口径对齐) - var uid = escapeHtml(String(u.id)); - var dur = escapeHtml(String(u.duration_seconds)); - var cred = u.duration_seconds < 0 ? '不限' : (dur + ' 次'); - var dev = [u.register_ip, u.trial_device].filter(Boolean).join(' · ') || '-'; // IP 优先显示 - if (u.last_province) dev += ' · ' + u.last_province; - return '' + - '' + uid + '' + - '' + escapeHtml(u.username) + - (banned ? ' 已封禁' : '') + - (disabled ? ' 已禁用' : '') + '' + - '' + escapeHtml(u.email || '-') + '' + - '' + (isAdmin ? '管理员' : '用户') + '' + - '' + cred + '' + - '' + escapeHtml(dev) + '' + - '' + - '' + - ' ' + - (isAdmin ? '' : - ' ' + - ' ' + - ' ' + - '') + - ''; - }).join(''); - $$('[data-uedit]', body).forEach(function (b) { - b.addEventListener('click', function () { - var v = prompt('设置该用户的剩余次数(-1 表示不限):', b.getAttribute('data-cur')); - if (v === null) return; - api('/admin/site/user/duration', { id: b.getAttribute('data-uedit'), count: v }) - .then(function (d) { toast(d.message || (d.status === 200 ? '已修改' : '修改失败'), d.status === 200 ? 'success' : 'error'); if (d.status === 200) uaLoadUsers(); }); - }); - }); - // 管理员自助改密(需验证旧密码;改完其他设备全部下线) - $$('[data-uself]', body).forEach(function (b) { - b.addEventListener('click', function () { - var oldpwd = prompt('请输入当前密码:', ''); - if (oldpwd === null) return; - var newpwd = prompt('请输入新密码(至少 8 位):', ''); - if (newpwd === null) return; - api('/admin/site/user/reset_password', { - id: b.getAttribute('data-uself'), password: newpwd, old_password: oldpwd - }).then(function (d) { - toast(d.message || (d.status === 200 ? '密码已修改' : '修改失败'), d.status === 200 ? 'success' : 'error'); - }); - }); - }); - $$('[data-upwd]', body).forEach(function (b) { - b.addEventListener('click', function () { - var v = prompt('为用户「' + b.getAttribute('data-name') + '」设置新密码(至少 8 位):', ''); - if (v === null) return; - api('/admin/site/user/reset_password', { id: b.getAttribute('data-upwd'), password: v }) - .then(function (d) { toast(d.message || (d.status === 200 ? '已重置' : '重置失败'), d.status === 200 ? 'success' : 'error'); }); - }); - }); - $$('[data-utoggle]', body).forEach(function (b) { - b.addEventListener('click', function () { - api('/admin/site/user/toggle', { id: b.getAttribute('data-utoggle') }) - .then(function (d) { toast(d.message || '操作成功', d.status === 200 ? 'success' : 'error'); if (d.status === 200) uaLoadUsers(); }); - }); - }); - $$('[data-uban]', body).forEach(function (b) { - b.addEventListener('click', function () { - var name = b.getAttribute('data-name'); - if (b.textContent === '封禁') { - var reason = prompt('封禁用户「' + name + '」\n请输入封禁原因(将展示给用户):', '违规使用,封禁处理'); - if (reason === null) return; - api('/admin/site/user/ban', { id: b.getAttribute('data-uban'), action: 'ban', reason: reason }) - .then(function (d) { toast(d.message || (d.status === 200 ? '已封禁' : '操作失败'), d.status === 200 ? 'success' : 'error'); if (d.status === 200) uaLoadUsers(); }); - } else { - if (!window.confirm('解封用户「' + name + '」?解封后可正常登录使用。')) return; - api('/admin/site/user/ban', { id: b.getAttribute('data-uban'), action: 'unban' }) - .then(function (d) { toast(d.message || (d.status === 200 ? '已解封' : '操作失败'), d.status === 200 ? 'success' : 'error'); if (d.status === 200) uaLoadUsers(); }); - } - }); - }); - $$('[data-udel]', body).forEach(function (b) { - b.addEventListener('click', function () { - if (!window.confirm('确定删除用户「' + b.getAttribute('data-name') + '」?')) return; - api('/admin/site/user/delete', { id: b.getAttribute('data-udel') }) - .then(function (d) { toast(d.message || '已删除', d.status === 200 ? 'success' : 'error'); uaLoadUsers(); }); - }); - }); - $$('[data-ucookie]', body).forEach(function (b) { - b.addEventListener('click', function () { - api('/admin/site/user/cookie', { username: b.getAttribute('data-ucookie') }) - .then(function (d) { - if (d.status !== 200) { toast(d.message || '获取失败', 'error'); return; } - var ck = (d.data && d.data.cookie) || ''; - if (!ck) { toast('该用户未登录过网易云账号', 'info'); return; } - prompt('用户网易云 Cookie(Ctrl+C 复制):', ck); - }); - }); - }); - }); - } - - /* ---- 全局 Cookie ---- */ - var uaQrTimer = null, uaUnikey = '', uaQrGen = 0; // 轮询代次令牌(删除 Cookie 后作废旧轮询) - function uaLoadCookieStatus() { - api('/admin/cookie/status', {}).then(function (data) { - var el = $('#ua-cookie-status'); - if (!el) return; - if (data.status !== 200) { el.textContent = data.message || '获取失败'; return; } - var info = data.data || {}; - el.textContent = (info.is_valid ? '✅ 有效' : '❌ 未登录') + - (info.cookie_count != null ? ' · Cookie ' + info.cookie_count + ' 个' : ''); - }); - } - function uaQrCreate() { - api('/admin/qr/create', {}).then(function (data) { - if (data.status !== 200) { toast(data.message || '生成二维码失败', 'error'); return; } - var d = data.data || {}; - uaUnikey = d.unikey || ''; - var box = $('#ua-qr-box'), tip = $('#ua-qr-tip'); - if (d.qr_svg) { - var uri = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(d.qr_svg))); - box.innerHTML = '二维码'; - } - if (tip) tip.textContent = '请使用网易云音乐手机 APP 扫码'; - if (uaQrTimer) clearInterval(uaQrTimer); - uaQrGen++; // 新轮询代次 - uaQrTimer = setInterval(uaQrPoll, 3000); - }); - } - function uaQrPoll() { - if (!uaUnikey) { if (uaQrTimer) clearInterval(uaQrTimer); return; } - var myGen = uaQrGen; // 本次轮询所属代次 - api('/admin/qr/check', { unikey: uaUnikey }).then(function (data) { - if (myGen !== uaQrGen) return; // 期间已删除 Cookie/重开扫码,丢弃迟到响应 - var tip = $('#ua-qr-tip'); - if (data.status !== 200) { - if (uaQrTimer) clearInterval(uaQrTimer); - if (tip) tip.textContent = data.message || '检查失败'; - return; - } - var st = data.data && data.data.status; - if (st === 'waiting') { if (tip) tip.textContent = '等待扫码...'; } - else if (st === 'scanned') { if (tip) tip.textContent = '✅ 已扫码,请在手机上确认'; } - else if (st === 'success') { - if (uaQrTimer) clearInterval(uaQrTimer); - if (tip) tip.textContent = '🎉 登录成功,Cookie 已保存'; - toast('全局账号登录成功', 'success'); - uaLoadCookieStatus(); - } else { - if (uaQrTimer) clearInterval(uaQrTimer); - if (tip) tip.textContent = '二维码已过期,请重新生成'; - } - }); - } - function uaCookieDelete() { - if (!window.confirm('确定删除全局 Cookie?删除后需重新扫码登录。')) return; - api('/admin/cookie/delete', {}).then(function (data) { - // 作废在途扫码轮询——迟到的 803 成功响应不得把刚删除的 Cookie 写回 - if (uaQrTimer) clearInterval(uaQrTimer); - uaUnikey = ''; - uaQrGen++; - toast(data.message || '已删除', data.status === 200 ? 'success' : 'error'); - var box = $('#ua-qr-box'), tip = $('#ua-qr-tip'); - if (box) box.innerHTML = ''; - if (tip) tip.textContent = '点击下方按钮生成二维码'; - uaLoadCookieStatus(); - }); - } - - /* ---- SMTP ---- */ - function uaLoadSmtp() { - api('/admin/site/smtp/get', {}).then(function (data) { - if (data.status !== 200) return; - var d = data.data || {}; - if ($('#ua-smtp-host')) $('#ua-smtp-host').value = d.host || ''; - if ($('#ua-smtp-port')) $('#ua-smtp-port').value = d.port || 587; - if ($('#ua-smtp-sender')) $('#ua-smtp-sender').value = d.sender || ''; - }); - } - function uaSaveSmtp() { - api('/admin/site/smtp/save', { - host: ($('#ua-smtp-host') || {}).value || '', - port: ($('#ua-smtp-port') || {}).value || 587, - sender: ($('#ua-smtp-sender') || {}).value || '', - password: ($('#ua-smtp-pass') || {}).value || '' - }).then(function (d) { - toast(d.message || '已保存', d.status === 200 ? 'success' : 'error'); - var p = $('#ua-smtp-pass'); if (p) p.value = ''; - }); - } - function uaTestSmtp() { - var to = ($('#ua-smtp-test-to') || {}).value || ''; - if (!to) { toast('请填写测试收件邮箱', 'error'); return; } - var btn = $('#ua-smtp-test'); - btn.disabled = true; - api('/admin/site/smtp/test', { to: to }).then(function (d) { - toast(d.message || '已发送', d.status === 200 ? 'success' : 'error'); - }).finally(function () { btn.disabled = false; }); - } - - /* ---- 日志 ---- */ - var uaLogPage = 1; - function uaLoadLogs() { - api('/admin/site/logs', { type: uaLogsType, page: uaLogPage }).then(function (data) { - var box = $('#ua-log-list'); - if (!box) return; - if (data.status !== 200) { box.textContent = data.message || '加载失败'; return; } - var logs = (data.data && data.data.logs) || []; - var total = (data.data && data.data.total) || 0; - var pages = (data.data && data.data.pages) || 1; - uaLogPage = (data.data && data.data.page) || 1; - window._uaLogPages = pages; // next 按钮按真实页数封顶 - var el; - if ((el = $('#ua-log-page'))) el.textContent = uaLogPage; - if ((el = $('#ua-log-pages'))) el.textContent = pages; - if ((el = $('#ua-log-total'))) el.textContent = total; - if (!logs.length) { box.textContent = '暂无日志'; return; } - box.innerHTML = logs.map(function (l) { - return '
[' + escapeHtml(l.time) + '] ' + escapeHtml(l.username || '-') + - ' · ' + escapeHtml(l.action) + ' · IP ' + escapeHtml(l.ip) + - (l.detail ? ' · ' + escapeHtml(l.detail) : '') + '
'; - }).join(''); - }); - } - - function uaClearLogs() { - var name = uaLogsType === 'fail' ? '失败' : '成功'; - if (!window.confirm('确定清除全部「' + name + '」日志?该操作不可恢复。')) return; - api('/admin/site/logs/clear', { type: uaLogsType }).then(function (d) { - toast(d.message || (d.status === 200 ? '已清除' : '清除失败'), d.status === 200 ? 'success' : 'error'); - uaLogPage = 1; - uaLoadLogs(); - }); - } - - /* ---- 卡密 ---- */ - function uaGenCards() { - var creditsVal = ($('#ua-card-minutes') || {}).value || '10'; - if (creditsVal === 'custom') { - creditsVal = ($('#ua-card-custom') || {}).value || '0'; - if (!(parseInt(creditsVal, 10) > 0)) { toast('请填写自定义次数(正整数)', 'error'); return; } - } - api('/admin/site/cards/generate', { - count: ($('#ua-card-count') || {}).value || 1, - credits: creditsVal, - prefix: ($('#ua-card-prefix') || {}).value || 'VIP', - note: ($('#ua-card-note') || {}).value || '', - valid_days: ($('#ua-card-days') || {}).value || 0 - }).then(function (data) { - if (data.status !== 200) { toast(data.message || '生成失败', 'error'); return; } - var box = $('#ua-card-codes'); - if (box) { - box.hidden = false; - box.textContent = (data.data.codes || []).join('\n'); - } - toast('已生成 ' + (data.data.count || 0) + ' 张卡密(' + (data.data.credits || 0) + ' 次/张),已复制到文本框,请及时保存', 'success'); - uaLoadCards(0); - }); - } - // 卡密管理操作列(禁用/启用/删除)+ 分页(后端已支持, - // 不分页则超出最新 100 张的旧卡密在界面上永远管理不到) - var _uaCardPage = 0; - var _uaCardTotal = 0; - var UA_CARD_PAGE_SIZE = 100; - function uaCardAction(act, code) { - if (!code) return; - if (act === 'delete' && !window.confirm('确定删除卡密 ' + code.slice(0, 4) + '**** 吗?删除后不可恢复。')) return; - var url = act === 'delete' ? '/admin/site/cards/delete' : '/admin/site/cards/disable'; - var body = { code: code, disabled: act === 'enable' ? '0' : '1' }; - api(url, body).then(function (data) { - toast(data.message || (data.status === 200 ? '操作成功' : '操作失败'), - data.status === 200 ? 'success' : 'error'); - if (data.status === 200) uaLoadCards(); - }); - } - function _uaRenderCardPager() { - var pager = $('#ua-cards-pager'); - if (!pager) return; - if (_uaCardTotal <= 0) { pager.hidden = true; return; } - var pages = Math.max(1, Math.ceil(_uaCardTotal / UA_CARD_PAGE_SIZE)); - pager.hidden = false; - pager.textContent = ''; - var prev = document.createElement('button'); - prev.className = 'btn btn-sm'; prev.type = 'button'; prev.textContent = '上一页'; - prev.disabled = _uaCardPage <= 0; - prev.addEventListener('click', function () { uaLoadCards(_uaCardPage - 1); }); - var next = document.createElement('button'); - next.className = 'btn btn-sm'; next.type = 'button'; next.textContent = '下一页'; - next.disabled = _uaCardPage >= pages - 1; - next.addEventListener('click', function () { uaLoadCards(_uaCardPage + 1); }); - var info = document.createElement('span'); - info.className = 'hint'; - info.textContent = '第 ' + (_uaCardPage + 1) + ' / ' + pages + ' 页 · 共 ' + _uaCardTotal + ' 张'; - pager.appendChild(prev); - pager.appendChild(info); - pager.appendChild(next); - } - function uaLoadCards(page) { - if (typeof page === 'number') _uaCardPage = Math.max(0, page); - api('/admin/site/cards/list', { limit: UA_CARD_PAGE_SIZE, offset: _uaCardPage * UA_CARD_PAGE_SIZE }) - .then(function (data) { - var body = $('#ua-cards-body'); - if (!body) return; - if (data.status !== 200) { body.innerHTML = '加载失败'; return; } - var cards = (data.data && data.data.cards) || []; - _uaCardTotal = (data.data && data.data.total) || 0; - if (!cards.length) { - body.innerHTML = '暂无卡密'; - _uaRenderCardPager(); - return; - } - body.innerHTML = cards.map(function (c) { - var st, act; - if (c.used_by) { - st = '已使用'; - act = '—'; // 兑换凭证保留,不给删除/禁用 - } else if (c.disabled) { - st = '已禁用'; - act = ' ' + - ''; - } else { - // 过期判定用服务端下发的 expired(客户端时钟偏差会误标) - st = c.expired ? '已过期' : '未使用'; - act = ' ' + - ''; - } - return '' + escapeHtml(c.code) + '' + escapeHtml(String(c.minutes)) + ' 次' + st + '' + escapeHtml(c.used_by || '-') + '' + - escapeHtml(c.note || '-') + '' + - (c.expire_at ? new Date(c.expire_at * 1000).toLocaleDateString() : '永久') + '' + - escapeHtml(c.created_at ? new Date(c.created_at * 1000).toLocaleDateString() : '-') + '' + act + ''; - }).join(''); - _uaRenderCardPager(); - Array.prototype.forEach.call(body.querySelectorAll('button[data-card-act]'), function (b) { - b.addEventListener('click', function () { - uaCardAction(b.getAttribute('data-card-act'), b.getAttribute('data-card-code')); - }); - }); - }); - } - - /* ---- 公告 ---- */ - function uaLoadAnn() { - api('/admin/site/ann/get', {}).then(function (data) { - if (data.status !== 200) return; - var d = data.data || {}; - if ($('#ua-ann-title')) $('#ua-ann-title').value = d.title || ''; - if ($('#ua-ann-content')) $('#ua-ann-content').value = d.content || ''; - if ($('#ua-ann-mode')) $('#ua-ann-mode').value = d.popup_mode || 'off'; - }); - } - function uaSaveAnn() { - api('/admin/site/ann/save', { - title: ($('#ua-ann-title') || {}).value || '', - content: ($('#ua-ann-content') || {}).value || '', - popup_mode: ($('#ua-ann-mode') || {}).value || 'off' - }).then(function (d) { toast(d.message || '已保存', d.status === 200 ? 'success' : 'error'); }); - } - - /* ---- 数据库 ---- */ - function fmtSize(n) { - if (!n && n !== 0) return '-'; - if (n > 1048576) return (n / 1048576).toFixed(2) + ' MB'; - if (n > 1024) return (n / 1024).toFixed(1) + ' KB'; - return n + ' B'; - } - function uaLoadDb() { - api('/admin/site/backup/list', {}).then(function (data) { - var body = $('#ua-db-body'); - if (!body) return; - if (data.status !== 200) { body.innerHTML = '加载失败'; return; } - var d = data.data || {}; - var size = $('#ua-db-size'); if (size) size.textContent = fmtSize(d.db_size || 0); - var hours = $('#ua-db-hours'); if (hours && d.interval_hours != null) hours.value = d.interval_hours; - var baks = d.backups || []; - if (!baks.length) { body.innerHTML = '暂无备份'; return; } - body.innerHTML = baks.map(function (b) { - return '' + escapeHtml(b.name) + '' + fmtSize(b.size) + '' + - escapeHtml(b.time) + '' + - '下载 ' + - ' ' + - ''; - }).join(''); - $$('[data-restore]', body).forEach(function (b) { - b.addEventListener('click', function () { - if (!window.confirm('确定用该备份覆盖当前数据库?')) return; - api('/admin/site/backup/restore', { name: b.getAttribute('data-restore') }) - .then(function (d2) { - toast(d2.message || '已恢复', d2.status === 200 ? 'success' : 'error'); - if (d2.status === 200) setTimeout(function () { location.reload(); }, 1200); - }); - }); - }); - $$('[data-bdel]', body).forEach(function (b) { - b.addEventListener('click', function () { - if (!window.confirm('确定删除该备份?')) return; - api('/admin/site/backup/delete', { name: b.getAttribute('data-bdel') }) - .then(function (d2) { toast(d2.message || '已删除', d2.status === 200 ? 'success' : 'error'); uaLoadDb(); }); - }); - }); - }); - } - function uaBackup() { - api('/admin/site/backup/create', {}).then(function (d) { - toast(d.message || '备份完成', d.status === 200 ? 'success' : 'error'); - uaLoadDb(); - }); - } - function uaBackupAuto() { - api('/admin/site/backup/auto', { hours: ($('#ua-db-hours') || {}).value || 0 }) - .then(function (d) { toast(d.message || '已保存', d.status === 200 ? 'success' : 'error'); }); - } - function uaBackupUpload(file) { - var fd = new FormData(); - fd.append('file', file); - fetch('/admin/site/backup/upload', { method: 'POST', body: fd, credentials: 'same-origin' }) - .then(function (r) { - if (!r.ok) throw new Error('上传失败(HTTP ' + r.status + ')'); - return r.json(); - }) - .then(function (d) { - toast(d.message || '上传完成', d.status === 200 ? 'success' : 'error'); - uaLoadDb(); - }) - .catch(function (err) { - // 全站唯一绕过统一请求封装的调用此前无失败兜底—— - // 网络失败/非 JSON 响应时管理员毫无感知还制造 unhandledrejection 噪音 - toast((err && err.message) || '备份上传失败,请检查网络后重试', 'error'); - }); - } - - /* ---------------- 绑定 ---------------- */ - function bind() { - bindAuthModal(); - var s1 = $('#login-submit'); if (s1) s1.addEventListener('click', doLogin); - var s2 = $('#login-password'); if (s2) s2.addEventListener('keydown', function (e) { if (e.key === 'Enter') doLogin(); }); - // 回车提交覆盖全部输入框(登录用户名框 / 注册四框) - var s2u = $('#login-username'); if (s2u) s2u.addEventListener('keydown', function (e) { if (e.key === 'Enter') doLogin(); }); - var s3 = $('#reg-submit'); if (s3) s3.addEventListener('click', doRegister); - var s4 = $('#reg-send-code'); if (s4) s4.addEventListener('click', sendCode); - ['#reg-username', '#reg-password', '#reg-email', '#reg-code'].forEach(function (sel) { - var el = $(sel); - if (el) el.addEventListener('keydown', function (e) { if (e.key === 'Enter') doRegister(); }); - }); - // 密码明文切换(登录/注册/找回) - [['#login-pwd-eye', '#login-password'], ['#reg-pwd-eye', '#reg-password'], ['#forgot-pwd-eye', '#forgot-password']].forEach(function (pair) { - var eye = $(pair[0]), input = $(pair[1]); - if (!eye || !input) return; - eye.addEventListener('click', function () { - var show = input.type === 'password'; - input.type = show ? 'text' : 'password'; - eye.textContent = show ? '隐藏' : '显示'; - }); - }); - // 发码后修改邮箱 → 提前终止倒计时并恢复按钮,提示需重发 - var regEmail = $('#reg-email'); - if (regEmail) regEmail.addEventListener('input', function () { - var btn = $('#reg-send-code'); - if (btn && btn.disabled && /^\d+s$/.test(btn.textContent)) { - clearInterval(window.__regCodeTimer || 0); - btn.disabled = false; btn.textContent = '发送验证码'; - toast('邮箱已修改,请重新发送验证码', 'info'); - } - }); - // 找回视图——发码/提交/回车/改邮箱终止倒计时(对齐注册交互) - var fsc = $('#forgot-send-code'); if (fsc) fsc.addEventListener('click', doForgotSendCode); - var fsub = $('#forgot-submit'); if (fsub) fsub.addEventListener('click', doForgotReset); - ['#forgot-email', '#forgot-code', '#forgot-password'].forEach(function (sel) { - var el = $(sel); - if (el) el.addEventListener('keydown', function (e) { if (e.key === 'Enter') doForgotReset(); }); - }); - var forgotEmail = $('#forgot-email'); - if (forgotEmail) forgotEmail.addEventListener('input', function () { - var btn = $('#forgot-send-code'); - if (btn && btn.disabled && /^\d+s$/.test(btn.textContent)) { - clearInterval(window.__forgotCodeTimer || 0); - btn.disabled = false; btn.textContent = '发送验证码'; - toast('邮箱已修改,请重新发送验证码', 'info'); - } - }); - // 管理员两步验证——发码/验证提交/回车提交 - var asc = $('#a2fa-send'); if (asc) asc.addEventListener('click', doA2faSendCode); - var asb = $('#a2fa-submit'); if (asb) asb.addEventListener('click', doA2faVerify); - ['#a2fa-email', '#a2fa-code'].forEach(function (sel) { - var el = $(sel); - if (el) el.addEventListener('keydown', function (e) { if (e.key === 'Enter') doA2faVerify(); }); - }); - var s6 = $('#announce-ok'); if (s6) s6.addEventListener('click', function () { closeModal('announce-modal'); }); - - var ucTab = $('#tab-usercenter'); - if (ucTab) ucTab.addEventListener('click', renderUserCenter); - var uaTab = $('#tab-useradmin'); - if (uaTab) uaTab.addEventListener('click', uaShowGate); - var av = $('#user-avatar'); - if (av) av.addEventListener('click', function () { - if (user && user.role === 'admin') { if (uaTab) uaTab.click(); } - else { var t = $('#tab-usercenter'); if (t) t.click(); } - }); - var ucRedeem = $('#uc-card-redeem'); if (ucRedeem) ucRedeem.addEventListener('click', doRedeem); - var ucAnn = $('#uc-ann-view'); if (ucAnn) ucAnn.addEventListener('click', function () { maybeShowAnnouncement(true); }); // 手动查看强制展示 - var ucLogout = $('#uc-logout'); if (ucLogout) ucLogout.addEventListener('click', doLogout); - - uaBindSide(); - var b1 = $('#ua-users-refresh'); if (b1) b1.addEventListener('click', uaLoadUsers); - var b2 = $('#ua-cookie-refresh'); if (b2) b2.addEventListener('click', uaLoadCookieStatus); - var b3 = $('#ua-qr-create'); if (b3) b3.addEventListener('click', uaQrCreate); - var b4 = $('#ua-cookie-delete'); if (b4) b4.addEventListener('click', uaCookieDelete); - var b5 = $('#ua-smtp-save'); if (b5) b5.addEventListener('click', uaSaveSmtp); - var b6 = $('#ua-smtp-test'); if (b6) b6.addEventListener('click', uaTestSmtp); - var b7 = $('#ua-log-refresh'); if (b7) b7.addEventListener('click', function () { uaLoadLogs(); }); - var b8 = $('#ua-log-tab-ok'); if (b8) b8.addEventListener('click', function () { uaLogsType = 'success'; uaLogPage = 1; uaLoadLogs(); }); - var b9 = $('#ua-log-tab-fail'); if (b9) b9.addEventListener('click', function () { uaLogsType = 'fail'; uaLogPage = 1; uaLoadLogs(); }); - var blogPrev = $('#ua-log-prev'); if (blogPrev) blogPrev.addEventListener('click', function () { if (uaLogPage > 1) { uaLogPage--; uaLoadLogs(); } }); - // next 补边界(原实现可翻到空页——前端无 pages 时放宽为 999 防呆) - var blogNext = $('#ua-log-next'); if (blogNext) blogNext.addEventListener('click', function () { var maxP = window._uaLogPages || 999; if (uaLogPage < maxP) { uaLogPage++; uaLoadLogs(); } }); - var blogClear = $('#ua-log-clear'); if (blogClear) blogClear.addEventListener('click', uaClearLogs); - var b10 = $('#ua-card-gen'); if (b10) b10.addEventListener('click', uaGenCards); - var cardType = $('#ua-card-minutes'); - if (cardType) cardType.addEventListener('change', function () { - var custom = $('#ua-card-custom'); - if (custom) custom.hidden = cardType.value !== 'custom'; - if (cardType.value === 'custom' && custom) custom.focus(); - }); - var b11 = $('#ua-ann-save'); if (b11) b11.addEventListener('click', uaSaveAnn); - var b12 = $('#ua-db-backup'); if (b12) b12.addEventListener('click', uaBackup); - var b13 = $('#ua-db-auto'); if (b13) b13.addEventListener('click', uaBackupAuto); - var b14 = $('#ua-db-upload'); - if (b14) b14.addEventListener('change', function () { - if (b14.files && b14.files[0]) uaBackupUpload(b14.files[0]); - b14.value = ''; - }); - - } - - function init() { - bind(); - refreshSession(); - } - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); - } else { - init(); - } -})(); diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index b14aeb0..0000000 --- a/templates/index.html +++ /dev/null @@ -1,7704 +0,0 @@ - - - - - - - 云村小助手 - - - - - - -
-
- 正在加载云村小助手... -
- - -
-
-
- -
-
云村小助手
-
实时解析网易云音乐单曲、获取真实下载地址及封面信息
-
-
-
-
- - -
- - - - -
-
-
- -
- - - - -
- - -
- 💡 小提示:可在「个人歌单」登录你自己的 SVIP 网易云音乐账号,并切换为个人账号解析,即可下载/试听更高音质。 -
- - -
- 🎧 全局音质 - - - 全局生效:单曲解析、音乐下载、批量下载均使用该音质;「最高音质(自动)」会按账号会员类型自动尝试最高可解析音质。 -
- -
-
搜索
-
单曲
-
歌单
-
专辑
-
播客
- - -
歌手
-
榜单
-
个人歌单
- - -
关于网站
-
- - -
-
-
🔍
-
- - - -
- -
-
支持搜索歌曲、歌单、专辑、歌手;结果点击即可跳转对应功能解析。
-
- - -
-
- - -
- -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - - -
-
- - -
-
支持 电台链接(djradio?id=)、单集链接(program?id= / sound?id=)、163cn.tv 短链或纯数字ID;解析后可在线试听、获取直链、下载单集、整台打包 ZIP。
- - - - - - - - - - - - - -
-
-
-
热门播客
-
-
-
- -
-
- - -
- -
-
- - -
-
管理员设置:全局二维码登录与 Cookie 管理。登录后 Cookie 自动保存,无需重复扫码。
-
- - - -
- - -
- -
-
-
- 全局账号歌单展示系统中全局账号(管理员 cookie.txt)收藏的歌单与「我喜欢的音乐」。 - 当前全局账号未登录或 Cookie 已失效,请先在「管理员设置」扫码登录全局账号。 -
-
- - -
-
-
- - - -
- - -
-
- - -
-
- - -
-
- 网易云官方实时排行榜,点击卡片即可查看榜单歌曲 -
-
📊
加载中...
- -
- - -
- -
-
-
- 扫码登录你自己的网易云账号,即可查看收藏的歌单(包括「我喜欢的音乐」)。 - 个人登录仅用于获取你的歌单,解析与下载音乐仍使用管理员全局账号。 -
-
- - -
-
- -
- - - -
- - -
-
- -
-
云村小助手
-
基于 Flask + 原生前端的云村小助手 · 当前版本 v2.1.0
-
-
- -
-
项目介绍
-

- 本项目是一个开箱即用的云村小助手,支持歌曲搜索、单曲 / 歌单 / 专辑 / 歌手解析、官方榜单、个人歌单、播客解析等完整功能, - 提供多音质试听与下载(标准 / 极高 / 无损 / Hi-Res / 沉浸环绕声等)、批量下载与 ZIP 打包、元数据写入、歌词下载(LRC / SRT)、 - 自定义播放器与逐字歌词、评论区、多账号体系(全局账号 + 个人账号)与下载任务进度管理。 - 服务端基于 Flask 与网易云官方接口实现,前端零框架原生实现,支持深色模式与响应式布局,可通过 Docker 一键部署。 -

- -
-
功能说明
-
-
-
🔍 多类型搜索
-
支持按歌曲、歌单、专辑、歌手搜索,结果一键跳转对应功能解析。
-
-
-
🎵 单曲 / 歌单 / 专辑 / 歌手解析
-
输入链接或ID即可解析,自动按账号实测最高可解析音质输出,含封面、歌词与直链。
-
-
-
🎙️ 播客解析
-
支持播客电台与单集解析:在线试听、音频直链、单集下载、整台打包 ZIP,热门播客分类浏览。
-
-
-
📊 官方榜单
-
63 个网易云官方实时排行榜卡片墙,点击即看榜单歌曲。
-
-
-
⬇ 下载能力
-
单曲 / 批量下载、ZIP 打包、文件命名格式、ffmpeg 元数据写入、LRC / SRT 歌词。
-
-
-
🎧 播放器与歌词
-
自定义深色播放器、SMTC 系统媒体控制、逐字 KTV 歌词高亮、翻译双行、偏移校准。
-
-
-
👥 多账号体系
-
全局账号负责解析下载;个人账号扫码登录查看自己的歌单,Cookie 保存在本机浏览器(站点账号登录时管理后台可按需查看)。
-
-
-
🔒 隐私与任务管理
-
隐私协议 Cookie 偏好、下载任务进度悬浮窗、断线续传与离线暂停恢复、接口统计。
-
-
- -
-
更新日志
-
- -
本工具仅供个人学习交流使用,音频内容版权归网易云音乐及内容创作者所有,请勿用于商业用途。
-
- - -
-
🔒
请先登录后查看个人中心
- -
- - -
-
🔒
仅管理员可访问管理后台
- -
-
- - - - - - - - - - - - - - - -
- - -
- - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
实时排队
-
0
-
预计等待:--
-
下载 - · 打包 -
-
- - -
-
- 📥 下载任务 0 - -
-
-
- - - - - - - diff --git a/uv.lock b/uv.lock index 10fcd93..1deedfc 100644 --- a/uv.lock +++ b/uv.lock @@ -743,18 +743,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, ] -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - [[package]] name = "loguru" version = "0.7.3" @@ -768,69 +756,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, ] -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - [[package]] name = "multidict" version = "6.9.1" @@ -1813,7 +1738,6 @@ dependencies = [ { name = "fastapi" }, { name = "filetype" }, { name = "itsdangerous" }, - { name = "jinja2" }, { name = "mutagen" }, { name = "python-multipart" }, { name = "qrcode" }, @@ -1840,7 +1764,6 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.115,<1.0" }, { name = "filetype", specifier = ">=1.2.0" }, { name = "itsdangerous", specifier = ">=2.2,<3.0" }, - { name = "jinja2", specifier = ">=3.1,<4.0" }, { name = "mutagen", specifier = ">=1.48.1" }, { name = "python-multipart", specifier = ">=0.0.9" }, { name = "qrcode", specifier = ">=7.0,<8.3" }, diff --git a/web/app.py b/web/app.py index 413f609..6031903 100644 --- a/web/app.py +++ b/web/app.py @@ -109,11 +109,6 @@ def create_app(api_prefix: str = '', *, start_monitor: bool = True) -> FastAPI: # 响应侧统一处理:安全头 + 音频直链签名保护 + 版本指纹抹除 app.add_middleware(ResponseHygieneMiddleware) - # 静态资源(/static/site_auth.js 等)——与 Flask static_folder 同目录 - _static_dir = Path(__file__).resolve().parent.parent / 'static' - if _static_dir.is_dir(): - app.mount('/static', StaticFiles(directory=str(_static_dir)), name='static') - # React 构建产物(vite build → frontend/dist,hash 指纹资源挂 /assets) _dist_dir = Path(__file__).resolve().parent.parent / 'frontend' / 'dist' if (_dist_dir / 'assets').is_dir(): diff --git a/web/routers/index.py b/web/routers/index.py index 2ab14dd..e7cf57a 100644 --- a/web/routers/index.py +++ b/web/routers/index.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -"""首页与静态资源。 +"""首页。 -- GET / 首页:优先服务 React 构建产物 frontend/dist/index.html; - dist 未构建时回退旧版 templates/index.html(平滑过渡) -- 静态资源由 Starlette StaticFiles 挂载 /assets 与 /static(见 web/app.py) +- GET / 首页:服务 React 构建产物 frontend/dist/index.html + (vite 固定产物文件名,字节级稳定,golden 契约依赖此点) +- 构建产物经 /assets 挂载(见 web/app.py) """ from pathlib import Path @@ -14,20 +14,11 @@ router = APIRouter() _PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent _DIST_INDEX = _PROJECT_ROOT / 'frontend' / 'dist' / 'index.html' -_LEGACY_TEMPLATE = _PROJECT_ROOT / 'templates' / 'index.html' def _render_index() -> str: - """渲染首页 HTML:React 构建产物优先,缺失时回退旧模板。""" - if _DIST_INDEX.is_file(): - return _DIST_INDEX.read_text(encoding='utf-8') - import jinja2 - - env = jinja2.Environment( - loader=jinja2.FileSystemLoader(str(_LEGACY_TEMPLATE.parent)), - autoescape=True, - ) - return env.get_template(_LEGACY_TEMPLATE.name).render() + """返回 React 构建产物原样字节(构建产物入库,恒存在)。""" + return _DIST_INDEX.read_text(encoding='utf-8') @router.get('/', response_class=HTMLResponse)