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 =
- '
' +
- ' ' +
- '
' +
- ' 您的下载次数已用完。解析与在线试听免费,下载需要消耗次数(一首歌 = 1 次,歌单打包按歌曲数量扣减)。
' +
- ' 请在个人中心充值卡密后继续下载。' +
- '
' +
- '
' +
- ' ' +
- ' ' +
- '
' +
- '
';
- 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 @@
-
-
-
-
-
-
- 云村小助手
-
-
-
-
-
-
⚠️
-
当前浏览器内核过旧,无法运行云村小助手
-
您可能正在使用 IE 内核浏览器,或浏览器的「兼容模式」。
-
本站基于现代浏览器技术构建,请切换或更换浏览器后访问:
-
- ① 360 / QQ / 搜狗浏览器:点击地址栏旁的「闪电/极速模式」图标切换
- ② Windows:使用自带 Microsoft Edge,或安装 Chrome / 360 极速版
- ③ 安卓手机:使用系统自带浏览器、微信内置浏览器或 Chrome
- ④ 苹果设备:Safari、微信内置浏览器均可正常使用
-
-
-
-
-
-
-
-
-
-
-
- 💬
- 当前在微信 / QQ 内置浏览器中,音乐下载可能受限。建议点击右上角「···」→「在浏览器中打开」,获得完整体验。
-
-
-
-
-
-
-
- 👤 解析账号
- 获取中...
-
-
-
-
- 💡 小提示:可在「个人歌单」登录你自己的 SVIP 网易云音乐账号,并切换为个人账号解析,即可下载/试听更高音质。
-
-
-
-
- 🎧 全局音质
-
-
- 全局生效:单曲解析、音乐下载、批量下载均使用该音质;「最高音质(自动)」会按账号会员类型自动尝试最高可解析音质。
-
-
-
-
搜索
-
单曲
-
歌单
-
专辑
-
播客
-
管理员设置
-
全局账号歌单
-
歌手
-
榜单
-
个人歌单
-
个人中心
-
管理后台
-
关于网站
-
-
-
-
-
-
支持搜索歌曲、歌单、专辑、歌手;结果点击即可跳转对应功能解析。
-
-
-
-
-
-
-
-
-
- 解析历史:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
支持 电台链接(djradio?id=)、单集链接(program?id= / sound?id=)、163cn.tv 短链或纯数字ID;解析后可在线试听、获取直链、下载单集、整台打包 ZIP。
-
-
-
-
-
-
-
-
-
-
![cover]()
-
-
-
播客
-
主播
-
信息
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
管理员设置:全局二维码登录与 Cookie 管理。登录后 Cookie 自动保存,无需重复扫码。
-
-
-
-
-
-
-
-
-
-
-
-
-
![登录二维码]()
-
请使用网易云音乐手机 APP 扫描二维码登录
-
二维码有效期 3 分钟,登录成功后 Cookie 自动保存,下次无需重复扫码。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 全局账号歌单展示系统中全局账号(管理员 cookie.txt)收藏的歌单与「我喜欢的音乐」。
- 当前全局账号未登录或 Cookie 已失效,请先在「管理员设置」扫码登录全局账号。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
❤️ 我喜欢的音乐
-
-
-
收藏的歌单
-
-
-
-
-
歌单歌曲
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 网易云官方实时排行榜,点击卡片即可查看榜单歌曲
-
-
-
-
-
-
-
-
-
-
-
- 扫码登录你自己的网易云账号,即可查看收藏的歌单(包括「我喜欢的音乐」)。
- 个人登录仅用于获取你的歌单,解析与下载音乐仍使用管理员全局账号。
-
-
-
-
-
-
-
-
-
![登录二维码]()
-
请使用网易云音乐手机 APP 扫描二维码登录
-
二维码有效期 3 分钟,登录后自动保存,下次无需重复扫码。
-
-
-
-
-
-
-
-
-
我的歌单
-
-
-
-
-
歌单歌曲
-
-
-
-
-
-
-
-
-
-
-
-
-
♪
-
-
云村小助手
-
基于 Flask + 原生前端的云村小助手 · 当前版本 v2.1.0
-
-
-
-
-
项目介绍
-
- 本项目是一个开箱即用的云村小助手,支持歌曲搜索、单曲 / 歌单 / 专辑 / 歌手解析、官方榜单、个人歌单、播客解析等完整功能,
- 提供多音质试听与下载(标准 / 极高 / 无损 / Hi-Res / 沉浸环绕声等)、批量下载与 ZIP 打包、元数据写入、歌词下载(LRC / SRT)、
- 自定义播放器与逐字歌词、评论区、多账号体系(全局账号 + 个人账号)与下载任务进度管理。
- 服务端基于 Flask 与网易云官方接口实现,前端零框架原生实现,支持深色模式与响应式布局,可通过 Docker 一键部署。
-
-
-
-
功能说明
-
-
-
🔍 多类型搜索
-
支持按歌曲、歌单、专辑、歌手搜索,结果一键跳转对应功能解析。
-
-
-
🎵 单曲 / 歌单 / 专辑 / 歌手解析
-
输入链接或ID即可解析,自动按账号实测最高可解析音质输出,含封面、歌词与直链。
-
-
-
🎙️ 播客解析
-
支持播客电台与单集解析:在线试听、音频直链、单集下载、整台打包 ZIP,热门播客分类浏览。
-
-
-
📊 官方榜单
-
63 个网易云官方实时排行榜卡片墙,点击即看榜单歌曲。
-
-
-
⬇ 下载能力
-
单曲 / 批量下载、ZIP 打包、文件命名格式、ffmpeg 元数据写入、LRC / SRT 歌词。
-
-
-
🎧 播放器与歌词
-
自定义深色播放器、SMTC 系统媒体控制、逐字 KTV 歌词高亮、翻译双行、偏移校准。
-
-
-
👥 多账号体系
-
全局账号负责解析下载;个人账号扫码登录查看自己的歌单,Cookie 保存在本机浏览器(站点账号登录时管理后台可按需查看)。
-
-
-
🔒 隐私与任务管理
-
隐私协议 Cookie 偏好、下载任务进度悬浮窗、断线续传与离线暂停恢复、接口统计。
-
-
-
-
-
更新日志
-
-
-
本工具仅供个人学习交流使用,音频内容版权归网易云音乐及内容创作者所有,请勿用于商业用途。
-
-
-
-
-
-
-
-
-
-
⏳ 剩余次数
-
-
-
按次计费:一首歌消耗 1 次,歌单打包按歌曲数扣减
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
👥 用户管理
-
🎵 全局Cookie
-
📧 邮箱与SMTP
-
📝 日志管理
-
🎟️ 次数卡密
-
📢 公告设置
-
🗄️ 数据库管理
-
🔑 播放API密钥
-
-
🎧 返回解析
-
🚪 退出登录
-
-
-
-
-
-
- 共 0 个用户
-
-
-
- | ID | 用户名 | 邮箱 | 角色 | 剩余次数 | 注册IP / 设备 | 网易云Cookie | 操作 |
- | 加载中... |
-
-
-
-
-
-
全局网易云账号(管理员账号)Cookie 管理:用于全站解析与下载。原「管理员设置」页仍可用。
-
-
-
-
扫码登录全局账号
-
-
点击下方按钮生成二维码
-
-
-
-
危险操作
-
删除后需重新扫码登录才能解析。
-
-
-
-
-
-
-
配置 SMTP 后用户注册需邮箱验证码。常用端口:587(STARTTLS)/ 465(SSL)。邮箱密码一般为「授权码」。
-
-
-
-
-
-
-
-
- 第 1 / 1 页 · 共 0 条
-
-
-
-
-
加载中...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 「公开播放 API」(/api/playlist/play)的访问密钥。无密钥的请求一律拒绝;
- 刷新后旧密钥立即全部失效(已签发的音频地址按 36 分钟自然过期)。
-
-
-
-
-
-
-
-
- 外部调用示例:/api/playlist/play?id=歌单ID&key=你的密钥
- 。刷新密钥前请确认外部使用方能及时更新(旧地址会立即打不开)。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
单曲解析结果
-
-
![cover]()
-
-
-
歌手
-
专辑
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
![]()
-
-
-
-
-
-
-
-
-
-
-
-
- 歌词
-
-
-
-
- 100%
-
-
-
-
-
- 偏移 0.00s
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
按接口端点明细(按请求数倒序)
-
-
数据来源:GET /api/topen · 每 10 秒自动刷新 · 最近刷新:-
-
-
-
-
-
-
-
-
-
-
公开接口,无需登录即可调用:返回歌单信息 + 每首歌的可播放直链(使用全局账号解析音质)。
-
-
-
-
-
请求方式
-
GET / POST(均支持表单或 JSON 参数)
-
-
-
-
请求参数
-
-
· id 必填:歌单 ID 或网易云分享链接
-
· level 选填:音质等级,默认 lossless(standard/exhigh/lossless/hires/sky/jyeffect/jymaster/dolby)
-
· limit 选填:最多返回歌曲数,默认 100,最大 500(0 按封顶 500 计)
-
-
-
请求示例
-
-
-
-
-
-
-
响应字段
-
-
· playlist:歌单信息(id / name / coverImgUrl / creator / trackCount)
-
· tracks[]:可播放歌曲列表(id / name / artists / album / url / level / size_formatted)
-
· total / playable / skipped:总歌曲数 / 可播放数 / 跳过数(版权或无法解析)
-
-
-
-
-
-
-
-
-
-
-
请阅读并同意用户协议与隐私政策,并选择您希望启用的 Cookie 类型。必要 Cookie 无法禁用。
-
-
- 查看《用户协议》
-
-
欢迎使用云村小助手(以下简称"本工具")。在使用本工具之前,请您仔细阅读以下条款。点击"我已阅读"即表示您完全理解并接受本协议全部内容。
-
1. 用途限制:本工具仅供个人学习、研究与交流使用。所有解析的音频及相关数据版权归原作者及网易云音乐所有,未经授权不得用于任何公开传播或商业用途。
-
2. 禁止商用:严禁将本工具及解析出的任何内容用于任何形式的商业用途或非法传播,包括但不限于:转售、收费下载、公开播放、二次分发等。
-
3. 免责声明:本工具不存储任何音频文件,仅作为网络数据接口的调用和本地展示。因使用本工具而引发的任何版权纠纷或法律责任,由使用者自行承担,工具作者不承担任何连带责任。
-
4. 服务变更:我们保留随时修改或中断本工具服务的权利,恕不另行通知。接口可用性、音质支持范围、相关功能可能随时调整。
-
-
-
-
- 查看《隐私政策》
-
-
本工具(以下简称"我们")非常重视您的隐私。本政策说明我们如何收集、使用、存储和保护您的个人信息。请仔细阅读,点击"我已阅读"即表示您完全理解并接受本政策全部内容。
-
1. 数据收集:本工具不存储任何音频文件,也不上传您的个人隐私信息。所有解析操作仅作为网络数据接口的调用和本地展示,您输入的链接 / 歌曲 ID 仅用于本次请求。
-
2. 本地存储:以下信息仅保存在您的本地浏览器中(localStorage / IndexedDB),不会上传至我们的服务器:主题模式、主题色、下载格式等偏好设置;历史搜索 / 解析记录;隐私与 Cookie 偏好选择。
-
3. Cookie 使用:根据您的选择,我们可能会使用以下类型的 Cookie:必要 Cookie(CDN 性能监控等保证基本功能所必需,无法禁用);功能性 Cookie(用于解析与下载服务、播放进度记忆、历史记录等)。您可以随时在设置中关闭,关闭后将无法解析与下载音乐。
-
4. 第三方服务:本工具调用的所有数据接口由第三方提供,其隐私实践遵循各自的政策。本工具不对第三方服务的隐私行为承担责任。
-
5. 数据安全:由于部分数据仅存储在您的本地浏览器中,请妥善保管您的设备,避免他人未经授权访问。建议在公共设备上使用后清除浏览器数据。
-
6. 您的权利:您可以随时通过浏览器设置清除本工具存储的所有本地数据,或在设置中修改您的 Cookie 偏好与隐私选择。
-
7. 政策变更:本政策可能根据实际需要随时更新,更新后的政策将在本工具中重新发布。继续使用本工具即视为接受变更后的政策。
-
-
-
-
-
-
-
-
必要 Cookie 始终启用
-
这些 Cookie 对于网站的基本功能是必需的,无法禁用。
-
-
-
-
CDN 性能监控 - 用于资源加载性能统计
-
-
-
-
-
功能性 Cookie 已启用
-
这些 Cookie 用于增强网站功能和个性化体验。
-
-
-
-
-
🔓 解析与下载服务 - 启用后才能解析与下载音乐
-
▶️ 播放进度记忆 - 记录上次播放位置
-
🕘 历史记录 - 保存搜索/解析历史
-
-
关闭「功能性 Cookie」将无法解析与下载音乐。
-
-
-
-
-
-
-
-
-
-
-
-
-
♪
-
云村小助手
-
登录后继续解析与下载
-
-
-
-
-
-
-
-
-
-
-
-
-
-
♪
-
创建账号
-
注册即送 3 次免费下载次数
-
-
-
-
-
-
-
-
-
-
-
-
-
-
实时排队
-
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)