chore: 清理前端与容器脚本中指向已删台账的过时编号

模板/样式/脚本的注释里散落着「第 N 条」「F59」「P3-43」这类编号,它们指向的
修复验收台账与交接文档已在本轮删除——留着既检索不到、也对不上号,还让后来读
注释的人以为存在一份可查的编号清单。本次只动注释,正文零改动。

覆盖范围(全仓一致):
- templates/index.html 163 处、static/site_auth.js 55 处,共 218 处编号整段摘除
  (含「第 N 条(Px):」前缀与「(Fxx 短标签)」括号)。
- 容器/配置 16 处:docker-compose.yml、entrypoint.sh(F59 / 第 93 条 / 第 97 条)、
  Dockerfile(T7 / 第 97 条)、.dockerignore(T1 / 第 97 条 / 第 55 条 / P2-9)、
  .env.example(F59 / 第 100 条 / 「详见交接文档 F11」这条失效指引)、.gitignore(P3-41)。
- Python 4 处:core/config.py(D-1)、services/tasks.py(O-5×2)、services/_shared.py(O-5)
  ——上一轮 Python 侧清理的漏网。

为什么规则要收窄(两类误伤,实测踩到):
1. 标签首字母不能用泛化的 [A-Z]:X5 是 QQ 浏览器内核、ES5 是 JS 版本,都不是编号。
   泛化后会把「微信 XWeb/X5、QQ 内置」改成「微信 XWeb/、QQ 内置」,把
   「Chromium <87 的老 X5 不识别 inset」改成「老 不识别」。现按本仓真实用过的
   任务族白名单收窄(A B C D F O P R S T U),X5/ES5 原样保留。
2. 括号内「编号 + 长说明」不能整段删:「(F51 只防了切歌,这里补同歌翻页)」
   整段删掉会连说明一起丢;现只摘编号、保留说明文字。

为什么同步改黄金契约快照:题头注释属于模板内容,改了注释首页字节数必然变
(466261 → 464333),tests/golden/responses.json 的 index 条目按既定流程刷新
html_len/html_sha256;并在 tests/test_golden.py 补一段说明,写清该条目记的是
「当前模板」而非 Flask 期数值、以及有意改模板后如何重算
(html_len = 文件字节数 - 1,sha256 = 文件去掉末尾换行后的哈希)。

验证(全部通过):
- 与 HEAD 比对:剥离全部注释后的正文逐行一致(唯一差异是删掉了一处行尾空注释),
  行数不变、每行半角括号/引号数量不变 → 代码与字符串零改动;
- node --check 通过(静态 JS + index.html 内联脚本),<style> 块花括号平衡;
- 真实 uvicorn 冒烟:/health /user/status /api/topen → 200、/admin/site/backup/list → 403,
  /static/site_auth.js 响应体与本地文件 sha1 相同,/ 无「第 N 条」残留;
- ruff All checks passed;pyright 0 errors / 0 warnings;pytest 144 passed。
This commit is contained in:
snowy-yang committed 2026-09-26 13:18:44 +08:00
1 parent 6288a98ecc
commit da3d013e9c
13 files changed
+246 -242

No files matched your search

+5 -5
View File
@@ -1,4 +1,4 @@
# T1:构建镜像严禁带入的敏感/私有/无关内容 # 构建镜像严禁带入的敏感/私有/无关内容
LICENSE LICENSE
README.md README.md
.gitignore .gitignore
@@ -15,7 +15,7 @@ _smoke_test_downloader.py
_test_podcast_api.py _test_podcast_api.py
部署文档.md 部署文档.md
功能介绍.md 功能介绍.md
# T1 新增:真实凭据、用户数据库、密钥、日志、交接文档、运维脚本与审查工作目录 # 真实凭据、用户数据库、密钥、日志、交接文档、运维脚本与审查工作目录
cookie.txt cookie.txt
user_cookie.txt user_cookie.txt
# Cookie 备份(qr_login._backup_cookie 产生,含真实 MUSIC_U): # Cookie 备份(qr_login._backup_cookie 产生,含真实 MUSIC_U):
@@ -25,14 +25,14 @@ user_cookie.*.txt
data/ data/
data/backups/ data/backups/
*.log *.log
# 第 97 条(P3):轮转件 music_api.log.1/2/3 不匹配 *.log——含 IP/UA 的历史日志 # 轮转件 music_api.log.1/2/3 不匹配 *.log——含 IP/UA 的历史日志
# 曾随 _mkpkg 同类问题外泄(第 55 条),docker 轨道同步堵上 # 曾随 _mkpkg 同类问题外泄,docker 轨道同步堵上
music_api.log* music_api.log*
*.log.* *.log.*
交接文档.md 交接文档.md
修复验收台账_F01-F117.md 修复验收台账_F01-F117.md
待修问题清单_三轮汇总.md 待修问题清单_三轮汇总.md
# P2-9:补齐后产生的审计清单与内部引导文件(含内部路径/运维细节) # 补齐后产生的审计清单与内部引导文件(含内部路径/运维细节)
待修问题清单_第四轮深挖.md 待修问题清单_第四轮深挖.md
待修问题清单_第五轮深挖.md 待修问题清单_第五轮深挖.md
新会话引导.md 新会话引导.md
+3 -3
View File
@@ -1,7 +1,7 @@
# 复制为 .env 后按需修改(F59:不提供 .env 时 docker-compose 使用默认 TZ,可直接运行) # 复制为 .env 后按需修改(不提供 .env 时 docker-compose 使用默认 TZ,可直接运行)
TZ=Asia/Shanghai TZ=Asia/Shanghai
# HTTPS 反代(Caddy/Nginx 等)上线后取消注释:会话 Cookie 仅经 HTTPS 传输 # HTTPS 反代(Caddy/Nginx 等)上线后取消注释:会话 Cookie 仅经 HTTPS 传输
# (纯 HTTP 部署不要开启,否则浏览器拒收 Cookie;详见交接文档 F11) # (纯 HTTP 部署不要开启,否则浏览器拒收 Cookie)
# ENABLE_SECURE_COOKIE=1 # ENABLE_SECURE_COOKIE=1
# 第 100 条:跨域来源控制——默认 `*`(历史口径);自用收敛同源可改为实际域名 # 跨域来源控制——默认 `*`(历史口径);自用收敛同源可改为实际域名
# CORS_ORIGINS=https://qf.linxingran.top # CORS_ORIGINS=https://qf.linxingran.top
+1 -1
View File
@@ -9,7 +9,7 @@ __pycache__
*.pyo *.pyo
*.pyd *.pyd
# P3-41:运行时敏感数据与私有文档——公开包买家若将本项目纳入 git 管理, # 运行时敏感数据与私有文档——公开包买家若将本项目纳入 git 管理,
# 以下内容绝不能进版本库(用户数据库/网易云 Cookie/日志/审计文档) # 以下内容绝不能进版本库(用户数据库/网易云 Cookie/日志/审计文档)
data/ data/
cookie.txt cookie.txt
+2 -2
View File
@@ -1,8 +1,8 @@
# T7:基础镜像升级到 Python 3.12(3.9 已于 2025-10 EOL);alpine + ffmpeg 组合保持不变 # 基础镜像 Python 3.12(3.9 已于 2025-10 EOL);alpine + ffmpeg 组合保持不变
FROM python:3.12-alpine3.21 FROM python:3.12-alpine3.21
WORKDIR /app WORKDIR /app
# 使用阿里云 apk 镜像源加速安装 ffmpeg # 使用阿里云 apk 镜像源加速安装 ffmpeg
# 第 97 条(P3):补 tzdata——alpine 无 zoneinfo,ENV TZ=Asia/Shanghai 不装它不生效 # 补 tzdata——alpine 无 zoneinfo,ENV TZ=Asia/Shanghai 不装它不生效
# (容器内日志/展示本地时间差 8 小时;计费/会话全部 epoch 秒不受影响) # (容器内日志/展示本地时间差 8 小时;计费/会话全部 epoch 秒不受影响)
RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories && \ RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories && \
apk add --no-cache tzdata ffmpeg apk add --no-cache tzdata ffmpeg
+1 -1
View File
@@ -11,7 +11,7 @@ from dataclasses import dataclass
# 站点统一版本号(/health、/api/info、前端 CHANGELOG 全部以此为准,别处不得硬编码) # 站点统一版本号(/health、/api/info、前端 CHANGELOG 全部以此为准,别处不得硬编码)
APP_VERSION = '2.4.0' APP_VERSION = '2.4.0'
# 交付结果保留期(/D-1:服务端与前端「领取」入口统一 35 分钟——前端入口保留 35 分钟, # 交付结果保留期(服务端与前端「领取」入口统一 35 分钟——前端入口保留 35 分钟,
# 原 30 分钟会在最后 5 分钟让领取必 404)。终态任务记录、任务目录、下载缓存均按此回收。 # 原 30 分钟会在最后 5 分钟让领取必 404)。终态任务记录、任务目录、下载缓存均按此回收。
RESULT_RETENTION_SECONDS = 35 * 60 RESULT_RETENTION_SECONDS = 35 * 60
+2 -2
View File
@@ -3,12 +3,12 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
# F59:.env 缺失时不再启动失败(required 属性 compose v2.24+ 支持;老版本可删掉该行) # .env 缺失时不再启动失败(required 属性 compose v2.24+ 支持;老版本可删掉该行)
env_file: env_file:
- path: .env - path: .env
required: false required: false
environment: environment:
- TZ=Asia/Shanghai # 未提供 .env 时的默认时区(F59:文档"默认即可运行"现在成立) - TZ=Asia/Shanghai # 未提供 .env 时的默认时区("默认即可运行"现在成立)
- ENABLE_SECURE_COOKIE=${ENABLE_SECURE_COOKIE:-} # HTTPS 反代上线后设 1(见 .env.example) - ENABLE_SECURE_COOKIE=${ENABLE_SECURE_COOKIE:-} # HTTPS 反代上线后设 1(见 .env.example)
ports: ports:
- "5001:5000" # 对外 5001 端口(容器内服务监听 5000) - "5001:5000" # 对外 5001 端口(容器内服务监听 5000)
+3 -3
View File
@@ -1,8 +1,8 @@
#!/bin/sh #!/bin/sh
# F59:exec 让 python 成为 PID 1 直接接收 SIGTERM,容器停止不再经 shell 转发而超时 # exec 让 python 成为 PID 1 直接接收 SIGTERM,容器停止不再经 shell 转发而超时
# 第 93 条(P2-4):compose 挂载 ./user_cookie.txt 时若宿主不存在该文件,Docker 会 # compose 挂载 ./user_cookie.txt 时若宿主不存在该文件,Docker 会
# 在启动前创建同名**目录**并挂载——容器内 touch 只对无卷 docker run 有效。 # 在启动前创建同名**目录**并挂载——容器内 touch 只对无卷 docker run 有效。
# 第 97 条(P3):检测到同名目录直接报错退出(fail-fast),不再静默带病运行 # 检测到同名目录直接报错退出(fail-fast),不再静默带病运行
for f in user_cookie.txt cookie.txt; do for f in user_cookie.txt cookie.txt; do
if [ -d "/app/$f" ]; then if [ -d "/app/$f" ]; then
echo "FATAL: /app/$f 是目录(宿主机缺文件时 Docker 挂载会建同名目录)。请在宿主机先 touch 该文件再启动。" >&2 echo "FATAL: /app/$f 是目录(宿主机缺文件时 Docker 挂载会建同名目录)。请在宿主机先 touch 该文件再启动。" >&2
+1 -1
View File
@@ -69,7 +69,7 @@ _TASKS_SWEEP_LAST = {'ts': 0.0} # 上次 tasks/ 孤儿目录补扫时间
def _sweep_rate_buckets() -> None: def _sweep_rate_buckets() -> None:
"""限流桶周期清扫(O-5):把超过 10 分钟无新请求的桶移出,防止低流量长期运行下 """限流桶周期清扫:把超过 10 分钟无新请求的桶移出,防止低流量长期运行下
缓慢累积(原实现只在"新建第 8001 个桶"时才触发清理,时机太晚)""" 缓慢累积(原实现只在"新建第 8001 个桶"时才触发清理,时机太晚)"""
now = time.time() now = time.time()
with _RATE_LOCK: with _RATE_LOCK:
+2 -2
View File
@@ -79,7 +79,7 @@ def _task_monitor_loop() -> None:
- 终态任务按 completed_at 保留 35 分钟(与前端「领取」入口对齐) - 终态任务按 completed_at 保留 35 分钟(与前端「领取」入口对齐)
- 运行中/排队/暂停任务超过 4 小时仍未结束 → 置取消(cancel Event 一并置位) - 运行中/排队/暂停任务超过 4 小时仍未结束 → 置取消(cancel Event 一并置位)
- 下载缓存按年龄清扫(配套,同为 35 分钟口径) - 下载缓存按年龄清扫(配套,同为 35 分钟口径)
- 顺带清理限流桶中的过期项(O-5:不再只依赖"新建桶时"的清理时机) - 顺带清理限流桶中的过期项(不再只依赖"新建桶时"的清理时机)
""" """
while True: while True:
time.sleep(HEARTBEAT_INTERVAL) time.sleep(HEARTBEAT_INTERVAL)
@@ -167,7 +167,7 @@ def _task_monitor_loop() -> None:
auth_system.ledger_settle(tid, 0, 'timeout') auth_system.ledger_settle(tid, 0, 'timeout')
except Exception: except Exception:
pass pass
# ---- 下载缓存清扫(保留期同交付口径 35 分钟)+ 限流桶过期项顺手清理(O-5)---- # ---- 下载缓存清扫(保留期同交付口径 35 分钟)+ 限流桶过期项顺手清理 ----
_sweep_download_cache(RESULT_RETENTION_SECONDS) _sweep_download_cache(RESULT_RETENTION_SECONDS)
_sweep_rate_buckets() _sweep_rate_buckets()
# 账本周期兜底(原仅启动执行一次)---- # 账本周期兜底(原仅启动执行一次)----
+56 -56
View File
@@ -5,7 +5,7 @@
(function () { (function () {
'use strict'; 'use strict';
/* P2-11:Promise.finally 老内核 polyfill(与 index.html 同一份标准实现—— /* Promise.finally 老内核 polyfill(与 index.html 同一份标准实现——
index.html 的脚本先加载,这里再兜底一次确保独立引用时也安全) */ index.html 的脚本先加载,这里再兜底一次确保独立引用时也安全) */
if (!Promise.prototype.finally) { if (!Promise.prototype.finally) {
Promise.prototype.finally = function (cb) { Promise.prototype.finally = function (cb) {
@@ -71,7 +71,7 @@
return r.json().then(function (d) { if (timer) clearTimeout(timer); return d; }, return r.json().then(function (d) { if (timer) clearTimeout(timer); return d; },
function (e) { if (timer) clearTimeout(timer); throw e; }); function (e) { if (timer) clearTimeout(timer); throw e; });
}).catch(function () { }).catch(function () {
// F9:默认失败兜底——网络断/超时/非 JSON 响应不再产生 unhandledrejection // 默认失败兜底——网络断/超时/非 JSON 响应不再产生 unhandledrejection
// 噪音(喂 fe_error 上报通道),统一转为标准失败对象走调用方既有的 toast // 噪音(喂 fe_error 上报通道),统一转为标准失败对象走调用方既有的 toast
if (timer) clearTimeout(timer); if (timer) clearTimeout(timer);
return { status: 0, success: false, message: '网络异常,请检查连接后重试' }; return { status: 0, success: false, message: '网络异常,请检查连接后重试' };
@@ -92,9 +92,9 @@
/* ---------------- 设备标识 ---------------- */ /* ---------------- 设备标识 ---------------- */
function getDeviceId() { function getDeviceId() {
/* F-7(第 79 条):与 index.html 的 getDeviceId 统一为同一键/同一值—— /* 与 index.html 的 getDeviceId 统一为同一键/同一值——
注册指纹与下载任务/心跳的设备号不再各走一套; 注册指纹与下载任务/心跳的设备号不再各走一套;
内存缓存兜底 localStorage 不可写时同一页面内保持稳定(F108 同款)。 */ 内存缓存兜底 localStorage 不可写时同一页面内保持稳定。 */
if (window.__siteDeviceIdCache) return window.__siteDeviceIdCache; if (window.__siteDeviceIdCache) return window.__siteDeviceIdCache;
var KEY = 'netease_device_id'; var KEY = 'netease_device_id';
var d = ''; var d = '';
@@ -111,8 +111,8 @@ function getDeviceId() {
var user = null; var user = null;
var durationText = ''; var durationText = '';
var ann = null; var ann = null;
var smtpOk = false; // F-8.2:fail-closed——/auth/me 返回前不显示注册入口(防止未配置 SMTP 的窗口期误开) var smtpOk = false; // fail-closed——/auth/me 返回前不显示注册入口(防止未配置 SMTP 的窗口期误开)
var registerCredits = 3; // F-6.1:注册赠送次数由 /auth/me 下发,文案不再硬编码 var registerCredits = 3; // 注册赠送次数由 /auth/me 下发,文案不再硬编码
/* ---------------- 弹窗 ---------------- */ /* ---------------- 弹窗 ---------------- */
function openModal(id) { var m = $('#' + id); if (m) m.classList.add('show'); } function openModal(id) { var m = $('#' + id); if (m) m.classList.add('show'); }
@@ -129,9 +129,9 @@ function getDeviceId() {
['goto-register', 'goto-register2'].forEach(function (id) { ['goto-register', 'goto-register2'].forEach(function (id) {
var a = $('#' + id); if (a) a.hidden = !showRegister; var a = $('#' + id); if (a) a.hidden = !showRegister;
}); });
// F-8.1:两个弹窗(登录/注册)里的试用提示都要受管——原 querySelector 只命中第一个 // 两个弹窗(登录/注册)里的试用提示都要受管——原 querySelector 只命中第一个
$$('.auth-trial-box').forEach(function (box) { box.hidden = !showRegister; }); $$('.auth-trial-box').forEach(function (box) { box.hidden = !showRegister; });
$$('.reg-gift-num').forEach(function (el) { el.textContent = registerCredits; }); // F-6.1 $$('.reg-gift-num').forEach(function (el) { el.textContent = registerCredits; });
var guest = $('#auth-guest-area'), uarea = $('#auth-user-area'); var guest = $('#auth-guest-area'), uarea = $('#auth-user-area');
if (guest && uarea) { if (guest && uarea) {
if (user) { if (user) {
@@ -163,12 +163,12 @@ function getDeviceId() {
durationText = d.duration_text || ''; durationText = d.duration_text || '';
ann = d.announcement || null; ann = d.announcement || null;
smtpOk = !!d.smtp_configured; smtpOk = !!d.smtp_configured;
// F-6.1:注册赠送次数动态下发(/auth/me),文案不再硬编码 // 注册赠送次数动态下发(/auth/me),文案不再硬编码
if (typeof d.register_credits === 'number') registerCredits = d.register_credits; if (typeof d.register_credits === 'number') registerCredits = d.register_credits;
renderAuthArea(); renderAuthArea();
renderUserCenter(); renderUserCenter();
maybeShowAnnouncement(); maybeShowAnnouncement();
// A1:确认登录后再恢复下载任务(游客调任务接口会 401 误弹登录框) // 确认登录后再恢复下载任务(游客调任务接口会 401 误弹登录框)
if (user && typeof window.SITE_TASKS_BOOTSTRAP === 'function') { if (user && typeof window.SITE_TASKS_BOOTSTRAP === 'function') {
try { window.SITE_TASKS_BOOTSTRAP(); } catch (e) {} try { window.SITE_TASKS_BOOTSTRAP(); } catch (e) {}
} }
@@ -190,14 +190,14 @@ function getDeviceId() {
showExpiredModal(); showExpiredModal();
return; return;
} }
// F53:会话失效时清掉内存里的旧用户状态再引导登录 // 会话失效时清掉内存里的旧用户状态再引导登录
//(原实现保留旧 user,页面继续显示"已登录"却处处 401) //(原实现保留旧 user,页面继续显示"已登录"却处处 401)
user = null; user = null;
durationText = ''; durationText = '';
renderAuthArea(); renderAuthArea();
renderUserCenter(); renderUserCenter();
authToastOnce(msg || '登录已失效,请重新登录'); authToastOnce(msg || '登录已失效,请重新登录');
// 第 87 条:登录弹窗已打开且正处于找回/两步验证视图时,不复位视图—— // 登录弹窗已打开且正处于找回/两步验证视图时,不复位视图——
// 未登录期间后台任务轮询/心跳会持续吃 401 反复进入本函数,若每次都把 // 未登录期间后台任务轮询/心跳会持续吃 401 反复进入本函数,若每次都把
// 弹窗拽回登录视图,管理员正在输入的验证码会被打断、票据被清空 // 弹窗拽回登录视图,管理员正在输入的验证码会被打断、票据被清空
var _lm = document.getElementById('login-modal'); var _lm = document.getElementById('login-modal');
@@ -209,7 +209,7 @@ function getDeviceId() {
hideForgotView(); hideForgotView();
hideAdmin2faView(); hideAdmin2faView();
} }
// 第 98 条:会话失效弹登录窗前,先关掉可能叠开的注册弹窗(双遮罩错乱), // 会话失效弹登录窗前,先关掉可能叠开的注册弹窗(双遮罩错乱),
// 已 show 则短路——401 风暴(1.5s 轮询)期间不反复重开 // 已 show 则短路——401 风暴(1.5s 轮询)期间不反复重开
var _reg = document.getElementById('register-modal'); var _reg = document.getElementById('register-modal');
if (_reg && _reg.classList.contains('show')) closeModal('register-modal'); if (_reg && _reg.classList.contains('show')) closeModal('register-modal');
@@ -265,14 +265,14 @@ function getDeviceId() {
durationText = d.duration_text || ''; durationText = d.duration_text || '';
renderAuthArea(); renderAuthArea();
renderUserCenter(); renderUserCenter();
// A1:登录成功后立即恢复本设备的进行中任务 // 登录成功后立即恢复本设备的进行中任务
if (user && typeof window.SITE_TASKS_BOOTSTRAP === 'function') { if (user && typeof window.SITE_TASKS_BOOTSTRAP === 'function') {
try { window.SITE_TASKS_BOOTSTRAP(); } catch (e) {} try { window.SITE_TASKS_BOOTSTRAP(); } catch (e) {}
} }
} }
/* ---------------- 登录 / 注册弹窗 ---------------- */ /* ---------------- 登录 / 注册弹窗 ---------------- */
// 第 81 条:注册唯一入口——导航按钮与登录弹窗两处链接(全站仅有的 3 条打开 // 注册唯一入口——导航按钮与登录弹窗两处链接(全站仅有的 3 条打开
// 路径)统一经此:未同意协议 → cookieGate('register') 弹「隐私与协议设置」 // 路径)统一经此:未同意协议 → cookieGate('register') 弹「隐私与协议设置」
// 注册模式(16 秒强制阅读计时)并中止;已同意 → 直接进注册弹窗。 // 注册模式(16 秒强制阅读计时)并中止;已同意 → 直接进注册弹窗。
// 提交侧 doRegister 另有 fail-closed 兜底闸,程序化打开也绕不过。 // 提交侧 doRegister 另有 fail-closed 兜底闸,程序化打开也绕不过。
@@ -294,7 +294,7 @@ function getDeviceId() {
['login-close', 'register-close', 'announce-close'].forEach(function (id) { ['login-close', 'register-close', 'announce-close'].forEach(function (id) {
var b = $('#' + id); var b = $('#' + id);
if (b) b.addEventListener('click', function () { if (b) b.addEventListener('click', function () {
if (id.indexOf('login') === 0 && window.resetAuthViews) window.resetAuthViews(); // 第 87 条:关闭侧复位 if (id.indexOf('login') === 0 && window.resetAuthViews) window.resetAuthViews(); // 关闭侧复位
closeModal(id.indexOf('login') === 0 ? 'login-modal' : id.indexOf('register') === 0 ? 'register-modal' : 'announce-modal'); closeModal(id.indexOf('login') === 0 ? 'login-modal' : id.indexOf('register') === 0 ? 'register-modal' : 'announce-modal');
}); });
}); });
@@ -302,7 +302,7 @@ function getDeviceId() {
var m = $('#' + p[0]); var m = $('#' + p[0]);
if (m) m.addEventListener('click', function (e) { if (m) m.addEventListener('click', function (e) {
if (e.target === m) { if (e.target === m) {
if (p[0] === 'login-modal' && window.resetAuthViews) window.resetAuthViews(); // 第 87 条:关闭侧复位 if (p[0] === 'login-modal' && window.resetAuthViews) window.resetAuthViews(); // 关闭侧复位
closeModal(p[0]); closeModal(p[0]);
} }
}); });
@@ -312,12 +312,12 @@ function getDeviceId() {
if (g2) g2.addEventListener('click', function () { closeModal('login-modal'); openRegisterFlow(); }); if (g2) g2.addEventListener('click', function () { closeModal('login-modal'); openRegisterFlow(); });
var gl1 = $('#goto-login'); var gl1 = $('#goto-login');
if (gl1) gl1.addEventListener('click', function () { closeModal('register-modal'); hideForgotView(); hideAdmin2faView(); openModal('login-modal'); }); if (gl1) gl1.addEventListener('click', function () { closeModal('register-modal'); hideForgotView(); hideAdmin2faView(); openModal('login-modal'); });
// 第 86 条:找回密码入口/返回(F5.1 只交付 DOM 漏了绑定) // 找回密码入口/返回(只交付 DOM 漏了绑定)
var gf = $('#goto-forgot'); var gf = $('#goto-forgot');
if (gf) gf.addEventListener('click', showForgotView); if (gf) gf.addEventListener('click', showForgotView);
var fbl = $('#forgot-back-login'); var fbl = $('#forgot-back-login');
if (fbl) fbl.addEventListener('click', hideForgotView); if (fbl) fbl.addEventListener('click', hideForgotView);
// 第 86 条:管理员两步验证视图的返回链接 // 管理员两步验证视图的返回链接
var a2b = $('#a2fa-back'); var a2b = $('#a2fa-back');
if (a2b) a2b.addEventListener('click', hideAdmin2faView); if (a2b) a2b.addEventListener('click', hideAdmin2faView);
@@ -334,7 +334,7 @@ function getDeviceId() {
api('/auth/login', { username: username, password: password }) api('/auth/login', { username: username, password: password })
.then(function (data) { .then(function (data) {
if (data.status !== 200) throw new Error(data.message || '登录失败'); if (data.status !== 200) throw new Error(data.message || '登录失败');
// 第 86 条:管理员密码验证通过——转两步验证视图(服务端未建会话) // 管理员密码验证通过——转两步验证视图(服务端未建会话)
if (data.data && data.data.admin_2fa) { if (data.data && data.data.admin_2fa) {
showAdmin2faView(data.data.ticket, data.data.email_masked); showAdmin2faView(data.data.ticket, data.data.email_masked);
toast('密码验证通过,请完成邮箱验证', 'success'); toast('密码验证通过,请完成邮箱验证', 'success');
@@ -358,7 +358,7 @@ function getDeviceId() {
/* ---------------- 注册 ---------------- */ /* ---------------- 注册 ---------------- */
var _REG_EMAIL_RE = /^[\w.%+-]+@[\w.-]+\.[A-Za-z]{2,}$/; var _REG_EMAIL_RE = /^[\w.%+-]+@[\w.-]+\.[A-Za-z]{2,}$/;
var _REG_NAME_RE = /^[A-Za-z0-9_\u4e00-\u9fa5]{3,20}$/; var _REG_NAME_RE = /^[A-Za-z0-9_\u4e00-\u9fa5]{3,20}$/;
// F5.1:注册弹窗内常驻错误行(长文案 2.6s toast 看不完) // 注册弹窗内常驻错误行(长文案 2.6s toast 看不完)
function regShowError(msg) { function regShowError(msg) {
toast(msg, 'error'); toast(msg, 'error');
var line = $('#reg-error-line'); var line = $('#reg-error-line');
@@ -391,8 +391,8 @@ function getDeviceId() {
} }
/* ---------------- 找回密码 ---------------- */ /* ---------------- 找回密码 ---------------- */
// 第 84 条:F5.1 只交付了找回视图 DOM 未实现 JS——此处补齐全链路。 // 只交付了找回视图 DOM 未实现 JS——此处补齐全链路。
// 后端 /auth/forgot_password(第 79 条)已就绪:验证码原子消费、防枚举 // 后端 /auth/forgot_password 已就绪:验证码原子消费、防枚举
// 统一文案、login_seq 踢全会话、5 次/分/IP 限流;发码复用 /auth/send_code。 // 统一文案、login_seq 踢全会话、5 次/分/IP 限流;发码复用 /auth/send_code。
function forgotShowError(msg) { function forgotShowError(msg) {
toast(msg, 'error'); toast(msg, 'error');
@@ -470,7 +470,7 @@ function getDeviceId() {
.finally(function () { btn.disabled = false; btn.textContent = '重置密码'; }); .finally(function () { btn.disabled = false; btn.textContent = '重置密码'; });
} }
/* ---------------- 管理员两步验证(第 86 条) ---------------- /* ---------------- 管理员两步验证 ----------------
* Step1 用户名+密码 → 服务端签发票据(不建会话);本视图完成 * Step1 用户名+密码 → 服务端签发票据(不建会话);本视图完成
* Step2 绑定邮箱比对(触发发码)与 Step3 验证码登录。普通用户不经过此流程。 */ * Step2 绑定邮箱比对(触发发码)与 Step3 验证码登录。普通用户不经过此流程。 */
var _a2faTicket = ''; var _a2faTicket = '';
@@ -495,7 +495,7 @@ function getDeviceId() {
_a2faTicket = ticket || ''; _a2faTicket = ticket || '';
var mask = $('#a2fa-email-mask'); var mask = $('#a2fa-email-mask');
if (mask) mask.textContent = emailMask || '-'; if (mask) mask.textContent = emailMask || '-';
// 第 87 条:清掉上次会话残留的输入(错码/旧邮箱不再被原样重复提交) // 清掉上次会话残留的输入(错码/旧邮箱不再被原样重复提交)
var ae = $('#a2fa-email'), ac = $('#a2fa-code'); var ae = $('#a2fa-email'), ac = $('#a2fa-code');
if (ae) ae.value = ''; if (ae) ae.value = '';
if (ac) ac.value = ''; if (ac) ac.value = '';
@@ -515,7 +515,7 @@ function getDeviceId() {
a2faClearError(); a2faClearError();
a2faStopTimer(); a2faStopTimer();
} }
// 第 87 条:关闭侧统一复位——登录弹窗的 X/遮罩/Escape/doLogout 关闭路径都调用, // 关闭侧统一复位——登录弹窗的 X/遮罩/Escape/doLogout 关闭路径都调用,
// 不再只依赖"打开侧纪律";同时供 index.html 的 Escape 通用分支使用 // 不再只依赖"打开侧纪律";同时供 index.html 的 Escape 通用分支使用
window.resetAuthViews = function () { hideForgotView(); hideAdmin2faView(); }; window.resetAuthViews = function () { hideForgotView(); hideAdmin2faView(); };
function doA2faSendCode() { function doA2faSendCode() {
@@ -527,7 +527,7 @@ function getDeviceId() {
api('/auth/admin_login/email', { ticket: _a2faTicket, email: email }) api('/auth/admin_login/email', { ticket: _a2faTicket, email: email })
.then(function (data) { .then(function (data) {
if (data.status === 401) { if (data.status === 401) {
// 第 87 条:先复位视图再提示——错误行随视图隐藏,此处 toast 是唯一可达提示 // 先复位视图再提示——错误行随视图隐藏,此处 toast 是唯一可达提示
hideAdmin2faView(); hideAdmin2faView();
a2faShowError(data.message || '请重新登录'); a2faShowError(data.message || '请重新登录');
return; return;
@@ -558,7 +558,7 @@ function getDeviceId() {
api('/auth/admin_login/verify', { ticket: _a2faTicket, code: code }) api('/auth/admin_login/verify', { ticket: _a2faTicket, code: code })
.then(function (data) { .then(function (data) {
if (data.status === 401) { if (data.status === 401) {
// 第 87 条:先复位视图再提示(同上,错误行随视图隐藏) // 先复位视图再提示(同上,错误行随视图隐藏)
hideAdmin2faView(); hideAdmin2faView();
a2faShowError(data.message || '请重新登录'); a2faShowError(data.message || '请重新登录');
return; return;
@@ -582,7 +582,7 @@ function getDeviceId() {
var email = ($('#reg-email') || {}).value || ''; var email = ($('#reg-email') || {}).value || '';
var code = ($('#reg-code') || {}).value || ''; var code = ($('#reg-code') || {}).value || '';
regClearError(); regClearError();
// 第 81 条:提交侧兜底闸(入口已统一走 openRegisterFlow,此处防任何程序化 // 提交侧兜底闸(入口已统一走 openRegisterFlow,此处防任何程序化
// 打开路径绕过)。fail-closed:协议闸缺失视为未同意。注册语境只查协议同意 // 打开路径绕过)。fail-closed:协议闸缺失视为未同意。注册语境只查协议同意
// (功能性 Cookie 不拦注册,拦解析/下载时另有提示);未同意时闸内已自动 // (功能性 Cookie 不拦注册,拦解析/下载时另有提示);未同意时闸内已自动
// 弹「隐私与协议设置」注册模式并 toast,此处不再重复提示 // 弹「隐私与协议设置」注册模式并 toast,此处不再重复提示
@@ -591,7 +591,7 @@ function getDeviceId() {
return; return;
} }
if (!window.SITE_COOKIE_GATE('register')) return; if (!window.SITE_COOKIE_GATE('register')) return;
// F1.1:前端即时校验(口径与后端一致,省一次网络往返) // 前端即时校验(口径与后端一致,省一次网络往返)
if (!username || !password || !email || !code) { regShowError('请填写完整注册信息'); return; } if (!username || !password || !email || !code) { regShowError('请填写完整注册信息'); return; }
if (!_REG_NAME_RE.test(username)) { regShowError('用户名需 3-20 位,仅支持中文、字母、数字、下划线,且不能纯下划线'); return; } if (!_REG_NAME_RE.test(username)) { regShowError('用户名需 3-20 位,仅支持中文、字母、数字、下划线,且不能纯下划线'); return; }
if (!_REG_EMAIL_RE.test(email.trim())) { regShowError('邮箱格式不正确'); return; } if (!_REG_EMAIL_RE.test(email.trim())) { regShowError('邮箱格式不正确'); return; }
@@ -619,10 +619,10 @@ function getDeviceId() {
api('/auth/logout', {}).then(function () { api('/auth/logout', {}).then(function () {
user = null; durationText = ''; user = null; durationText = '';
renderAuthArea(); renderAuthArea();
renderUserCenter(); // 第 83 条:同步个人中心 gate/content 显隐,与其他登出路径一致 renderUserCenter(); // 同步个人中心 gate/content 显隐,与其他登出路径一致
if (window.resetAuthViews) window.resetAuthViews(); // 第 87 条:登出复位登录弹窗视图 if (window.resetAuthViews) window.resetAuthViews(); // 登出复位登录弹窗视图
closeModal('register-modal'); closeModal('login-modal'); closeModal('register-modal'); closeModal('login-modal');
// 第 102 条(U15 拍板):退出连带清除个人网易云登录(index.html 注册的钩子: // 退出连带清除个人网易云登录(index.html 注册的钩子:
// 清 localStorage 的个人 Cookie/解析账号选择并复位账号栏) // 清 localStorage 的个人 Cookie/解析账号选择并复位账号栏)
if (typeof window.SITE_ON_LOGOUT === 'function') { if (typeof window.SITE_ON_LOGOUT === 'function') {
try { window.SITE_ON_LOGOUT(); } catch (e) {} try { window.SITE_ON_LOGOUT(); } catch (e) {}
@@ -680,7 +680,7 @@ function getDeviceId() {
.then(function (data) { .then(function (data) {
if (data.status !== 200) throw new Error(data.message || '兑换失败'); if (data.status !== 200) throw new Error(data.message || '兑换失败');
durationText = (data.data && data.data.duration_text) || durationText; durationText = (data.data && data.data.duration_text) || durationText;
// F69:同步更新内存余额(批量下载预检读的是 user.duration_seconds, // 同步更新内存余额(批量下载预检读的是 user.duration_seconds,
// 原实现只改显示文字,预检仍用充值前的旧值) // 原实现只改显示文字,预检仍用充值前的旧值)
if (user && data.data && typeof data.data.duration_seconds === 'number') { if (user && data.data && typeof data.data.duration_seconds === 'number') {
user.duration_seconds = data.data.duration_seconds; user.duration_seconds = data.data.duration_seconds;
@@ -700,7 +700,7 @@ function getDeviceId() {
/* ---------------- 公告 ---------------- */ /* ---------------- 公告 ---------------- */
function maybeShowAnnouncement(force) { function maybeShowAnnouncement(force) {
// F81:手动"查看公告"(force=true)无条件展示; // 手动"查看公告"(force=true)无条件展示;
// 自动弹出仍按 popup_mode 与 once 已读版本判断 // 自动弹出仍按 popup_mode 与 once 已读版本判断
if (!force && (!ann || !ann.content || ann.popup_mode === 'off')) return; if (!force && (!ann || !ann.content || ann.popup_mode === 'off')) return;
if (!force && ann && ann.popup_mode === 'once') { if (!force && ann && ann.popup_mode === 'once') {
@@ -717,7 +717,7 @@ function getDeviceId() {
var t = $('#announce-title'), c = $('#announce-content'); var t = $('#announce-title'), c = $('#announce-content');
if (t) t.textContent = ann.title || '系统公告'; if (t) t.textContent = ann.title || '系统公告';
if (c) c.innerHTML = String(ann.content).split('\n').map(function (l) { return escapeHtml(l); }).join('<br>'); if (c) c.innerHTML = String(ann.content).split('\n').map(function (l) { return escapeHtml(l); }).join('<br>');
// 第 98 条:首访 Cookie 协议窗显示中不叠加公告弹窗(双遮罩错乱); // 首访 Cookie 协议窗显示中不叠加公告弹窗(双遮罩错乱);
// 跳过时不记已读,下次进入页面再弹 // 跳过时不记已读,下次进入页面再弹
var _cm = document.getElementById('cookie-modal'); var _cm = document.getElementById('cookie-modal');
if (_cm && _cm.classList.contains('show')) return; if (_cm && _cm.classList.contains('show')) return;
@@ -747,7 +747,7 @@ function getDeviceId() {
function uaBindSide() { function uaBindSide() {
$$('#admin-side .admin-side-item[data-view]').forEach(function (item) { $$('#admin-side .admin-side-item[data-view]').forEach(function (item) {
// 第 97 条(P3):侧栏项补键盘语义(F109 同型——tabindex/role + Enter/空格激活) // 侧栏项补键盘语义(同型——tabindex/role + Enter/空格激活)
item.setAttribute('tabindex', '0'); item.setAttribute('tabindex', '0');
item.setAttribute('role', 'button'); item.setAttribute('role', 'button');
item.addEventListener('click', function () { item.addEventListener('click', function () {
@@ -767,7 +767,7 @@ function getDeviceId() {
}); });
var lo = $('#ua-logout'); var lo = $('#ua-logout');
if (lo) lo.addEventListener('click', doLogout); if (lo) lo.addEventListener('click', doLogout);
// 第 97 条(P3):返回/退出两个无 data-view 的侧栏项同样补键盘语义 // 返回/退出两个无 data-view 的侧栏项同样补键盘语义
[back, lo].forEach(function (el) { [back, lo].forEach(function (el) {
if (!el) return; if (!el) return;
el.setAttribute('tabindex', '0'); el.setAttribute('tabindex', '0');
@@ -776,7 +776,7 @@ function getDeviceId() {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); el.click(); } if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); el.click(); }
}); });
}); });
/* ---- 播放 API 密钥(第 78 条) ---- */ /* ---- 播放 API 密钥 ---- */
var pkShow = $('#ua-playkey-show'); var pkShow = $('#ua-playkey-show');
var pkInput = $('#ua-playkey-value'); var pkInput = $('#ua-playkey-value');
function pkCopyToClipboard(text) { function pkCopyToClipboard(text) {
@@ -891,12 +891,12 @@ function getDeviceId() {
var isAdmin = u.role === 'admin'; var isAdmin = u.role === 'admin';
var disabled = (u.status === 0); var disabled = (u.status === 0);
var banned = (u.status === 2); var banned = (u.status === 2);
// 第 97 条(P3):数值字段同过转义(纵深防御——后端口径若变为字符串 // 数值字段同过转义(纵深防御——后端口径若变为字符串
// 不再形成管理后台存储型 XSS 面,与同表 username/email 口径对齐) // 不再形成管理后台存储型 XSS 面,与同表 username/email 口径对齐)
var uid = escapeHtml(String(u.id)); var uid = escapeHtml(String(u.id));
var dur = escapeHtml(String(u.duration_seconds)); var dur = escapeHtml(String(u.duration_seconds));
var cred = u.duration_seconds < 0 ? '不限' : (dur + ' 次'); var cred = u.duration_seconds < 0 ? '不限' : (dur + ' 次');
var dev = [u.register_ip, u.trial_device].filter(Boolean).join(' · ') || '-'; // R5-79:IP 优先显示 var dev = [u.register_ip, u.trial_device].filter(Boolean).join(' · ') || '-'; // IP 优先显示
if (u.last_province) dev += ' · ' + u.last_province; if (u.last_province) dev += ' · ' + u.last_province;
return '<tr' + ((disabled || banned) ? ' style="opacity:.55;"' : '') + '>' + return '<tr' + ((disabled || banned) ? ' style="opacity:.55;"' : '') + '>' +
'<td>' + uid + '</td>' + '<td>' + uid + '</td>' +
@@ -925,7 +925,7 @@ function getDeviceId() {
.then(function (d) { toast(d.message || (d.status === 200 ? '已修改' : '修改失败'), d.status === 200 ? 'success' : 'error'); if (d.status === 200) uaLoadUsers(); }); .then(function (d) { toast(d.message || (d.status === 200 ? '已修改' : '修改失败'), d.status === 200 ? 'success' : 'error'); if (d.status === 200) uaLoadUsers(); });
}); });
}); });
// T3:管理员自助改密(需验证旧密码;改完其他设备全部下线) // 管理员自助改密(需验证旧密码;改完其他设备全部下线)
$$('[data-uself]', body).forEach(function (b) { $$('[data-uself]', body).forEach(function (b) {
b.addEventListener('click', function () { b.addEventListener('click', function () {
var oldpwd = prompt('请输入当前密码:', ''); var oldpwd = prompt('请输入当前密码:', '');
@@ -990,7 +990,7 @@ function getDeviceId() {
} }
/* ---- 全局 Cookie ---- */ /* ---- 全局 Cookie ---- */
var uaQrTimer = null, uaUnikey = '', uaQrGen = 0; // T2:轮询代次令牌(删除 Cookie 后作废旧轮询) var uaQrTimer = null, uaUnikey = '', uaQrGen = 0; // 轮询代次令牌(删除 Cookie 后作废旧轮询)
function uaLoadCookieStatus() { function uaLoadCookieStatus() {
api('/admin/cookie/status', {}).then(function (data) { api('/admin/cookie/status', {}).then(function (data) {
var el = $('#ua-cookie-status'); var el = $('#ua-cookie-status');
@@ -1013,13 +1013,13 @@ function getDeviceId() {
} }
if (tip) tip.textContent = '请使用网易云音乐手机 APP 扫码'; if (tip) tip.textContent = '请使用网易云音乐手机 APP 扫码';
if (uaQrTimer) clearInterval(uaQrTimer); if (uaQrTimer) clearInterval(uaQrTimer);
uaQrGen++; // T2:新轮询代次 uaQrGen++; // 新轮询代次
uaQrTimer = setInterval(uaQrPoll, 3000); uaQrTimer = setInterval(uaQrPoll, 3000);
}); });
} }
function uaQrPoll() { function uaQrPoll() {
if (!uaUnikey) { if (uaQrTimer) clearInterval(uaQrTimer); return; } if (!uaUnikey) { if (uaQrTimer) clearInterval(uaQrTimer); return; }
var myGen = uaQrGen; // T2:本次轮询所属代次 var myGen = uaQrGen; // 本次轮询所属代次
api('/admin/qr/check', { unikey: uaUnikey }).then(function (data) { api('/admin/qr/check', { unikey: uaUnikey }).then(function (data) {
if (myGen !== uaQrGen) return; // 期间已删除 Cookie/重开扫码,丢弃迟到响应 if (myGen !== uaQrGen) return; // 期间已删除 Cookie/重开扫码,丢弃迟到响应
var tip = $('#ua-qr-tip'); var tip = $('#ua-qr-tip');
@@ -1045,7 +1045,7 @@ function getDeviceId() {
function uaCookieDelete() { function uaCookieDelete() {
if (!window.confirm('确定删除全局 Cookie?删除后需重新扫码登录。')) return; if (!window.confirm('确定删除全局 Cookie?删除后需重新扫码登录。')) return;
api('/admin/cookie/delete', {}).then(function (data) { api('/admin/cookie/delete', {}).then(function (data) {
// T2:作废在途扫码轮询——迟到的 803 成功响应不得把刚删除的 Cookie 写回 // 作废在途扫码轮询——迟到的 803 成功响应不得把刚删除的 Cookie 写回
if (uaQrTimer) clearInterval(uaQrTimer); if (uaQrTimer) clearInterval(uaQrTimer);
uaUnikey = ''; uaUnikey = '';
uaQrGen++; uaQrGen++;
@@ -1099,7 +1099,7 @@ function getDeviceId() {
var total = (data.data && data.data.total) || 0; var total = (data.data && data.data.total) || 0;
var pages = (data.data && data.data.pages) || 1; var pages = (data.data && data.data.pages) || 1;
uaLogPage = (data.data && data.data.page) || 1; uaLogPage = (data.data && data.data.page) || 1;
window._uaLogPages = pages; // P3-43:next 按钮按真实页数封顶 window._uaLogPages = pages; // next 按钮按真实页数封顶
var el; var el;
if ((el = $('#ua-log-page'))) el.textContent = uaLogPage; if ((el = $('#ua-log-page'))) el.textContent = uaLogPage;
if ((el = $('#ua-log-pages'))) el.textContent = pages; if ((el = $('#ua-log-pages'))) el.textContent = pages;
@@ -1147,7 +1147,7 @@ function getDeviceId() {
uaLoadCards(0); uaLoadCards(0);
}); });
} }
// 第 81 条:卡密管理操作列(禁用/启用/删除)+ 分页(F85 后端已支持, // 卡密管理操作列(禁用/启用/删除)+ 分页(后端已支持,
// 不分页则超出最新 100 张的旧卡密在界面上永远管理不到) // 不分页则超出最新 100 张的旧卡密在界面上永远管理不到)
var _uaCardPage = 0; var _uaCardPage = 0;
var _uaCardTotal = 0; var _uaCardTotal = 0;
@@ -1312,7 +1312,7 @@ function getDeviceId() {
uaLoadDb(); uaLoadDb();
}) })
.catch(function (err) { .catch(function (err) {
// 第 97 条(P2):全站唯一绕过统一请求封装的调用此前无失败兜底—— // 全站唯一绕过统一请求封装的调用此前无失败兜底——
// 网络失败/非 JSON 响应时管理员毫无感知还制造 unhandledrejection 噪音 // 网络失败/非 JSON 响应时管理员毫无感知还制造 unhandledrejection 噪音
toast((err && err.message) || '备份上传失败,请检查网络后重试', 'error'); toast((err && err.message) || '备份上传失败,请检查网络后重试', 'error');
}); });
@@ -1323,7 +1323,7 @@ function getDeviceId() {
bindAuthModal(); bindAuthModal();
var s1 = $('#login-submit'); if (s1) s1.addEventListener('click', doLogin); 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 s2 = $('#login-password'); if (s2) s2.addEventListener('keydown', function (e) { if (e.key === 'Enter') doLogin(); });
// F-3.1:回车提交覆盖全部输入框(登录用户名框 / 注册四框) // 回车提交覆盖全部输入框(登录用户名框 / 注册四框)
var s2u = $('#login-username'); if (s2u) s2u.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 s3 = $('#reg-submit'); if (s3) s3.addEventListener('click', doRegister);
var s4 = $('#reg-send-code'); if (s4) s4.addEventListener('click', sendCode); var s4 = $('#reg-send-code'); if (s4) s4.addEventListener('click', sendCode);
@@ -1331,7 +1331,7 @@ function getDeviceId() {
var el = $(sel); var el = $(sel);
if (el) el.addEventListener('keydown', function (e) { if (e.key === 'Enter') doRegister(); }); if (el) el.addEventListener('keydown', function (e) { if (e.key === 'Enter') doRegister(); });
}); });
// F-4.1:密码明文切换(登录/注册/找回) // 密码明文切换(登录/注册/找回)
[['#login-pwd-eye', '#login-password'], ['#reg-pwd-eye', '#reg-password'], ['#forgot-pwd-eye', '#forgot-password']].forEach(function (pair) { [['#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]); var eye = $(pair[0]), input = $(pair[1]);
if (!eye || !input) return; if (!eye || !input) return;
@@ -1341,7 +1341,7 @@ function getDeviceId() {
eye.textContent = show ? '隐藏' : '显示'; eye.textContent = show ? '隐藏' : '显示';
}); });
}); });
// F-2.1:发码后修改邮箱 → 提前终止倒计时并恢复按钮,提示需重发 // 发码后修改邮箱 → 提前终止倒计时并恢复按钮,提示需重发
var regEmail = $('#reg-email'); var regEmail = $('#reg-email');
if (regEmail) regEmail.addEventListener('input', function () { if (regEmail) regEmail.addEventListener('input', function () {
var btn = $('#reg-send-code'); var btn = $('#reg-send-code');
@@ -1351,7 +1351,7 @@ function getDeviceId() {
toast('邮箱已修改,请重新发送验证码', 'info'); toast('邮箱已修改,请重新发送验证码', 'info');
} }
}); });
// 第 84 条:找回视图——发码/提交/回车/改邮箱终止倒计时(对齐注册交互) // 找回视图——发码/提交/回车/改邮箱终止倒计时(对齐注册交互)
var fsc = $('#forgot-send-code'); if (fsc) fsc.addEventListener('click', doForgotSendCode); var fsc = $('#forgot-send-code'); if (fsc) fsc.addEventListener('click', doForgotSendCode);
var fsub = $('#forgot-submit'); if (fsub) fsub.addEventListener('click', doForgotReset); var fsub = $('#forgot-submit'); if (fsub) fsub.addEventListener('click', doForgotReset);
['#forgot-email', '#forgot-code', '#forgot-password'].forEach(function (sel) { ['#forgot-email', '#forgot-code', '#forgot-password'].forEach(function (sel) {
@@ -1367,7 +1367,7 @@ function getDeviceId() {
toast('邮箱已修改,请重新发送验证码', 'info'); toast('邮箱已修改,请重新发送验证码', 'info');
} }
}); });
// 第 86 条:管理员两步验证——发码/验证提交/回车提交 // 管理员两步验证——发码/验证提交/回车提交
var asc = $('#a2fa-send'); if (asc) asc.addEventListener('click', doA2faSendCode); var asc = $('#a2fa-send'); if (asc) asc.addEventListener('click', doA2faSendCode);
var asb = $('#a2fa-submit'); if (asb) asb.addEventListener('click', doA2faVerify); var asb = $('#a2fa-submit'); if (asb) asb.addEventListener('click', doA2faVerify);
['#a2fa-email', '#a2fa-code'].forEach(function (sel) { ['#a2fa-email', '#a2fa-code'].forEach(function (sel) {
@@ -1386,7 +1386,7 @@ function getDeviceId() {
else { var t = $('#tab-usercenter'); if (t) t.click(); } else { var t = $('#tab-usercenter'); if (t) t.click(); }
}); });
var ucRedeem = $('#uc-card-redeem'); if (ucRedeem) ucRedeem.addEventListener('click', doRedeem); var ucRedeem = $('#uc-card-redeem'); if (ucRedeem) ucRedeem.addEventListener('click', doRedeem);
var ucAnn = $('#uc-ann-view'); if (ucAnn) ucAnn.addEventListener('click', function () { maybeShowAnnouncement(true); }); // F81:手动查看强制展示 var ucAnn = $('#uc-ann-view'); if (ucAnn) ucAnn.addEventListener('click', function () { maybeShowAnnouncement(true); }); // 手动查看强制展示
var ucLogout = $('#uc-logout'); if (ucLogout) ucLogout.addEventListener('click', doLogout); var ucLogout = $('#uc-logout'); if (ucLogout) ucLogout.addEventListener('click', doLogout);
uaBindSide(); uaBindSide();
@@ -1400,7 +1400,7 @@ function getDeviceId() {
var b8 = $('#ua-log-tab-ok'); if (b8) b8.addEventListener('click', function () { uaLogsType = 'success'; uaLogPage = 1; 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 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(); } }); var blogPrev = $('#ua-log-prev'); if (blogPrev) blogPrev.addEventListener('click', function () { if (uaLogPage > 1) { uaLogPage--; uaLoadLogs(); } });
// P3-43:next 补边界(原实现可翻到空页——前端无 pages 时放宽为 999 防呆) // 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 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 blogClear = $('#ua-log-clear'); if (blogClear) blogClear.addEventListener('click', uaClearLogs);
var b10 = $('#ua-card-gen'); if (b10) b10.addEventListener('click', uaGenCards); var b10 = $('#ua-card-gen'); if (b10) b10.addEventListener('click', uaGenCards);
+164 -164
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -534,7 +534,7 @@
"payload": null, "payload": null,
"status": 200, "status": 200,
"body": null, "body": null,
"html_len": 466261, "html_len": 464333,
"html_sha256": "5ba897c2f07b43b6c34e998505fea385cb6a6c8bfd5115d014f80044a38c6200" "html_sha256": "0596d87ccbe8e7b5a19494c5d1ab1fd11bc95b82665ddc600ba90e4f2f790b06"
} }
] ]
+4
View File
@@ -8,6 +8,10 @@ tests/golden/responses.json 是**删除 Flask 之前**从 Flask 应用捕获的
说明: 说明:
- 会话 Cookie 在运行时用当前库的 login_seq 动态构造(快照不存 Cookie)。 - 会话 Cookie 在运行时用当前库的 login_seq 动态构造(快照不存 Cookie)。
- 请求为只读/确定性调用,不含实时上游数据(那些由 parity/单测覆盖)。 - 请求为只读/确定性调用,不含实时上游数据(那些由 parity/单测覆盖)。
- `index` 条目的 html_len/html_sha256 记的是**当前模板**渲染结果,不是 Flask 期数值
(该条目 body 为 null)。有意改模板后必须同步刷新这两项:
服务端渲染 = templates/index.html 去掉末尾换行,故
html_len = 文件字节数 - 1,html_sha256 = sha256(文件去掉末尾换行)。
""" """
import hashlib import hashlib
import json import json