| # 目录结构 |
|
|
| ## 根目录 |
|
|
| ``` |
| music-together/ |
| ├── packages/ |
| │ ├── client/ # React 前端 |
| │ ├── server/ # Node.js 后端 |
| │ └── shared/ # 共享类型与常量 |
| ├── docs/ # 项目文档(含本文件 PROJECT_ARCHITECTURE.md) |
| ├── package.json # 根 package(工作区编排) |
| ├── pnpm-workspace.yaml # pnpm 工作区定义 |
| ├── pnpm-lock.yaml |
| ├── README.md |
| └── .gitignore |
| ``` |
|
|
| ## packages/client/src/ — 前端源码 |
|
|
| ``` |
| src/ |
| ├── main.tsx # 入口:ReactDOM.createRoot |
| ├── App.tsx # 根组件:Router + Provider + ErrorBoundary + Suspense 懒加载 |
| ├── index.css # 全局样式:Tailwind + 配色变量 + 自定义动画 |
| │ |
| ├── pages/ # 页面级组件 |
| │ ├── HomePage.tsx # 大厅:创建/加入房间、房间列表 |
| │ ├── RoomPage.tsx # 房间:播放器 + 聊天(桌面侧栏/移动端 Drawer) + 覆盖层弹窗 |
| │ └── NotFoundPage.tsx # 404 页面 |
| │ |
| ├── components/ # UI 组件 |
| │ ├── Chat/ |
| │ │ ├── ChatMessage.tsx # 单条消息(用户/系统) |
| │ │ └── ChatPanel.tsx # 聊天面板(消息列表 + 输入框) |
| │ ├── Lobby/ |
| │ │ ├── CreateRoomDialog.tsx # 创建房间弹窗 |
| │ │ ├── NicknameDialog.tsx # 设置昵称弹窗 |
| │ │ ├── PasswordDialog.tsx # 输入房间密码弹窗 |
| │ │ ├── RoomCard.tsx # 房间列表卡片 |
| │ │ ├── UserPopover.tsx # 用户信息气泡 |
| │ │ ├── HeroSection.tsx # 首页 Hero 标题区域 |
| │ │ ├── ActionCards.tsx # 创建/加入房间卡片 |
| │ │ └── RoomListSection.tsx # 活跃房间列表区域 |
| │ ├── TrackListItem.tsx # 共享曲目行渲染(序号+封面+标题VIP+歌手可点击+时长+isAdded 添加按钮,memo 优化) |
| │ ├── VirtualTrackList.tsx # 共享虚拟滚动曲目列表(@tanstack/react-virtual + 无限加载 + skeleton + 空态,forwardRef 暴露 scrollToTop) |
| │ ├── Overlays/ |
| │ │ ├── QueueDrawer.tsx # 播放队列抽屉(vaul Drawer,移动端底部/桌面端右侧) |
| │ │ ├── SearchDialog.tsx # 音乐搜索弹窗(VirtualTrackList 虚拟滚动 + 自动无限加载 + AbortController 竞态防护) |
| │ │ ├── SettingsDialog.tsx # 设置弹窗(壳,Tab 导航:房间/成员/账号/个人/外观,移动端 nav scrollbar-hide) |
| │ │ └── Settings/ |
| │ │ ├── SettingRow.tsx # 设置行共享组件 |
| │ │ ├── RoomSettingsSection.tsx # 房间设置(名称、密码) |
| │ │ ├── MembersSection.tsx # 成员列表(角色管理) |
| │ │ ├── PlatformAuthSection.tsx # 平台账号认证(VIP Cookie,旧版,已被 PlatformHub 替代) |
| │ │ ├── PlatformHub.tsx # 平台中心(登录 + 歌单浏览 + 导入,替代 PlatformAuthSection) |
| │ │ ├── LoginSection.tsx # 精简版平台登录区域(PlatformHub 子组件) |
| │ │ ├── PlaylistSection.tsx # 歌单列表 + 手动输入(PlatformHub 子组件) |
| │ │ ├── PlaylistDetail.tsx # 歌单详情(header + VirtualTrackList + queueKeys/addedIds 去重 + 动态全部添加过滤重复) |
| │ │ ├── ProfileSettingsSection.tsx # 个人设置(昵称) |
| │ │ ├── AppearanceSection.tsx # 外观设置(歌词 + 背景 + 布局) |
| │ │ ├── OtherSettingsSection.tsx # 其他设置 |
| │ │ ├── ManualCookieDialog.tsx # 手动输入 Cookie 弹窗 |
| │ │ └── QrLoginDialog.tsx # 通用 QR 扫码登录弹窗(网易云 + 酷狗 + QQ 音乐) |
| │ ├── Player/ |
| │ │ ├── constants.ts # 共享动画常量(SPRING / LAYOUT_TRANSITION),NowPlaying 和 SongInfoBar 统一导入 |
| │ │ ├── AudioPlayer.tsx # 主播放器布局(桌面:左右分栏;移动:双模式封面/歌词切换) |
| │ │ ├── LyricDisplay.tsx # AMLL 歌词渲染(LRC 正则支持 [mm:ss] / [mm:ss.x] / [mm:ss.xx] / [mm:ss.xxx]) |
| │ │ ├── NowPlaying.tsx # 当前曲目展示(支持 compact 小封面横排模式 + layoutId 共享动画) |
| │ │ ├── SongInfoBar.tsx # 歌曲信息栏(标题/艺术家 + 音量/聊天按钮,竖屏模式自适应缩放) |
| │ │ └── PlayerControls.tsx # 进度条+播放控制+播放模式切换 |
| │ ├── Room/ |
| │ │ └── RoomHeader.tsx # 房间头部(房间名/人数/连接状态;移动端 DropdownMenu 收纳设置/离开等操作) |
| │ ├── Vote/ |
| │ │ └── VoteBanner.tsx # 投票横幅(进行中的投票显示 + 投票按钮) |
| │ ├── InteractionGate.tsx # 浏览器交互解锁(点击后才能播放音频) |
| │ └── ui/ # shadcn/ui 基础组件 |
| │ ├── avatar.tsx |
| │ ├── badge.tsx |
| │ ├── button.tsx |
| │ ├── card.tsx |
| │ ├── dialog.tsx |
| │ ├── drawer.tsx |
| │ ├── dropdown-menu.tsx |
| │ ├── input.tsx |
| │ ├── label.tsx |
| │ ├── popover.tsx |
| │ ├── resize-handle.tsx |
| │ ├── scroll-area.tsx |
| │ ├── select.tsx |
| │ ├── separator.tsx |
| │ ├── marquee-text.tsx |
| │ ├── responsive-dialog.tsx |
| │ ├── sheet.tsx |
| │ ├── skeleton.tsx |
| │ ├── slider.tsx |
| │ ├── switch.tsx |
| │ ├── tabs.tsx |
| │ └── tooltip.tsx |
| │ |
| ├── hooks/ # 自定义 Hooks |
| │ ├── useSocketEvent.ts # 通用 Socket 事件订阅工具 Hook(自动 on/off,ref 稳定) |
| │ ├── usePlayer.ts # 播放器主 hook(组合 useHowl + useLyric + usePlayerSync) |
| │ ├── useHowl.ts # Howler.js 音频实例管理 |
| │ ├── useLyric.ts # 歌词加载(TTML → 平台逐词 YRC/KRC → LRC) |
| │ ├── usePlayerSync.ts # 播放同步(Scheduled Execution + conductor 上报 + 周期性漂移校正) |
| │ ├── useClockSync.ts # NTP 时钟同步 hook(校准客户端时钟与服务器对齐) |
| │ ├── useRoom.ts # 房间组合 hook(编排 5 个子 hook,对外 API 不变) |
| │ ├── room/ # useRoom 子 hook(按职责拆分) |
| │ │ ├── useRoomState.ts # ROOM_STATE / JOIN / LEFT / SETTINGS / ROLE_CHANGED / ERROR + 挂载时补发 cookie(覆盖 HomePage 提前消费 ROOM_STATE 的场景) |
| │ │ ├── useChatSync.ts # CHAT_HISTORY / CHAT_MESSAGE |
| │ │ ├── useQueueSync.ts # QUEUE_UPDATED |
| │ │ ├── useAuthSync.ts # AUTH_SET_COOKIE_RESULT + localStorage 持久化(验证失败只做 toast 反馈,永不删除 cookie;删除权仅在 useAuth.logout) |
| │ │ └── useConnectionGuard.ts # disconnect → resetAllRoomState |
| │ ├── useAuth.ts # 平台认证 UI & Socket 事件 |
| │ ├── useVote.ts # 投票(使用 useSocketEvent) |
| │ ├── useChat.ts # 聊天消息收发 |
| │ ├── useLobby.ts # 大厅房间列表与操作(使用 useSocketEvent) |
| │ ├── useQueue.ts # 播放队列操作(含 addBatchTracks 批量添加) |
| │ ├── usePlaylist.ts # 歌单管理(用户歌单列表、分页曲目获取 + 无限加载、URL 解析、批量导入) |
| │ ├── useIsMobile.ts # 布局维度:orientation 检测(portrait=竖屏布局,landscape=横屏布局) |
| │ ├── useHasHover.ts # 交互维度:hover 能力检测((hover: hover) 媒体查询,触控设备=false) |
| │ ├── useContainerPortrait.ts # 容器宽高比检测(ResizeObserver,用于播放器横竖屏切换) |
| │ └── useCoverWidth.ts # 封面容器最小尺寸测量(约束信息栏/控件宽度与封面对齐) |
| │ |
| ├── stores/ # Zustand 状态仓库 |
| │ ├── playerStore.ts # 播放状态(currentTrack, isPlaying, volume 等) |
| │ ├── roomStore.ts # 房间状态(room, currentUser, users) |
| │ ├── chatStore.ts # 聊天(messages, unreadCount, isChatOpen) |
| │ ├── lobbyStore.ts # 大厅(rooms 列表, isLoading) |
| │ └── settingsStore.ts # 设置(歌词参数、背景参数,持久化到 localStorage) |
| │ |
| ├── providers/ # React Context Provider |
| │ ├── SocketProvider.tsx # Socket.IO 连接管理,提供 socket + isConnected + 断线/重连 Toast |
| │ └── AbilityProvider.tsx # CASL 权限上下文(基于 currentUser.role) |
| │ |
| └── lib/ # 工具库 |
| ├── config.ts # 配置常量(SERVER_URL) |
| ├── constants.ts # 命名常量(定时器、阈值、布局尺寸) |
| ├── clockSync.ts # NTP 时钟同步引擎(采样、offset 计算、getServerTime) |
| ├── resetStores.ts # 全局 store 重置工具 |
| ├── socket.ts # Socket.IO 客户端实例 |
| ├── storage.ts # localStorage 封装(带类型校验) |
| ├── platform.ts # 平台常量(PLATFORM_LABELS / PLATFORM_SHORT_LABELS / PLATFORM_COLORS / VIP_LABELS / 状态查找函数) |
| ├── format.ts # 格式化工具(时间、文本等) |
| ├── audioUnlock.ts # 浏览器音频自动播放解锁 |
| └── utils.ts # cn() + trackKey() 等通用工具 |
| ``` |
|
|
| ## packages/server/src/ — 后端源码 |
|
|
| ``` |
| src/ |
| ├── index.ts # 入口:Express + HTTP + Socket.IO 服务启动与优雅关闭 |
| ├── config.ts # 环境变量配置(PORT, CLIENT_URL, CORS) |
| │ |
| ├── controllers/ # 控制器:注册 Socket 事件处理器(薄编排层,不含业务逻辑) |
| │ ├── index.ts # 统一注册入口 |
| │ ├── roomController.ts # 房间生命周期(创建/加入/离开/发现/设置/角色) |
| │ ├── playerController.ts # 播放控制(play/pause/seek/next/prev/sync/set_mode)+ NTP ping/pong |
| │ ├── queueController.ts # 队列管理(add/remove/reorder/clear) |
| │ ├── chatController.ts # 聊天消息(含限流反馈) |
| │ ├── voteController.ts # 投票系统(发起/投票/超时/执行,支持 set-mode / play-track / remove-track 投票) |
| │ ├── authController.ts # 平台认证(QR 登录/Cookie 管理/状态查询;支持网易云/酷狗/QQ 音乐三平台;策略模式——通过 AUTH_PROVIDERS 映射表统一处理;fast path: 内存池命中跳过 API;slow path: getUserInfo + 任意失败重试 1 次) |
| │ └── playlistController.ts # 歌单管理(获取用户歌单列表 via Socket,使用 getUserCookie 取请求者自己的 cookie,歌单私有) |
| │ |
| ├── services/ # 服务层:业务逻辑 |
| │ ├── roomService.ts # 房间 CRUD + 角色管理 + 加入校验(validateJoinRequest) |
| │ ├── roomLifecycleService.ts # 房间生命周期定时器(空置删除)+ 防抖广播 |
| │ ├── playerService.ts # 播放状态管理 + 流 URL 解析 + 切歌防抖 + 加入播放同步 |
| │ ├── queueService.ts # 队列操作(reorder 保留未包含曲目防丢歌,getNextTrack 支持 4 种播放模式,clearQueue 清空,addBatchTracks 批量添加) |
| │ ├── chatService.ts # 聊天消息处理 + HTML 转义(含系统消息) |
| │ ├── syncService.ts # 播放位置估算工具(estimateCurrentTime) |
| │ ├── musicProvider.ts # 音乐数据聚合(3 层引用式 LRU 缓存 + 外部 API 超时保护 + 歌单分页获取;Netease 歌单使用 ncmApi.playlist_track_all 分块请求突破 1000 首限制,Kugou 用户歌单使用原生 API (get_other_list_file_nofilt) + Meting fallback,Tencent 使用 Meting 原始模式保留 VIP/时长字段) |
| │ ├── authService.ts # Cookie 池管理(房间级作用域;getAnyCookie 用于 VIP 播放共享,getUserCookie 用于歌单等用户私有操作) |
| │ ├── authProvider.ts # 统一认证接口(AuthProvider 接口定义 + GetUserInfoResult/UserInfoData 共享类型 + AUTH_PROVIDERS 策略映射表) |
| │ ├── neteaseAuthService.ts # 网易云 API 认证(QR / Cookie 验证 / 用户信息 / 用户歌单列表;getUserInfo 返回 { ok, data? } | { ok: false, reason: 'expired' | 'error' } 区分过期与临时故障) |
| │ ├── kugouAuthService.ts # 酷狗 API 认证(QR 扫码登录 + VIP 检查 + 用户昵称(RSA) + 用户歌单列表 + 歌单歌曲获取;kugouRequest 含 HTTP 状态检查与 JSON 安全解析;自包含签名实现,状态码归一化为 800-803 与网易云统一) |
| │ ├── tencentAuthService.ts # QQ 音乐认证(5 步 OAuth QR 扫码登录:ptqrshow/ptqrlogin/check_sig/authorize/QQLogin 换取 musickey;zzc 签名防风控;getUserInfo 获取昵称 + VIP 状态;getUserPlaylists 获取自建 + 收藏歌单;getPlaylistTracks 分页获取歌单歌曲) |
| │ └── voteService.ts # 投票状态管理 |
| │ |
| ├── repositories/ # 数据仓库:内存状态与 SQLite 持久化 |
| │ ├── types.ts # 接口定义(RoomRepository, ChatRepository) |
| │ ├── roomRepository.ts # 房间数据 + Socket 映射 + per-socket RTT + roomToSockets 反向索引(Map<string, RoomData>) |
| │ ├── chatRepository.ts # 聊天记录(内存最多 200 条;永久房间同步持久化到 SQLite) |
| │ └── audioProxyPolicyRepository.ts # 全局酷狗音频代理策略(SQLite server_settings) |
| │ |
| ├── middleware/ # Socket.IO 中间件 |
| │ ├── types.ts # TypedServer, TypedSocket, HandlerContext |
| │ ├── withRoom.ts # 房间成员身份校验 |
| │ ├── withControl.ts # 操作权限校验(包装 withRoom) |
| │ └── socketRateLimiter.ts # Socket 事件速率限制(per-socket,10次/5秒)+ 断连清理(cleanupSocketRateLimit) |
| │ |
| ├── routes/ # Express REST 路由 |
| │ ├── music.ts # GET /api/music/search|url|lyric|cover|playlist|ttml(统一 validated() 路由包装器消除重复 try/catch + Zod 模式) |
| │ └── rooms.ts # GET /api/rooms/:roomId/check(房间预检) |
| │ |
| ├── types/ |
| │ └── meting.d.ts # @meting/core 类型声明 |
| │ |
| └── utils/ |
| ├── logger.ts # 结构化日志(基于 pino,info/warn/error + JSON context) |
| └── roomUtils.ts # 房间数据转换纯函数(toPublicRoomState / toPublicRoomStateForOwner,密码仅 owner 可见) |
| ``` |
|
|
| ## packages/shared/src/ — 共享代码 |
|
|
| ``` |
| src/ |
| ├── index.ts # 统一导出(re-export 所有模块) |
| ├── types.ts # 核心类型:ERROR_CODE, Track, RoomState, PlayState, ScheduledPlayState, PlayMode, AudioQuality, User, ChatMessage, VoteAction (incl. play-track, remove-track), VoteState, RoomListItem, Playlist |
| ├── events.ts # 事件常量:EVENTS 对象(room:*, player:*, queue:*, chat:*, auth:*, ntp:*, playlist:*) |
| ├── socket-types.ts # Socket.IO 类型:ServerToClientEvents, ClientToServerEvents |
| ├── constants.ts # 业务常量:LIMITS(长度/数量限制), TIMING(同步间隔/宽限期), NTP(时钟同步参数), QR_STATUS(扫码状态码), QR_TIMING(轮询间隔) |
| ├── schemas.ts # Zod 验证 schema |
| └── abilities.ts # CASL 权限定义(Actions incl. set-mode, Subjects, defineAbilityFor) |
| ``` |
|
|
| --- |
|
|