# 交接文档 · 音乐在线电台 v2 > 本文件用于跨会话交接。上下文耗尽后,新会话**先读此文件**再动手。 --- ## 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`(收尾) --- ## 1. 运行 / 环境 - 环境:Windows 11,Node v24.5.0,npm 11.5.1,FFmpeg 9.0.1(含 ffprobe,均在 PATH),Python 3.11 - 启动: ```cmd cd /d F:\music_online\demo set PANEL_PASSWORD=test123 && node index.js ``` - 依赖:`better-sqlite3`、`multer`、`chokidar`、`express`(已装) - 登录:浏览器开 `http://127.0.0.1:3000`,输入密码 --- ## 2. 目录结构(v2) ``` demo/ ├── index.js # 入口,仅 require("./src/server").start() ├── package.json # v2.0.0 ├── data.db # SQLite(自动生成,WAL) ├── HANDOFF.md # 本文件 ├── README.md # v2 说明 ├── 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/* (缓存/日志/配置/设置) ├── 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 # 测试脚本 ``` --- ## 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 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, created_at, expires_at) ``` **关键**:切片缓存 `hls_cache/{md5(文件名)}/` 与「流」无关,多路共享,省空间。 --- ## 4. API 清单 除 `/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 鉴权) | **统一返回**:`{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 数据**:用户明确说旧数据不要。 3. **鉴权选"中档"**:整站 Cookie 会话登录,面板+API 锁,**HLS 流不锁**(播放器难带 cookie)。 4. **封面/资源全本地化**:`public/covers/` 存封面,`public/vendor/` 存前端库;系统字体替代 Google Fonts。 5. **songs.file 存文件名**:不是绝对路径,迁移目录不失效。 6. **导入先落 tmp 再 rename**:避免 chokidar 扫到半截文件。 --- ## 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 成功) ``` 上次全绿:**38/38**。但**注意**:这些只测了 HTTP 层,**浏览器端 JS 交互没自动化测过**。 --- ## 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 --- ## 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` --- ## 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` 即可。 --- ## 11. 当前运行态 - 后台服务:`svc005`(PID 15308 起),日志 `service_log svc005` - DB:`data.db`(5 首歌曲,封面已本地化) - 缓存:`hls_cache/` 5 个目录,约 28MB - Git 工作区:干净(最新 `a4745db`) --- ## 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 的 `