# 交接文档 · 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 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) ```sql 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. 测试方法 服务起来后: ```cmd 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 素材) ``` 素材生成: ```cmd "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 查行号**: ```js 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 -- ` 恢复 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。 **测试脚本适配**:三个 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::`),兼容 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 白名单实测。