docs: HANDOFF 精简重写 (v2.6 交接版)

- 版本流水账压缩为时间线表
- 补充 v2.5/v2.6 权限模型与 API
- 工具坑更新 powershell 行号偏移教训
This commit is contained in:
Xiao-no-love committed 2026-09-25 22:43:40 +08:00
1 parent 02ea161134
commit 789ee50391
1 file changed
+128 -398
+128 -398
View File
@@ -1,73 +1,64 @@
# 交接文档 · 音乐在线电台 v2
# 交接文档 · 音乐在线电台
> 本文件用于跨会话交接。上下文耗尽后,新会话**先读此文件**再动手。
> 跨会话交接。**新会话先读此文件再动手**。最后更新:2026-09-25(v2.6)。
---
## 0. TL;DR
- **项目**:`F:\music_online\demo` — Node.js + FFmpeg 的 HLS 音乐电台
- **状态**:v2.3 **功能完整 + UI 已去 AI 味重构**,38 项测试全绿(api16/e2e17/write5);详见 §16。
- **服务**:正在运行 `svc005`,`http://127.0.0.1:3000`,密码 `test123`(测试用)
- **Git**:`ea5aa0c`(基线) → `b8e9bf3`(v2主体) → `a4745db`(收尾)
- **项目**:`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,npm 11.5.1,FFmpeg 9.0.1(含 ffprobe,均在 PATH),Python 3.11
- 启动:
- Windows 11,Node v24.5.0,FFmpeg 9.0.1(`F:\ffmpge\ffmpeg-9.0.1-essentials_build\bin`,含 ffprobe),Python 3.11
- **启动**(推荐托管):
```cmd
cd /d F:\music_online\demo
set PANEL_PASSWORD=test123 && node index.js
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`(已装)
- 登录:浏览器开 `http://127.0.0.1:3000`,输入密码
---
## 2. 目录结构(v2)
## 2. 目录结构
```
demo/
├── index.js # 入口,仅 require("./src/server").start()
├── package.json # v2.0.0
├── data.db # SQLite(自动生成,WAL)
├── HANDOFF.md # 本文件
├── README.md # v2 说明
├── 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 初始化 + 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 # Cookie 会话鉴权 + requireAuth 中间件
│ ├── sse.js # Server-Sent Events 广播
│ └── routes/
│ ├── auth.js # /api/auth/*
│ ├── music.js # /api/music/* (搜索/详情/下载/导入/删除/enrich)
│ ├── resources.js # /api/resources
│ ├── streams.js # /api/streams/* (流管理 + 播放列表)
│ └── system.js # /api/system/* (缓存/日志/配置/设置)
│ ├── 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 # 主面板
│ ├── login.html # 登录页(新)
│ ├── css/style.css # 含 v2 追加样式(.stream-select/.import-card/.now-badge)
│ ├── js/app.js # v2 重写:多路流/鉴权/导入/删除
│ ├── vendor/ # 本地化:bootstrap/icons/sortable + woff2
│ ├── covers/ # 本地化封面(每首一个 jpg)
│ └── favicon.ico/.png
├── scripts/fetch_vendor.py # 一次性下载前端 CDN 资源(已跑过)
├── audio/ # 音频(5 首 mp3,已 gitignore)
├── hls/ # 直播输出 hls/{streamId}/index.m3u8
├── hls_cache/ # 切片缓存 hls_cache/{fileHash}/
├── tmp/uploads/ # multer 上传临时目录
└── test_api.py / test_e2e.py / test_write.py / test_enrich.py # 测试脚本
│ ├── 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
```
---
@@ -75,405 +66,144 @@ demo/
## 3. 数据模型(SQLite, src/db.js)
```sql
songs(id, file UNIQUE, name, artist, cover, duration, file_size,
heat, cache_status, created_at)
-- file 存「文件名」不是绝对路径(可移植)
-- cover 存 web 路径如 /covers/xxx.jpg
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
-- 默认流 id='main',不可删
stream_playlist(id PK, stream_id, song_id, position) -- 外键 CASCADE
settings(key PK, value)
sessions(token PK, created_at, expires_at)
sessions(token PK, role, created_at, expires_at) -- role: admin|user
```
**关键**:切片缓存 `hls_cache/{md5(文件名)}/` 与「流」无关,多路共享,省空间。
- 切片缓存 `hls_cache/{md5(文件名)}/` 与流无关,多路共享
- **运行时迁移**:init() 用 `PRAGMA table_info` 检测缺列后 ALTER(已支持 sessions.role、songs.cover_remote)
---
## 4. API 清单
除 `/api/auth/*`、`/hls`、`/hls_cache`、静态资源、`/login` 外,**全部需登录**。
除 `/api/auth/*`、`/hls`、`/hls_cache`、静态资源、`/login` 外**全部需登录**。
🔒 = 需**管理员**。
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/auth/login` | `{password}` → 设 cookie |
| POST | `/api/auth/logout` | 登出 |
| GET | `/api/auth/me` | 登录状态 |
| GET | `/api/resources` | 资源列表(含 cacheSize/cover) |
| GET | `/api/music/search?keyword=` | 网易云搜索 |
| POST | `/api/music/detail` | `{id}` |
| POST | `/api/music/download` | `{id,name,artist,albumImage}` |
| POST | `/api/music/import` | **multipart**:audio + title + artist |
| POST | `/api/music/delete` | `{file}` 级联删 |
| POST | `/api/music/delete-batch` | `{files:[]}` |
| POST | `/api/music/enrich` | `{file, skipMatch?}` 重匹配元数据 |
| GET | `/api/streams` | 流列表 |
| POST | `/api/streams` | `{id,name}` 创建 |
| DELETE | `/api/streams/:id` | 删除(main 不可删) |
| POST | `/api/streams/:id/start\|stop` | 启停 |
| GET | `/api/streams/:id/playlist` | 播放列表(含 entryId/isCurrent) |
| POST | `/api/streams/:id/playlist/add` | `{file}` |
| POST | `/api/streams/:id/playlist/remove` | `{entryId}` |
| POST | `/api/streams/:id/playlist/reorder` | `{order:[entryId...]}` |
| POST | `/api/streams/:id/playlist/clear\|next\|prev\|jump` | |
| GET | `/api/system/cache` | 缓存占用 |
| POST | `/api/system/cache/cleanup` | 手动清理 |
| GET | `/api/system/logs?tail=` | 日志 |
| GET | `/api/events` | SSE(需 cookie 鉴权) |
| 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:"..."}`。
统一返回 `{ok:true,...}` / `{ok:false,error:"..."}`。
---
## 5. 直播流地址
- 默认:`/hls/main/index.m3u8`
- 其它:`/hls/{streamId}/index.m3u8`
- 切片:`/hls_cache/{hash}/indexN.ts`
- 默认 `/hls/main/index.m3u8`;其它 `/hls/{streamId}/index.m3u8`
- 切片 `/hls_cache/{hash}/indexN.ts`
---
## 6. 关键设计决策(勿轻易推翻)
1. **SQLite 用 better-sqlite3 + WAL**:单进程应用,同步 API 最省事;WAL 解决读写互斥。用户曾担心并发,已解释并落地。
2. **不迁移 v1 的 JSON 数据**:用户明确说旧数据不要。
3. **鉴权选"中档"**:整站 Cookie 会话登录,面板+API 锁,**HLS 流不锁**(播放器难带 cookie)。
4. **封面/资源全本地化**:`public/covers/` 存封面,`public/vendor/` 存前端库;系统字体替代 Google Fonts。
5. **songs.file 存文件名**:不是绝对路径,迁移目录不失效。
6. **导入先落 tmp 再 rename**:避免 chokidar 扫到半截文件。
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. 测试方法
服务起来后(svc005 / 或新起)跑:
服务起来后:
```cmd
cd /d F:\music_online\demo
set PYTHONIOENCODING=utf-8
python test_api.py # 16 项:鉴权/资源/流/缓存/日志
python test_e2e.py # 17 项:静态资源/登录/首页/HTML元素
python test_write.py # 5 项:导入 + 删除(用 tmp 副本,不动原曲)
python test_enrich.py # 给 5 首补封面(已跑过,5/5 成功)
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 素材)
```
上次全绿:**38/38**。但**注意**:这些只测了 HTTP 层,**浏览器端 JS 交互没自动化测过**。
素材生成:
```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. 已完成清单
## 8. 已知问题 / 待实测
- [x] SQLite 替换 JSON(5 表 + WAL)
- [x] 级联删除 `/api/music/delete` + 批量
- [x] 多路流 LiveStream 类 + 每路独立播放列表
- [x] Cookie 会话鉴权 + 登录页 + 鉴权绕过漏洞修复
- [x] 本地上传导入(multer)+ 元数据自动匹配
- [x] 封面本地化 + vendor 本地化 + 系统字体
- [x] 日志系统(分级/落盘/面板可查)
- [x] API 统一返回结构
- [x] 优雅退出补 FFmpeg 子进程 kill
- [x] 模块化拆分(1233 行单文件 → src/ 12 模块)
- [x] 前端 app.js 重写(多路流/鉴权/导入/删除)
- [x] **前端多路流管理 UI(频道管理模态框:新建/删除/切换)** ← v2.1
- **浏览器端未实测**:SSE 实时刷新、拖拽 reorder、mini player、上传进度、频道弹窗交互
- `metadata.js enrichSong` 启动重匹配慢歌 → 可接受
- 多路流并发预生成排队 → 观察中
- `app.js esc()` 用 `String.fromCharCode(38)` 绕过工具转义,**勿优化**
- 登录失败锁定 `failMap` 是内存态,重启清零(可接受)
---
## 9. ⚠️ 待排查 / 已知问题(重点!)
## 9. 工具层踩坑记录
### 9.1 用户反馈"功能还有问题" —— **具体未明,需先问清楚**
> 下一会话**第一件事**:问用户到底哪个功能出问题(现象、复现步骤、控制台报错)。
> 可能的方向见下。
### 9.2 前端多路流管理 UI —— ✅ 已解决(v2.1)
- 已补:header 新增 `#btnStreamMgr` 入口 → 弹出「频道管理」模态框 `#streamModal`。
- 功能:列出所有流(含运行状态/当前标记)、一键切换、删除(main 受保护)、新建(ID+名称,前端正则校验)。
- 相关:`index.html` 新增 modal 结构;`app.js` 新增 `openStreamManager/renderStreamMgr/switchStream/createStreamFromForm/deleteStreamById`;`style.css` 末尾追加 `.modal-*/.stream-*` 样式。
- 已验证:后端 create/delete/重复/非法ID/main保护 全通过;app.js `node --check` 通过;JS 引用 ID 与 HTML 无缺失。
### 9.3 前端 JS 交互未实测
- `public/js/app.js` 是重写的,只做了语法检查 + HTTP 层测试。
- **未验证**:SSE 实时刷新、拖拽排序 reorder、mini player、页面切换、导入进度提示等浏览器行为。
- 建议:手动开浏览器点一遍,看 Console 有无报错。
### 9.4 潜在 Bug 点 —— ✅ 已处理(v2.1)
- ~~`enforceCacheSizeLimit` 依赖 streams Map~~ → 保护集为空时最坏误删缓存,无害;保留。
- ~~`scanner.js` 里 `const parsed = meta.sanitizeFileName;` 死代码~~ → **已删**。
- `metadata.js enrichSong` 启动时重匹配慢歌 → 保留(可接受)。
- 多路流并发预生成排队 → 观察中。
- `app.js` 的 `esc()` 用 `String.fromCharCode(38)` 绕过工具层转义,**勿优化**。
### 9.4b v2.1 死代码清理 + Bug 修复(本轮)
- **删死代码**:`media.setCacheCallbacks`(含 `onCacheReady/onCacheError`,无调用方)、`auth.getPassword/isGenerated`、`store.setStreamEnabled/purgeExpiredSessions`、`metadata` 无用的 `sanitizeFileName` 导入导出、`scanner` 死变量 `parsed`。全部 `node --check` 通过。
- **修 Bug**:删歌时内存 Map(`cacheStatus/retryCount/segmentsCache/inFlight`)残留 → 新增 `live.forgetSong(file)`,删除路由两处改用它,避免同名文件重导读到脏状态。
- **修 Bug**:`server.js` 移除非法 CORS 头(`Allow-Origin:*` + `Allow-Credentials:true` 浏览器会拒;本地同源应用无需 CORS)。
- **测试**:45 项全绿(api 16 / e2e 17 / write 5 / 真实 ogg 导入 7)。
### 9.5 前端 HTML 元素 ID 对照(改前端时用)
- 头部:`cacheInfo` `streamSelect` `liveLink` `btnLogout`
- 搜索页:`musicId` `btnSearch` `parseHint` `parsedId` `musicPreview` `musicCover` `musicName` `musicArtist` `musicDetails` `btnDownload` `searchResults` `searchTips`
- 导入卡片:`importCard` `importFile` `importTitle` `importArtist` `btnImport` `importProgress`
- 资源页:`resourceCount` `resourceSearch` `resourceList`
- 列表页:`playlistCount` `btnClear` `playlistList`
- mini player:`miniPlayer` `mpProgress` `mpBody` `mpCover` `mpName` `mpArtist` `mpTime` `mpPlay` `mpExpand` `musicPlayer`
- 导航:`.nav-tab[data-page]`,页面 `pageSearch/pageResources/pagePlaylist/pageAbout`
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 <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. 工具层踩坑记录(写代码时注意)
## 10. 版本时间线
1. **XML 实体转义会吃掉 `&` `"`**:写 JS/CSS 含这些字面量时,用 `String.fromCharCode(38)` 或字符码构造,别直接写。
2. **replace_lines 多段锚点行号易偏**:先 `file_read` / python 打印实际行号再改;改错了整批不生效或误删。
3. **smart_patch 行数必须精确**,且锚点行号要对;容易失败,简单改动优先 replace_lines。
4. **后台服务用 action background="true"**(不要 `start /b`,会粘连卡死)。
5. **service_stop 会连子进程一起杀**(node 的 ffmpeg 子进程会被带走)。
6. **Windows 无 `head`/`tail`**,用 `findstr` 或 python。
7. **改完源码要重启 node**(磁盘 .py/.js 改了,运行中的进程不生效)。
8. **Python 跑 Windows 输出**:`chcp 65001 && set PYTHONIOENCODING=utf-8`。
9. **`node -X utf8` 是错的**(那是 Python 的选项),node 直接 `node` 即可。
| 版本 | 日期 | 要点 |
|------|------|------|
| 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. 当前运行态
## 11. 下一步可选
- 后台服务:`svc005`(PID 15308 起),日志 `service_log svc005`
- DB:`data.db`(5 首歌曲,封面已本地化)
- 缓存:`hls_cache/` 5 个目录,约 28MB
- Git 工作区:干净(最新 `a4745db`)
1. **浏览器手动过一遍**所有交互,修 Console 报错 ← 最优先
2. 播放延迟调节(HLS_TIME 热更新,老板曾暂缓)
3. 密码设置 UI(现走环境变量)
4. 缓存生成进度条、搜索分页/加载更多
5. mini player 音量控制、播放历史/最热排序(heat 字段已有)
6. 正式化:固定强密码、服务化启动
---
## 12. 下一步建议(按优先级)
## 12. 当前运行态
1. **问清用户的"问题"具体是什么** ← 最优先
2. 补前端"新建/删除流"UI(若需求确认)
3. 浏览器手动过一遍所有交互,修 Console 报错
4. 清理死代码(§9.4)
5. 正式化:设置固定 `PANEL_PASSWORD`,写 systemd/服务化启动
6. 补多路流并发压力测试
---
## 13. 用户视角 · 待优化点(下一会话可选)
> 站在真实使用者角度梳理,按"体验影响"排序。均**未实施**,需老板定优先级。
### 高价值(体验硬伤)
1. **缺"电台收听页"** —— 现在 `liveLink` 只打开裸 `.m3u8`,浏览器无法直接播放。应在站内加一个 HLS.js 播放页(选频道→收听直播),否则"电台"没法在浏览器里直接听。
2. **面板无"正在播放"实时卡片** —— 打开面板不知道电台此刻放到哪首。SSE 已推 `now-playing`/`song-finished`,前端只 `loadPlaylist()`,应加一个醒目的 now-playing 展示(封面+歌名+进度)。
### 中价值
3. **设置页无 UI** —— `/api/system/settings`、`/api/system/config` 已就绪,前端没有对应界面。
4. **导入上传无进度百分比** —— 现只有"上传中..."文字,fetch 不支持上传进度,需换 XHR + `upload.onprogress`。
5. **缓存生成无进度** —— 导入后只能看到"生成中/已缓存"两态,无进度条。
6. **搜索无分页/加载更多** —— `searchNetEase` 固定 limit 20。
### 低价值 / 打磨
7. **mini player 无音量控制**;8. 无播放历史/最热排序(`heat` 字段已有);9. 单弱密码 `test123`(正式化要固定强密码,见 §12);10. 网易云无版权只给文字提示,可做得更友好。
### 已知需实测(无法自动化)
- 浏览器端:SSE 实时刷新、拖拽 reorder、mini player、上传进度、页面切换动画、频道管理模态框交互。
---
## 14. v2.2 前端增强(已完成)
本轮在 v2.1 基础上补齐了用户视角的 4 个高/中价值缺口:
### 14.1 站内 HLS 收听页(`#pageLive`,默认首页)
- 新增底部导航「收听」tab(5→6 个),作为默认 landing 页。
- 引入本地化 `public/vendor/hls.min.js`(hls.js 1.5.17,414KB,全本地化无 CDN)。
- `app.js`:`startLive/stopLive/toggleLive/loadLiveCover`,用 HLS.js 挂载到全局 `#musicPlayer`。
- 修复了「点 liveLink 打开裸 m3u8 无法播放」的根本问题——现在浏览器内可直接收听。
- 卡片展示:封面(取当前播放曲目封面)+ 频道名 + 当前曲目 + 播放按钮 + 状态。
### 14.2 面板「正在播放」实时化
- 发现并修复:后端**从不发 `now-playing` 事件**,前端那个 `es.addEventListener("now-playing")` 是**死监听**,已删。
- 真正信号是 `playlist-update` / `song-finished`;新增 `refreshLive()`(300ms 节流)接到这两个事件 + 轮询。
### 14.3 设置页(`#pageSettings`)
- 缓存管理:已用/上限 + 进度条(绿/黄/红三档)+ 一键清理。
- 运行配置:HTTP 端口/切片时长/窗口/缓存上限/FFmpeg 并发(`/api/system/config`)。
- 运行日志:tail 行数可选(50/100/200/500),暗色终端风格(`/api/system/logs`)。
### 14.4 上传进度百分比
- `fetch` 不支持上传进度,新增 `uploadWithProgress()`(XHR + `upload.onprogress`)替换导入逻辑。
- 上传时实时显示「上传中... N%」。
### 14.5 验证
- 回归:api 16/16、e2e 17/17。
- 新增功能校验:12/12(页面元素 + 3 个接口)。
- HLS 流实测:`/hls/main/index.m3u8` 15 段滑动窗口,切片可达。
### 14.6 待老板浏览器实测
- 收听页点播放是否出声、切频道是否正确切流。
- 设置页日志滚动、缓存条颜色。
- 上传大文件进度条。
---
## 15. v2.2.1 资源排序(已完成)
- 资源页头部新增排序下拉 `#resourceSort`:**加入时间 ↓↑ / 热度 ↓↑ / 名称 ↑↓** 共 6 种。
- 后端 `/api/resources` 补 `createdAt` 字段(取 `songs.created_at`)——之前没返回,时间排序会失效。
- 前端 `sortResources()` 本地排序;名称用 `localeCompare(..., "zh", {numeric:true})` 支持中文与数字。
- 与搜索框组合:排序改数据顺序,搜索只是隐藏不匹配项,二者不冲突。
- 测试脚本改为**动态基线**(不再硬编码 5 项),新增歌曲不会误报。回归 38/38 全绿。
---
## 16. v2.3 UI 去 AI 味重构(已完成 2026-09-24)
> 原「MIUIX 风格重构」任务。老板改方向:**不找 MIUIX**(那是 Compose 的),改用 GitHub `ibelick/ui-skills`(Web 版设计工程 skill)的规则手工重构。
### 16.1 采用的 skill
- **`C:\Users\xiao\.claude\skills\ui-skills`**(clone 自 github.com/ibelick/ui-skills,9046★)。
- 关键规则(`skills/baseline-ui/SKILL.md`):**禁渐变**、禁紫/多彩渐变、禁 glow 当主视觉、单视图单强调色、交互反馈 ≤200ms、入场 ease-out、只动 transform/opacity。
- 我们的项目是原生 HTML/CSS/JS(非 Tailwind/React),只取其**原则**,类名约束不适用。
### 16.2 改动清单
1. **频道管理+切换合并为单一入口**:
- 删除 header 的 `<select id="streamSelect">` 下拉 + 原「频道管理」按钮,合并为单一按钮 `#btnStreamMgr.channel-btn`(图标+当前频道名 `#currentChannelName`),点击打开原有 `#streamModal`(内含切换/删除/新建)。
- `app.js`:`loadStreams()` 不再填 select,改写 `#currentChannelName` 文本;删除 select 的 change 监听;`switchStream()` 补 `if (livePlaying) stopLive()`。
- `style.css`:`.stream-select` 样式 → `.channel-btn`。
2. **去 AI 味(清全部渐变/glow)**:
- `page-bg` 径向光晕 → 纯色 `var(--bg)`;`.bg-shapes` 紫色 `#8b5cf6` → `var(--accent)`。
- `.about-hero` 渐变 → `var(--accent-light)`;`.mp-progress` 渐变 → 半透明纯色。
- `.live-cover-ph` / `.live-play-btn` / `.live-progress-fill` 橙红渐变 `#ff5f6d→#ffc371` → `var(--accent)`;play-btn glow 阴影 → `var(--shadow-sm)`;live-badge glow → 中性。
- `.cache-bar-fill` 三档渐变 → `var(--success/--warning/--danger)`。
- live 区、modal 区硬编码色(#222/#999/#333/#888/#fff/rgba)统一改为 token。
- 保留:login.html 暗色径向(独立深色主题,非 AI 味)。
3. **测试更新**:`test_e2e.py` 断言 `id="streamSelect"` → `id="btnStreamMgr" and id="currentChannelName"`。
### 16.3 验证
- `node --check public/js/app.js` 通过。
- 回归:api 16/16、e2e 17/17、write 5/5(需先 ffmpeg 生成 `tmp/test_import.mp3` 素材)。
- 全局扫描:public/ 下渐变计数 = 0(除 login.html 暗色径向)。
### 16.4 备份
- `F:\music_online\demo_backup_20260924_2129`(本次改动前的备份,仍有效)。
### 16.5 待老板浏览器实测(无法自动化)
- 频道按钮点开弹窗、切频道是否正确换流。
- 收听页配色是否满意(现为单 accent 蓝,无渐变)。
- 各项 hover/动效观感。
### 16.6 未做 / 可选后续
- 是否 `git commit`(工作区改动含本次 + 上轮 v2.2,均未提交)。
- 深色模式、缓存条颜色再打磨、其余「AI 味」细节。
---
## 17. v2.4 UI 规范化 / Token 体系(已完成 2026-09-24)
> 老板反馈「整体 UI 风格很撕裂」→ 全量重构 `style.css` 的 token 与取值体系。
### 17.1 问题诊断
`style.css` 原是两段拼接:**主体系**(1~1730,16 空格缩进、有分节) + **补丁区**(1731+,不缩进、硬编码色的 v2/v2.1/v2.2 追加)。具体失控:
- 字号 **42 种**(0.54~2.4rem 密密麻麻,还混用 `0.82rem`/`.82rem` 写法)
- 圆角 **约 20 种**(3/5/6/7/8/9/10/11/12/14/16/18/20/22/28px,定义了 `--radius-*` 却大半没用)
- 颜色硬编码:55 hex + 38 rgba,兜底色 `#1f6feb` 出现 8 次(带 `var(--accent,#1f6feb)` fallback)
### 17.2 重构内容
**建立完整 token 体系**(`:root` 重写):
- 色板:`--bg/surface/white/accent*/text*/border*/danger*/success*/warning*/overlay/log-*`
- 字号阶梯 9 档:`--fs-2xs/…/--fs-4xl`
- 圆角阶梯:`--radius-xs/sm/base/lg/xl/pill`
- 阴影:`--shadow-xs/sm/base/lg/accent`
- 动效:`--dur-fast/base/slow` + `--ease-out/spring`
- 间距:`--sp-1`~`--sp-6`
**全局收敛**:
- `font-size`:42 种 → **9 种**(67 处替换为 var)
- `border-radius`:20 种 → **8 种**(28 处替换为 var,余 50%/xl 特殊)
- 硬编码色:正文区 **清零**(全转 var)
- accent 阴影重复值收进 `--shadow-accent`;`#fff`→`--white`;`#fdd`→`--danger-hover`
- **补丁区整体并入主体系**:统一 12 空格缩进、改用 `--fs-*`/`--radius-*`/`--sp-*`、重写分节注释
### 17.3 关键取值变化(有意微调,贴近原值)
- `--radius-sm`: 12px→10px,`--radius`: 16px→14px,`--radius-lg`: 24px→20px,`--radius-xl`: 28px 保留
- 补丁区硬编码圆角 14/18/22px → 归入 `--radius/--radius-lg`
- live-cover-ph 大图标 2.4rem → `--fs-4xl`
### 17.4 验证
- 全文括号配平({} 360/360、( ) 581/581),无 var 残渣/嵌套。
- 回归:api 16/16、e2e 17/17、write 5/5 全绿。
- `node --check app.js` 通过。
### 17.5 待老板浏览器实测
- 整体观感是否统一(字号阶梯、圆角、配色)。
- 特别注意:卡片圆角、按钮胶囊、模态框圆角的和谐度。
### 17.6 保留项(非撕裂)
- 毛玻璃 `rgba(255,255,255,…)` 背景、阴影 rgba:正当设计值。
- `login.html` 独立暗色主题(自己的 `:root`)。
- `body` 字体栈、`--log-bg` 终端风格:保留。
---
## 18. v2.5 安全加固 + 权限体系 + 封面网络化(已完成 2026-09-25)
> 老板提出 6 点问题,本轮解决其中 5 点(延迟调节因复杂度暂缓)。
### 18.1 双密码 + 角色权限(核心)
- **config.js**:`ADMIN_PASSWORD`(兼容旧 `PANEL_PASSWORD`)/ `USER_PASSWORD`;新增 `LOGIN_MAX_FAILS`/`LOGIN_LOCK_MS`/`SEARCH_CACHE_TTL_MS`/`SEARCH_RATE_MAX`/`SEARCH_RATE_WINDOW_MS`。
- **db.js**:`sessions` 表加 `role` 列;`songs` 表加 `cover_remote` 列。均带**运行时迁移**(PRAGMA table_info 检测后 ALTER)。
- **auth.js**:重写。`verifyLogin` 返回 `"admin"|"user"|null`;`createSession(role)`;新增 `requireAdmin` 中间件、`getRole/isAdmin`;**按 IP 登录失败锁定**(默认 5 次锁 10 分钟,内存 Map)。
- **routes/auth.js**:登录返回 `{ok, role}`;锁定返回 429;`/me` 返回 role。
- **权限矩阵**:用户 = 管理员 − 上传 − 设置页。`/api/music/import` 加 `requireAdmin`;`/api/system/*` 整块 `requireAdmin`。其余接口(资源/流/播放列表/搜索/下载)两者一致。
- **前端**:`init()` 先查 `/api/auth/me` 拿 role → `applyRoleUI()` 隐藏设置页 tab + 本地导入卡片;`loadSettings()` 非管理员直接 return。
### 18.2 搜索缓存 + 限流(防服务器 IP 被封)
- **实测确认**:网易云搜索 API **无 CORS 头**(返回 text/plain),**无法下放到浏览器**,必须后端代理。
- **对策**:`routes/music.js` 的 `/search` 加 ①内存缓存(同关键词 TTL 5 分钟内直接返回,上限 200 条 LRU);②按 IP 限流(默认 60 秒 20 次,超限 429)。命中缓存不消耗限流额度。
### 18.3 封面网络化(远程优先 + 本地回退)
- **实测确认**:网易云图片 CDN 返回 `Access-Control-Allow-Origin: *` 且不校验 Referer,浏览器可直接加载。
- **改动**:`enrichSong` 下载封面时**同时**存远程 URL(`cover_remote`)和本地(`cover`);API 返回 `albumImage`(远程优先)+ `coverLocal`(本地兜底)。
- **前端**:`coverSrcAttr(item)` 生成 `<img src=远程 data-local=本地 onerror=回退>`;mini player / 收听页封面同样 onerror 回退。
### 18.4 刷新优化
- init 时统一获取角色;SSE 事件保持增量刷新(`loadPlaylist`+`refreshLive` 节流)。
### 18.5 验证
- 回归:**api 16/16、e2e 17/17、write 5/5** 全绿。
- 新增功能手测(curl):
- admin 登录 → `/api/system/cache` 200、`/logs` 200;user 登录 → 同接口 **403**,`/api/music/import` **403**,`/api/resources` 200、`/api/streams` 200。
- `/api/auth/me` 正确返回 role。
- 搜索同关键词第二次返回 `cached:true`。
### 18.6 启动方式
- `start.cmd` 更新为:`set "ADMIN_PASSWORD=test123"` + `set "USER_PASSWORD=user123"` + `node index.js`。
- 托管启动:`cmd /c "set ADMIN_PASSWORD=test123&& set USER_PASSWORD=user123&& node index.js"`(注意用 `&&` 且不加尾空格,否则密码带空格)。
### 18.7 未做
- **播放延迟调节**(HLS_TIME 运行时热更新):老板明确暂缓。
- 用户密码/管理员密码的 UI 设置界面(现走环境变量)。
- 会话表已有 role 列,但登录失败锁定的 failMap 是内存态,重启清零(可接受)。
---
## 19. v2.6 一键刷新缺失封面(已完成 2026-09-25)
### 19.1 需求
设置页加「刷新缺失封面」按钮:为所有没有封面的音乐重新匹配网易云封面。
### 19.2 实现
- **后端**:`routes/music.js` 新增 `POST /api/music/refresh-covers`(`requireAdmin`)。遍历 `cover` 和 `cover_remote` **都为空**的歌曲,逐首调 `enrichSong` 重匹配,返回 `{total, success, fail, details}`。
- **HTML**:`index.html` 设置页新增「封面维护」卡片(`#btnRefreshCovers` + `#coverRefreshProgress`),位于缓存管理和运行配置之间。
- **前端**:`app.js` 绑定按钮 → confirm 确认 → POST → 显示「共 N 首,成功 X,失败 Y」→ 刷新资源列表 + 收听页封面。
### 19.3 验证
- admin 调用 → `{total:0}`(无缺失时正常);user 调用 → **403**。
- **真实场景**:手动清空 1 首歌封面 → 调接口返回 `{total:1, success:1}` → 复查无封面歌曲数归 0 ✅。
### 19.4 工具坑(重要教训)
- **powershell Select-String 行号 ≠ replace_lines 行号**(差约 2 行,因换行/编码处理不同),导致多段 replace_lines 反复错位、文件被搞乱。
- **正解**:用 node 写查找小工具(`fs.readFileSync(f,"utf8").split("\n").forEach`),行号与 replace_lines 完全一致。改前必查、改后必 `node --check`。
- 文件搞乱时:`git checkout <commit> -- <file>` 从最近提交恢复,比手工修快且稳。
- 后台服务:`svc003`(`http://127.0.0.1:3000`)
- DB:`data.db`(6 首歌曲,封面已本地化)
- 缓存:`hls_cache/` 约 34MB
- Git:`2fc2548` 干净