# 目录结构 ## 根目录 ``` 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) │ ├── 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) ``` ---