Files
Xiao-no-love f0fe50e484 feat: StarLight 第三方登录 + 安全加固 + 循环池 + 热配置
功能:
- StarLight Yggdrasil 第三方登录(后端中转,accessToken 不落地)
- 管理员可视化权限管理(用户列表 + 一键授予/移出)
- 设置页热配置(11 项可编辑,立即生效)
- 循环池(每流独立手动歌单,队列空时随机不重复抽取)
- 登录页统一主站浅色风格
- 全站改名「Starlight专属电台」
- 删除关于页

安全:
- 全局 CSRF 防护(双提交 Cookie)
- 登录限流防 XFF 伪造、错误信息脱敏
- 权限中间件 requireAdminForWrites(读放行/写 admin)
- 密码哈希存储(scrypt/sha256)
- SSRF 防护(封面下载域名白名单)
- 空 catch 加日志、SSE 连接上限

修复:
- 多角色账号 selectedProfile 缺失
- 普通用户越权删歌(V4)
- streams 写操作越权(V9)
- 设置页布局溢出

测试:38 项全绿(api16 / e2e17 / write5)
2026-10-06 16:37:03 +08:00

26 KiB
Raw Permalink Blame History

交接文档 · Starlight专属电台

跨会话交接。新会话先读此文件再动手。最后更新:2026-09-25(v2.6)。


0. TL;DR

  • 项目:F:\music_online\demo — Node.js + FFmpeg 的 HLS 个人音乐电台
  • 状态:v2.6,功能完整。回归测试 api 16 / e2e 17 / write 5 全绿
  • 服务:svc003 运行中,http://127.0.0.1:3000
  • 密码:管理员 test123 / 用户 user123(start.cmd 里设的环境变量,测试用)
  • Git:a6a01b7(v2.5) → 2fc2548(v2.6 最新)。工作区干净。

1. 运行 / 环境

  • Windows 11,Node v24.5.0,FFmpeg 9.0.1(F:\ffmpge\ffmpeg-9.0.1-essentials_build\bin,含 ffprobe),Python 3.11
  • 启动(推荐托管):
    cmd /c "set ADMIN_PASSWORD=test123&& set USER_PASSWORD=user123&& node index.js"
    
    或双击 start.cmd(已配好双密码)
  • 登录:浏览器开 http://127.0.0.1:3000
  • 依赖:better-sqlite3、multer、chokidar、express(已装)

2. 目录结构

demo/
├── index.js              # 入口,仅 require("./src/server").start()
├── start.cmd             # 启动脚本(含 ADMIN_PASSWORD/USER_PASSWORD)
├── data.db               # SQLite(WAL,自动生成)
├── HANDOFF.md / README.md
├── src/
│   ├── server.js         # 装配:中间件/路由/启动/优雅退出
│   ├── config.js         # 全部配置(环境变量解析)
│   ├── logger.js         # 分级日志 + 落盘 + 内存环形缓冲(tail)
│   ├── db.js             # SQLite init + schema(5表) + WAL + 运行时迁移
│   ├── store.js          # 数据访问层,所有 SQL 收敛于此
│   ├── util.js           # fileHash/文件名解析/原子写/目录大小
│   ├── media.js          # ffprobe + FFmpeg 切片 + 并发队列 + 进程追踪
│   ├── metadata.js       # 网易云 API + 封面下载 + 元数据补全
│   ├── livestream.js     # LiveStream 类 + 多路流 + 缓存状态/清理
│   ├── scanner.js        # audio/ 监听 + 同步 DB
│   ├── auth.js           # 双密码鉴权 + 角色 + 会话 + 登录锁定
│   ├── sse.js            # SSE 广播
│   └── routes/           # auth / music / resources / streams / system
├── public/
│   ├── index.html        # 主面板(6 tab)
│   ├── login.html        # 登录页(独立暗色主题)
│   ├── css/style.css     # token 体系(v2.4 重构)
│   ├── js/app.js         # 前端逻辑
│   ├── vendor/           # 本地化:bootstrap/icons/sortable/hls
│   └── covers/           # 本地封面
├── audio/ hls/ hls_cache/  # 音频 / 直播输出 / 切片缓存(均 gitignore)
├── scripts/fetch_vendor.py
└── test_api.py / test_e2e.py / test_write.py / test_enrich.py

3. 数据模型(SQLite, src/db.js)

songs(id, file UNIQUE, name, artist, cover, cover_remote, duration,
      file_size, heat, cache_status, created_at)
  -- file 存「文件名」非绝对路径;cover 本地路径;cover_remote 远程 URL(优先展示)
streams(id PK, name, enabled, current_pos, media_seq, created_at)
  -- 默认流 id='main',不可删
stream_playlist(id PK, stream_id, song_id, position)  -- 外键 CASCADE
settings(key PK, value)
sessions(token PK, role, created_at, expires_at)  -- role: admin|user
  • 切片缓存 hls_cache/{md5(文件名)}/ 与流无关,多路共享
  • 运行时迁移:init() 用 PRAGMA table_info 检测缺列后 ALTER(已支持 sessions.role、songs.cover_remote)

4. API 清单

除 /api/auth/*、/hls、/hls_cache、静态资源、/login 外全部需登录。 🔒 = 需管理员。

方法 路径 说明
POST /api/auth/login {password} → {ok,role};失败过多返回 429
POST /api/auth/logout · GET /api/auth/me 登出 / 返回 {authed,role}
GET /api/resources 资源列表(含 albumImage/coverLocal/cacheSize)
GET /api/music/search?keyword= 网易云搜索(后端缓存+限流)
POST /api/music/detail · /download 详情 / 下载入库
POST /api/music/import 🔒 multipart:audio+title+artist
POST /api/music/delete · /delete-batch 级联删
POST /api/music/enrich {file,skipMatch?} 重匹配元数据
POST /api/music/refresh-covers 🔒 一键刷新所有缺失封面
GET/POST/DELETE /api/streams[/:id] · /start|stop 流管理
GET/POST /api/streams/:id/playlist[/add|remove|reorder|clear|next|prev|jump] 播放列表
GET /api/system/cache 🔒 · POST /cache/cleanup 🔒 缓存
GET /api/system/logs?tail= 🔒 · /config 🔒 · /settings 🔒 系统
GET /api/events SSE(cookie 鉴权)

统一返回 {ok:true,...} / {ok:false,error:"..."}。


5. 直播流地址

  • 默认 /hls/main/index.m3u8;其它 /hls/{streamId}/index.m3u8
  • 切片 /hls_cache/{hash}/indexN.ts

6. 关键设计决策(勿轻易推翻)

  1. SQLite better-sqlite3 + WAL:单进程同步 API 最省事,WAL 解读写互斥
  2. 不迁 v1 JSON 数据(旧 heat/metadata/playlist.json 已删)
  3. 鉴权整站 Cookie 会话;HLS 流不锁(播放器难带 cookie)
  4. 封面/资源全本地化:public/covers/、public/vendor/;系统字体
  5. songs.file 存文件名非绝对路径,可移植
  6. 导入先落 tmp 再 rename,避免 chokidar 扫到半截
  7. 双密码角色:用户 = 管理员 − 上传 − 设置页;后端 requireAdmin 硬挡,非只藏前端

7. 测试方法

服务起来后:

cd /d F:\music_online\demo
set PYTHONIOENCODING=utf-8
python -X utf8 test_api.py     # 16 项
python -X utf8 test_e2e.py     # 17 项
python -X utf8 test_write.py   # 5 项(需 tmp/test_import.mp3 素材)

素材生成:

"F:\ffmpge\ffmpeg-9.0.1-essentials_build\bin\ffmpeg.exe" -y -f lavfi -i anullsrc=r=44100:cl=stereo -t 3 -q:a 9 tmp\test_import.mp3

⚠️ 测试只覆盖 HTTP 层,浏览器 JS 交互未自动化。


8. 已知问题 / 待实测

  • 浏览器端未实测:SSE 实时刷新、拖拽 reorder、mini player、上传进度、频道弹窗交互
  • metadata.js enrichSong 启动重匹配慢歌 → 可接受
  • 多路流并发预生成排队 → 观察中
  • app.js esc() 用 String.fromCharCode(38) 绕过工具转义,勿优化
  • 登录失败锁定 failMap 是内存态,重启清零(可接受)

9. 工具层踩坑记录

  1. XML 实体转义吃 & ":写 JS/CSS 用 String.fromCharCode(38) 或字符码构造
  2. ⚠️ powershell 行号偏移:Select-String/Get-Content 行号比 replace_lines 实际大 ~2,照它改必错位!用 node 查行号:
    fs.readFileSync(f,"utf8").split("\n").forEach((l,i)=>{if(l.includes(p))console.log((i+1)+": "+l)})
    
  3. replace_lines 多段连锁崩:同一文件多段改,某段行号错会留残留。逐条改,改前 node 查号、改后 node --check
  4. 文件被改乱:别手修,git checkout <commit> -- <file> 恢复
  5. 后台服务用 action background="true"(勿 start /b)
  6. service_stop 连子进程一起杀(含 node 的 ffmpeg 子进程)
  7. 改完源码要重启 node 才生效
  8. Windows 无 head/tail,用 findstr 或 node
  9. cmd 下 curl:%{http_code} 要写 %%{http_code};中文输出 GBK 乱码(功能正常)
  10. git 提交中文:powershell 写无 BOM UTF-8 文件 + git -c i18n.commitEncoding=utf-8 commit -F 文件
  11. python 输出:chcp 65001 + set PYTHONIOENCODING=utf-8;脚本末尾 sys.stdout.flush() 再 os._exit(0)

10. 版本时间线

版本 日期 要点
v2 09-23 SQLite+多路流+鉴权+导入/删除+资源本地化+模块化重构
v2.1 09-23 频道管理模态框、死代码清理、forgetSong 内存泄漏修复
v2.2 09-24 站内 HLS 收听页、设置页、上传进度、正在播放实时化
v2.2.1 09-24 资源排序(6 种)
v2.3 09-24 UI 去 AI 味:清全部渐变/glow,单强调色(依 ibelick/ui-skills)
v2.4 09-24 UI token 化:字号 42→9、圆角 20→8、硬编码色清零
v2.5 09-25 双密码+角色权限、登录锁定、搜索缓存限流、封面远程优先
v2.6 09-25 设置页一键刷新缺失封面

各版本详细变更见 git log(git log --oneline)。


11. 下一步可选

  1. 浏览器手动过一遍所有交互,修 Console 报错 ← 最优先
  2. 播放延迟调节(HLS_TIME 热更新,老板曾暂缓)
  3. 密码设置 UI(现走环境变量)
  4. 缓存生成进度条、搜索分页/加载更多
  5. mini player 音量控制、播放历史/最热排序(heat 字段已有)
  6. 正式化:固定强密码、服务化启动

12. 当前运行态

  • 后台服务:svc003(http://127.0.0.1:3000)
  • DB:data.db(6 首歌曲,封面已本地化)
  • 缓存:hls_cache/ 约 34MB
  • Git:2fc2548 干净

13. StarLight 第三方登录接入(v2.7)

背景:管理员要求用 StarLight(skin.starlight.cool/yggdrasil)做第三方登录,替换原密码登录(保留应急后门)。

方案:后端中转(浏览器→本服务→Yggdrasil),accessToken 不落地、不下发。

改动文件:

  • src/starlight.js(新)— Yggdrasil 客户端,仅 authenticate();CLIENT_TOKEN 启动随机
  • src/config.js — 新增 STARLIGHT_API、STARLIGHT_ADMINS(逗号分隔,小写匹配)
  • src/db.js — sessions 表迁移加 starlight_name 列
  • src/store.js — createSession(token, role, expiresAt, starlightName)
  • src/auth.js — loginViaStarLight()、getStarlightName();exports 更新
  • src/routes/auth.js — /login 双模式:有 username→StarLight;无→应急密码
  • public/login.html — 用户名+密码表单 + "管理员应急登录"折叠区
  • public/index.html — 顶栏 #userName 显示"你好,xxx"
  • public/js/app.js — applyRoleUI(role, starlightName)
  • start.cmd — 加 STARLIGHT_API / STARLIGHT_ADMINS 配置项

登录逻辑:

  • 前端 {username, password} → 后端调 authenticate → 成功建本地会话
  • 角色:STARLIGHT_ADMINS 命中=admin,否则 user
  • 应急后门:username 留空 + {password} → 走 ADMIN_PASSWORD(防皮肤站挂掉锁死)

测试:api 16 / e2e 17 / write 5 全绿。真实打到 StarLight(错误凭证→401)。

注意:test_write.py 第 30 行写死旧路径 F:\music_online\demo\tmp\,复制项目时未改(跑测试需先把素材放到该路径)。

14. 安全加固(v2.7.1,2026-10-05)

对上一条 StarLight 登录做了自审+修复,详见 SECURITY_REVIEW.md。

修复清单:

  • 🔴 XFF 伪造绕过限流 → trust proxy=false + getClientIp() 取 socket IP
  • 🔴 白名单按用户名判定可抢注提权 → 改绑 UUID
  • 🟠 错误信息泄露 StarLight 内部细节 → 脱敏
  • 🟠 用户名字符集未校验 → 正则过滤 [A-Za-z0-9_]
  • 🟠 日志记 PII(邮箱)→ 移除
  • 🟠 无 body 大小限制 → express.json({limit:"64kb"})
  • 🟡 无滑动续期 → getValidSession 加续期 + store.touchSession()

验证:38 项回归全绿 + XFF 伪造测试(6 个不同 XFF 仍触发 429)+ 真实账号登录正常。

待办:

  • STARLIGHT_ADMINS 填 UUID(不是用户名!)
  • 生产环境用强密码 + HTTPS(Cookie 加 Secure)
  • 登录锁定无解锁途径(重启服务可清 failMap)

15. 全局 CSRF 防护(v2.7.2,2026-10-05)

背景:管理员要求加全局 CSRF(选项 C)。

方案:双提交 Cookie(Double Submit Cookie)

  • 登录时发 mr_csrf cookie(非 HttpOnly,JS 可读)+ session cookie
  • 前端所有写请求带 X-CSRF-Token 头(值 = cookie)
  • 后端 csrfProtection 中间件校验(GET/HEAD/OPTIONS 豁免)

改动文件:

  • src/auth.js — CSRF_COOKIE / genCsrfToken / setCsrfCookie / clearCsrfCookie / csrfProtection;setSessionCookie 改用 res.append(与 csrf cookie 共存)
  • src/routes/auth.js — 登录成功发 csrf cookie;登出清 csrf
  • src/server.js — 受保护 API 挂 auth.csrfProtection
  • public/js/app.js — api() 统一给写请求加 X-CSRF-Token
  • test_api.py / test_e2e.py / test_write.py — call() 加 CSRF 头(从 cookie jar 读);test_write 的 multipart import 也加

验证:38 项全绿(api16/e2e17/write5)+ CSRF 专项(无头403 / 带头200 / GET豁免)。

⚠️ 代价:所有 API 客户端(脚本、第三方集成)写操作必须带 X-CSRF-Token 头,否则 403。

16. 渗透测试修复(v2.7.3,2026-10-05)

依据 SECURITY_PENTEST.md(黑盒渗透报告)修复 3 个漏洞:

  • V1 硬编码密码 → start.cmd 改存 sha256 哈希;auth.js 加 passwordMatches() 支持明文/hash 两种。⚠️ git 历史里的明文未清(老板决定不动云端),test123 需轮换。
  • V2 堆栈泄露 → server.js 加全局错误处理中间件,客户端只收通用文案。
  • V3 限流 DoS → ①类型校验前置不计失败 ②新增 TRUSTED_IPS 白名单豁免 ③isTrustedIp 归一化 IPv4-mapped IPv6。

新配置:TRUSTED_IPS(默认 127.0.0.1,::1)。 测试:38 项全绿 + V1/V2/V3 专项验证。 遗留:轮换 test123、生产 TRUSTED_IPS 收紧、HTTPS。

17. 第三/四轮渗透修复(v2.7.4,2026-10-05)

依据 SECURITY_PENTEST_R2.md(复测)+ SECURITY_PENTEST_R3_WHITEBOX.md(白盒):

  • V4【高危·水平越权】:普通用户可删歌/批量删/改元数据 → routes/music.js 给 delete/delete-batch/enrich 补 requireAdmin。已动态验证 403。
  • V5【低】:settings 无 schema → 字段白名单。
  • V6【低】:/hls_cache 未鉴权 → 评估后不改(切片路径被 m3u8 引用,加锁断播放;低危接受)。
  • V8【低】:updateSong 动态列名 → 字段白名单。
  • V1:passwordMatches 新增 scrypt 支持(scrypt:<salt>:<hash>),兼容 sha256/明文。start.cmd 仍是 sha256,可升级。

测试脚本:改从 TEST_PASSWORD 环境变量读密码(默认 123test)。 回归:38 项全绿。 遗留:V1 密码强度(弱密码救不了)、git 历史明文。

18. 第五轮渗透修复(v2.7.5,2026-10-05)

依据更新的 SECURITY_PENTEST.md:

  • V9【高危】/api/streams 写操作缺管理员 → 根治:新增 auth.requireAdminForWrites(GET/HEAD/OPTIONS 放行,写要 admin),挂到 server.js 的 /api/music + /api/streams。
  • V10/V11 → 被同一中间件覆盖。
  • V12 SSE 无上限 → sse.js 加总 500 + 每 IP 10 上限,超限 503。
  • V13 搜索限流被缓存绕过 → 限流检查前置。
  • V14 日志 CRLF 注入 → logger.js 过滤控制字符。
  • V1 → start.cmd 升级为 scrypt 加盐慢哈希。
  • V5 上轮已修;V6 评估后不改(切片被 m3u8 引用)。

测试:38 项全绿。 遗留:弱密码 123test(建议换强随机)。

19. 权限中间件细化(v2.7.6,2026-10-05)

背景:requireAdminForWrites 的"非 GET 即需 admin"太粗暴,误伤 /api/music/detail(POST 但是普通用户查详情用的)。另:要求搜索仅管理员,普通用户只能用 id/链接解析。

改动:

  1. auth.js requireAdminForWrites 加 POST 读白名单 POST_READ_WHITELIST = {"/music/detail"}——这些 POST 语义是读,放行给普通用户。
  2. routes/music.js /search 单独加 requireAdmin(GET 也要 admin)。
  3. public/js/app.js searchMusic():普通用户输入非 id → 提示"请用 ID 或链接",不调 search。

验证:

  • 普通用户:detail 200 ✅ / search 403 ✅
  • 管理员:search 200 ✅
  • 38 项回归全绿。

权限模型现状:

  • 读(GET):普通用户可用(除非单独加 admin,如 search)
  • 写(POST/DELETE/...):默认 admin,白名单里的"读语义 POST"(detail)例外

20. 前端误退档恢复(v2.7.6,2026-10-06)

事件:前端 public/ 被误退档回 v2.6 旧版(丢了 StarLight 登录、CSRF、userName 显示、搜索适配)。

恢复源:F:\music_online\demo_starlight_gitbackup_20261005_2206(本喵清 git 历史前的完整快照,10-05 21:33~21:55)。

恢复动作:

  1. 从备份拷回 public/login.html、index.html、js/app.js、css/style.css。
  2. 备份缺的"搜索仅管理员"适配重做:app.js searchMusic() 加 state.role === "admin" 判断,普通用户提示用 id/链接。
  3. test_*.py 从 demo_starlight_backup_20261005_2130 恢复后,重做 CSRF 头 + TEST_PASSWORD 环境变量适配。

验证:38 项全绿(api16/e2e17/write5)。

教训:改动后及时 commit(哪怕本地),别让工作区长期处于"未提交"状态——一旦误操作(退档/覆盖),未提交的改动最难找回。本次全靠"清 git 历史前的意外备份"救回。

21. 多角色账号登录修复(v2.7.7,2026-10-06)

问题:账号加了第 2 个角色(Xiao_Tps)后,StarLight 不再返回 selectedProfile(改为返回 availableProfiles 数组),导致 starlight.js 误判"该账号尚未创建游戏角色",登录 401。

修复:starlight.js 角色选择逻辑改为——

  1. 有 selectedProfile → 用它
  2. 无但有 availableProfiles → 取第一个(老板选方案 A,自动选第一个)
  3. 都无 → 才报"无角色"

验证:多角色账号(1184070342@qq.com)登录 → 200,自动选 Xiao_love(role=admin)。

踩坑:修复后首次测试仍 401——旧服务进程占着 3000 端口没清(EADDRINUSE),新服务没起来,跑的还是旧代码。taskkill /F /T /PID 106328 后正常。

22. 设置页扩充 + 删关于页(v2.8.0,2026-10-06)

删关于页

  • index.html:删导航 tab + pageAbout 页面(139 行);app.js pageOrder 去掉 pageAbout。

设置页热配置(真·管理员控制)

新增 11 项可热改配置(存 settings 表,立即生效,无需重启):

配置项 key 说明
切片时长 hlsTime 影响新流
滑动窗口 slideWindow 影响新流
缓存上限 MB maxCacheMB
默认频道 defaultStream 存值(待接入默认流逻辑)
日志级别 logLevel 热改 levelNum
会话有效期 sessionTtlHours
登录失败上限 loginMaxFails
登录锁定时长 loginLockMin
搜索缓存时长 searchCacheTtlSec
搜索限流上限 searchRateMax
搜索限流窗口 searchRateWindowSec

实现:

  • config.js 新增 applyOverrides(settings)——从 settings 覆盖 config。
  • logger.js 新增 setLevel(name)(levelNum 从 const 改 let)。
  • routes/system.js POST /settings 扩白名单 + 保存后调 applyOverrides + setLevel;GET /config 返回全部可配字段。
  • server.js bootstrap 启动时加载 settings 覆盖。
  • public/index.html 设置页"运行配置"只读 grid → 可编辑表单(11 项)。
  • public/js/app.js loadSettings 填充表单 + btnSaveSettings 保存。
  • public/css/style.css 加 .settings-form/.setting-row 样式。

验证:POST 改 hlsTime/logLevel/searchRateMax → GET /config 立即反映新值。38 项回归全绿。

注意:HTTP 端口、FFmpeg 并发等需重启的项不做(老板要求只做热生效)。

23. 登录页风格统一(v2.8.1,2026-10-06)

背景:登录页原本是旧暗色主题(#0d1117),与主页面浅色主题(#f4f5f9)不一致。

改动:public/login.html 完全重写样式,套用主页面设计 token:

  • 浅色背景 --bg #f4f5f9 + 白卡片 --surface + 阴影 --shadow-lg
  • 强调色统一 --accent #4f6ef7
  • 字体 Plus Jakarta Sans
  • 大圆角 --radius-xl (28px) + 卡片升起动画
  • 输入框聚焦光晕 --accent-glow
  • 主按钮 .btn-primary(蓝底白字)+ 应急按钮 .btn-secondary(描边)
  • 引入 bootstrap-icons.css(登录/图标)

功能不变:StarLight 用户名+密码登录、管理员应急登录、/api/auth/login 调用。

验证:/login.html 200、38 项回归全绿。

24. 可视化权限管理(v2.8.2,2026-10-06)

背景:原管理员靠 start.cmd 的 STARLIGHT_ADMINS 环境变量(改要重启),老板问"总不能还去改文件"。

方案:设置页加"权限管理"——用户列表 + 一键授予/取消管理员,热生效。

改动:

  • db.js:新增 admins 表(uuid PK, name, added_at);sessions 加 starlight_uuid 列(迁移)。
  • store.js:listAdmins/getAdmin/isAdminUuid/addAdmin/removeAdmin/listUsers。
  • auth.js:loginViaStarLight 判角色先查 admins 表再查环境变量;createSession 存 uuid;新增 getStarlightUuid。
  • routes/auth.js:登录传 uuid 给 session;/me 返回 myUuid。
  • routes/system.js:GET /users(登录过的用户+isAdmin)、GET/POST/DELETE /admins。
  • public/index.html:设置页加"权限管理"卡片(用户列表)。
  • public/js/app.js:loadUsers() 渲染用户列表 + 设为/取消管理员按钮。
  • public/css/style.css:.user-list/.user-item/.admin-badge/.btn-mini 样式。

判定优先级:admins 表 ∪ 环境变量 STARLIGHT_ADMINS(UUID)。

验证:真实账号登录→/me 返回 myUuid;用户列表显示 isAdmin;加/删管理员正常。38 项回归全绿。

注意:旧 sessions 记录无 uuid(迁移前登录的),需重新登录一次补上。

25. 设置页布局优化(v2.8.3,2026-10-06)

问题:设置页"很挤,每个卡片都变成独立滚动"。

根因:.section-body 有 flex:1; min-height:0; overflow-y:auto → 每个卡片内容区各自滚动,一堆小滚动条。

修复:

  • .section-body 去掉 flex:1 / min-height:0 / overflow-y:auto(内容自然撑开)。
  • .page.active 加 overflow-y:auto(整页统一滚动,一个滚动条)。
  • 设置页布局优化:#pageSettings gap 加大、.section-body padding 加大、.setting-row 行距加大、日志区限高自身滚动、用户列表间距加大。

验证:CSS 括号平衡、38 项回归全绿。各页无独立滚动规则,统一滚动安全。

26. 设置页布局优化 v2(v2.8.4,2026-10-06)

前情:v2.8.3 第一次改崩(去掉 .section-body 的 flex+overflow 导致内容溢出),已回滚。

本次正确解法(理解布局链后):

  • .page.active:+overflow-y: auto → 整页统一滚动
  • .section-card:+flex-shrink: 0 → 卡片不被压缩(关键!首次失败就漏了这个)
  • .section-body:去掉 flex:1 / min-height:0 / overflow-y:auto → 内容自然撑开

布局链(勿乱动):

.page-container (flex:1; min-height:0)
  └── .page.active (flex:1; min-height:0; overflow-y:auto) ← 整页滚
        └── .section-card (flex-shrink:0) ← 不压缩
              ├── .section-header (flex-shrink:0)
              └── .section-body (自然高度)

验证:CSS 括号平衡 395/395、38 项回归全绿。

教训:CSS 布局改动前先理解 flex/height/overflow 依赖链;用 python 精确替换 + assert count==1 防误伤;改前备份。

27. 全站改名:Starlight专属电台(v2.8.5,2026-10-06)

改动:"Xiao の 电台" / "音乐在线电台" → "Starlight专属电台",全项目统一。

文件 位置
index.html title + 顶栏品牌
login.html title + h1
app.js 文件注释
package.json description
README.md 标题
HANDOFF.md 标题

验证:全项目零旧名残留。

28. 循环池(手动歌单 + 随机不重复播放)(v2.9.0,2026-10-06)

需求:资源列表"重新匹配作者"按钮 → 换成"加入/移出循环池";队列播完空时从循环池随机抽(抽过标记,全播完重置);池空降级从全部资源随机。

改动:

  • db.js:新增 stream_pool 表(stream_id, song_id, played, added_at, PK(stream_id,song_id))。
  • store.js:getPoolRows/listPoolSongIds/isInPool/addToPool/removeFromPool/drawFromPool/markPoolPlayed。drawFromPool 抽未播的,全播过则重置。
  • livestream.js:ensureNotEmpty 队列空→drawFromPool(抽到标记 played)→池空降级 getAllSongs 随机。
  • routes/streams.js:GET /:id/pool、POST /:id/pool/add|remove。
  • public/js/app.js:资源列表按钮 js-enrich→js-pool(切换加/移,active 高亮);loadResources 查池标记 inPool;togglePool()。
  • public/css/style.css:.js-pool.active 样式。

每流独立池。enrich 接口保留(仅删资源列表入口)。

验证:池增删查正常;抽歌不重复+重置;38 项回归全绿。

29. 白盒安全审查 + 修复(v2.9.1,2026-10-06)

审查范围:全部 src/(2963 行)+ 前端。发现并修复:

🔴 P1 SSRF(metadata.js)✅

  • request() / downloadCover() URL 来自网易云响应(HTTP 明文可劫持),无域名校验 → 可请求内网/file://。
  • 修:加 isAllowedHost() 白名单(*.music.163.com / *.music.126.net),request 和 downloadCover 都校验。实测 169.254.169.254 被拒。

🟡 P3 SSE 计数泄漏(sse.js)✅

  • broadcast 删失败 client 时未减 ipCount → 计数泄漏。
  • 修:client 打 __sseIp 标记,删除时同步减计数。

🟡 P4 空 catch 加日志 ✅

  • logger.js(日志流)、metadata.js(时长/文件大小探测)、media.js(计数)、routes/music.js(删临时文件)——加 warn 日志。
  • util.js(getDirSize 读不到文件)——保持静默 + 注释说明。

✅ 已确认无问题

权限挂载、SQL 参数化、输入校验(isValidFileName)、无命令注入(spawn 数组)、advance 同步无重入、TaskQueue 正确、FFmpeg 超时 kill、SSE 连接上限。

验证:38 项回归全绿 + SSRF 白名单实测。