ECHO Next 开发规则
参与 ECHO Next 开发?这些规则是边界和护栏。新功能、重构、插件接口、构建脚本和文档说明都应遵守。
安全边界、架构分层、元数据优先级、封面规则和测试要求。改代码前过一遍,能少踩很多坑。
核心开发规则
Section titled “核心开发规则”- 安全稳定优先。任何会影响用户本地音乐文件、播放链路、授权状态、数据库迁移、自动更新或远程服务的改动,都必须先说明风险,并优先选择可回退、可验证的实现。
- ECHO 是本地音乐播放器和合法扩展平台,不是破解、绕过授权、规避付费、绕过平台访问控制或获取侵权内容的工具。
- 明确禁止任何破解行为。ECHO 官方代码、插件接口、文档、示例、脚本、构建产物和支持流程不得提供、暗示、协助或鼓励破解软件、破解服务、破解音源、绕过 DRM、绕过账号/会员/地区/付费限制、伪造授权、篡改激活状态、移除水印、逆向第三方保护机制或分发侵权内容。
- 任何第三方来源、插件、脚本或用户自备 URL 都必须只处理用户有权访问和使用的内容。插件作者和集成者不得把 ECHO 接口包装成下载、盗链、破解或规避平台规则的能力。
- 授权和 ECHO Pro 相关逻辑必须保持主机/服务端签名链路为最终可信来源。前端和插件只能展示状态、发起请求或携带证明,不能成为授权真相源,也不能加入绕过校验的后门。
- 开发中发现可能被用于破解、侵权或绕过限制的需求时,应明确拒绝实现该用途,并把设计收敛到合法、本地、用户自有内容和公开可验证接口范围内。
文件大小与归属
Section titled “文件大小与归属”- 不要巨型
App.tsx。 - 不要巨型
main/index.ts。 - 不要巨型全局 CSS 文件。
- 超过 500 行的页面必须拆分。
- 超过 800 行的 service 必须拆分。
- 共享抽象必须有明确的 owner 和用途。
src/renderer/app/App.tsx 只能组合:
- providers
- layout
- routes
- 未来的 error boundary
src/main/index.ts 只能组合:
- app lifecycle
- 通过 lifecycle 创建主窗口
- IPC 注册
- 必要的服务 bootstrap
Renderer 规则
Section titled “Renderer 规则”Renderer 不得:
- 扫文件夹
- 读 metadata
- 解析封面
- 为列表加载完整封面
- 决定专辑分组
- 在 React state 里持有全库
- 对全量内存曲目数组做重搜索
- 让高频播放状态重渲染整个应用
- 知道 library worker 是 TypeScript、Rust 还是 C++
歌曲、专辑、艺术家和搜索结果必须分页或虚拟化。
当前 Phase 1 列表默认值:
- songs:
pageSize = 100 - albums:
pageSize = 60 - track 行虚拟化,估计行高 70px
- 列表和专辑墙图片必须懒加载、异步解码
- AlbumsPage 必须先请求第 1 页,只在接近滚动底部时追加;不能一开始循环拉完所有专辑页
- AlbumWall 在 Phase 1.2 可以保持分页 + 懒图片;只有大曲库烟测证明需要时,才加网格虚拟化
Preload 规则
Section titled “Preload 规则”Preload 必须:
- 暴露
window.echo - API 按域分组
- 返回 typed 结果
Preload 不得:
- 暴露 raw
ipcRenderer - 直接访问文件
- 实现业务逻辑
- 解析 metadata 或封面
- 知道 Library Core 背后是哪个 worker 实现
Renderer 不得直接打开 Electron 对话框。选文件夹的 UX 必须走 preload 和 IPC,不能从 React 组件调 dialog。
Renderer EQ UI 可以渲染控件、曲线、警告和预设操作。它不得处理音频 buffer、计算原生滤波系数、直接读写预设文件,或绕过 typed window.echo.eq preload API。
Native Worker 边界
Section titled “Native Worker 边界”Library Core 重活必须通过稳定接口调用:
MetadataReaderCoverExtractorFileScanner
LibraryService 可以组合具体默认实现,但编排必须依赖接口。IPC 和 Renderer 不得 import TsMetadataReader、TsCoverExtractor 或 TsFileScanner。
未来的 Go/C#/Rust worker 必须保持相同返回结构:
- metadata 字段、field sources、warnings、errors 和 status
- cover source、thumb path、album path、large path、original reference、source hash、warnings 和 errors
- scanned file path、size 和 mtime
换 worker 实现时,SQLite schema、IPC payload 和 Renderer 列表视图不得改变。
Metadata 优先级
Section titled “Metadata 优先级”Metadata 优先级固定:
- user manual edit
- embedded tags
- sidecar/info files
- folder structure
- network completion
- filename fallback
文件名猜测永远不能覆盖嵌入的 title、artist 或 album。
网络 metadata 永远不能覆盖嵌入标签。
embedded_metadata_status 为 pending 或 reading 时,网络 metadata 不得写字段。只有嵌入 metadata 为 missing 或 error,且当前字段来源为 unknown、filename_fallback 或 network 时,才可以 missing-only 应用。
每条存储的 track 必须在 field_sources_json 里保留每字段来源信息。
Phase 1 至少持久化:
titleartistalbumalbumArtisttrackNodiscNoyeardurationcodecsampleRatebitDepthbitrate
长期封面优先级固定:
- user manual cover
- embedded cover
- local folder cover
- sidecar cover
- network cover
- generated placeholder
网络封面永远不能覆盖 manual、embedded 或 local 封面。
网络封面查找是手动的、弱的。embedded_cover_status 为 pending 或 reading 时不得写封面;只有当前封面来源为 default 时才可以应用。
当前 TS+sharp v0.2 封面必须存为:
thumb.webp96x96,给LibraryTrack.coverThumbalbum.webp320x320,给LibraryAlbum.coverThumblarge.webp最大 768x768,给 NowPlaying/详情- original
sharp 做真正的 resize。TypeScript 管封面优先级、缓存目录调度和兜底行为。
列表视图只用 track thumb。专辑墙只用 album thumb。完整封面在列表滚动外按需加载。
列表 API 不得返回 cover_large、cover_original、largePath、originalRef、原始 binary 封面数据或 base64 封面 payload。
在 benchmark 或烟测数据证明 TS+sharp 不够之前,不要启动 Go/C#/Rust CoverWorker。决策信号包括:生成 1000 张专辑缩略图时 CPU 持续高于 50%、3000/10000 封面时内存峰值不可接受、Electron sharp 打包/rebuild 不稳定、或衍生图已存在后封面缓存命中仍然慢。
所有长任务必须:
- 后台运行
- 可取消
- 可报进度
- 可收集错误
包括扫描、metadata 提取、封面生成、音频分析和未来的网络 enrichment。
网络 enrichment 不得在应用启动时自动跑,不得为每条扫描曲目发请求,必须使用 provider 超时,并发保持在 2 或以下,provider 失败不得影响本地曲库行。
本地曲库扫描在 path + size_bytes + mtime_ms 未变时必须跳过 metadata 解析。
扫描任务必须报告以下阶段之一:
discoveringchecking_cachereading_metadataextracting_coversgrouping_albumswriting_databasefinishedfailedcancelled
单文件 metadata 或封面错误必须收集,但不能让整个扫描失败。
Metadata 和 cover worker 必须有并发限制。封面缩略图必须在扫描时创建,不能在列表滚动时创建。
扫描后 SQLite 是唯一事实来源。重启应用不得重新解析全库。
桌面 dev 跑之前,better-sqlite3 必须为 Electron 运行时 ABI rebuild。npm run dev 通过 npm run rebuild:native 负责这个检查;在 Electron 里测文件夹导入或曲库扫描时,不要依赖为系统 Node.js ABI 编译的二进制。Vitest 用系统 Node.js ABI,所以 Vitest 全局 setup 负责相反的 rebuild——即使用 vitest、编辑器或 npm test 直接启动测试也一样。scripts/ensure-native-abi.mjs 在 node_modules/.echo-native-cache 下缓存 ABI 专用二进制,让 Node/Electron 切换更快。
必须持久化的表:
folderstracksalbumsalbum_tracksartistscoversscan_jobs
专辑墙视图必须读 albums 表。不得在 renderer 里对全量 track 表重新分组。
文件从已扫描文件夹移除后,下次扫描必须在列表 API 里隐藏它,但不能动磁盘上的文件。
当前 v0.1 策略:缺失文件标 missing = 1 并从列表 API 过滤。这样保留缓存历史,又不删用户磁盘上的文件。
专辑分组必须在 Library Core 里做并持久化。
规则:
- 同 album + 同 album artist → 合并
- 同 album + 不同 album artist → 不合并
- album artist 缺失或 unknown 时,用文件夹路径做弱分隔
- 空或 unknown 的 album 值不得塌成一张巨型专辑
- 有 year 时 year 参与 album key
改动涉及 metadata、封面、音频、曲库、编码、数据库迁移或文件扫描行为时,必须包含针对性测试。
Library Core 测试优先用真实 SQLite 和 mock metadata reader,除非解析器集成 bug 明确需要真实媒体文件。
碰 Library Core 的测试必须用假的 MetadataReader、CoverExtractor 和 FileScanner 实现覆盖 worker 边界,保持架构对 Rust/C++ 就绪。
文件夹导入 UX 必须保持 library.chooseFolder() 在 main/preload,重复导入视为幂等重扫,导入或扫描完成后通过共享 library:changed 事件刷新 SongsPage / AlbumsPage。侧边栏导入项是直接动作:Import Folder 打开文件夹选择器而不是导航,Import File 打开本地音频文件选择器,不向 Renderer 暴露 Electron 对话框。
SongsPage 必须保持列表视图,不是导入向导。它的文件夹加号按钮可以通过轻量 app:navigate:import-folder 事件导航到 ImportFolderPage;FoldersPage、ImportFolderPage 和 Settings 复用 LibraryFoldersPanel。
TrackRow 可以通过从 SongsPage 传下来的回调启动单曲本地播放。SongsPage 可以存 currentTrackId,但高频播放位置和音频状态必须留在 App.tsx 外,不得重渲染歌曲列表。
当前播放队列只是 SongsPage 可见/已加载窗口。在 LibraryService 支撑的队列服务存在之前,不要扩成完整播放队列。
PlayerBar 轮询是临时的。未来播放/音频状态应走节流的 IPC 推送事件,如 playback:onStatus 和 audio:onStatus;位置更新不得重渲染 SongsPage 或 TrackList。
曲库诊断仅 dev 可用。必须用 library.getDiagnostics(),不得触发扫描,不得返回全量 track 列表、全量封面记录、binary 封面数据或 base64 封面数据。
EQ 改动必须保持音频线程边界。预设 JSON 存储属于 main/native 非实时代码,不属于 JUCE 回调。原生 EQ 参数必须通过 atomic 或 lock-free 状态传递,使用前平滑,禁用/bypass 后 bypass fade 完成时必须保持输出 bit-transparent。