功能: - StarLight Yggdrasil 第三方登录(后端中转,accessToken 不落地) - 管理员可视化权限管理(用户列表 + 一键授予/移出) - 设置页热配置(11 项可编辑,立即生效) - 循环池(每流独立手动歌单,队列空时随机不重复抽取) - 登录页统一主站浅色风格 - 全站改名「Starlight专属电台」 - 删除关于页 安全: - 全局 CSRF 防护(双提交 Cookie) - 登录限流防 XFF 伪造、错误信息脱敏 - 权限中间件 requireAdminForWrites(读放行/写 admin) - 密码哈希存储(scrypt/sha256) - SSRF 防护(封面下载域名白名单) - 空 catch 加日志、SSE 连接上限 修复: - 多角色账号 selectedProfile 缺失 - 普通用户越权删歌(V4) - streams 写操作越权(V9) - 设置页布局溢出 测试:38 项全绿(api16 / e2e17 / write5)
26 KiB
交接文档 · 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. 关键设计决策(勿轻易推翻)
- SQLite better-sqlite3 + WAL:单进程同步 API 最省事,WAL 解读写互斥
- 不迁 v1 JSON 数据(旧 heat/metadata/playlist.json 已删)
- 鉴权整站 Cookie 会话;HLS 流不锁(播放器难带 cookie)
- 封面/资源全本地化:
public/covers/、public/vendor/;系统字体 - songs.file 存文件名非绝对路径,可移植
- 导入先落 tmp 再 rename,避免 chokidar 扫到半截
- 双密码角色:用户 = 管理员 − 上传 − 设置页;后端
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. 工具层踩坑记录
- XML 实体转义吃
&":写 JS/CSS 用String.fromCharCode(38)或字符码构造 - ⚠️ 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)}) - replace_lines 多段连锁崩:同一文件多段改,某段行号错会留残留。逐条改,改前 node 查号、改后
node --check - 文件被改乱:别手修,
git checkout <commit> -- <file>恢复 - 后台服务用 action background="true"(勿
start /b) - service_stop 连子进程一起杀(含 node 的 ffmpeg 子进程)
- 改完源码要重启 node 才生效
- Windows 无 head/tail,用 findstr 或 node
- cmd 下 curl:
%{http_code}要写%%{http_code};中文输出 GBK 乱码(功能正常) - git 提交中文:powershell 写无 BOM UTF-8 文件 +
git -c i18n.commitEncoding=utf-8 commit -F 文件 - 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. 下一步可选
- 浏览器手动过一遍所有交互,修 Console 报错 ← 最优先
- 播放延迟调节(HLS_TIME 热更新,老板曾暂缓)
- 密码设置 UI(现走环境变量)
- 缓存生成进度条、搜索分页/加载更多
- mini player 音量控制、播放历史/最热排序(heat 字段已有)
- 正式化:固定强密码、服务化启动
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_csrfcookie(非 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;登出清 csrfsrc/server.js— 受保护 API 挂auth.csrfProtectionpublic/js/app.js—api()统一给写请求加X-CSRF-Tokentest_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。
测试脚本适配:三个 test_*.py 已改,写请求自动读 cookie jar 的 mr_csrf。
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/链接解析。
改动:
auth.jsrequireAdminForWrites加 POST 读白名单POST_READ_WHITELIST = {"/music/detail"}——这些 POST 语义是读,放行给普通用户。routes/music.js/search单独加requireAdmin(GET 也要 admin)。public/js/app.jssearchMusic():普通用户输入非 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)。
恢复动作:
- 从备份拷回
public/login.html、index.html、js/app.js、css/style.css。 - 备份缺的"搜索仅管理员"适配重做:
app.jssearchMusic()加state.role === "admin"判断,普通用户提示用 id/链接。 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 角色选择逻辑改为——
- 有
selectedProfile→ 用它 - 无但有
availableProfiles→ 取第一个(老板选方案 A,自动选第一个) - 都无 → 才报"无角色"
验证:多角色账号(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.jspageOrder去掉 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.jsPOST /settings扩白名单 + 保存后调applyOverrides+setLevel;GET /config返回全部可配字段。server.jsbootstrap启动时加载 settings 覆盖。public/index.html设置页"运行配置"只读 grid → 可编辑表单(11 项)。public/js/app.jsloadSettings填充表单 +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(整页统一滚动,一个滚动条)。- 设置页布局优化:
#pageSettingsgap 加大、.section-bodypadding 加大、.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 白名单实测。