故障排除
本文档整理自后端源码(internal/、pkg/tag/)、AGENTS.md 业务踩坑总结、FAQ 文档,以及各模块的错误定义与配置逻辑。
目录
扫描问题
章节来源:internal/services/scanner.go、internal/services/scan_progress.go、internal/services/auto_scan.go、AGENTS.md 业务踩坑总结
文件找不到 / 扫描结果为空
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 扫描后歌曲数为 0 | 音乐目录路径未配置或不正确 | 通过 PUT /api/v1/settings/music-path 设置正确的绝对路径 |
| 部分文件未出现 | 文件格式不在支持列表中 | 检查 scan_config 中的 SupportedFormats 配置 |
| 子目录文件缺失 | 目录被排除规则命中 | 检查 ExcludeDirs(按名称匹配)和 ExcludePaths(精确路径匹配) |
| Docker 环境下文件不可见 | 卷挂载路径不正确 | 使用绝对路径挂载:-v /absolute/path/to/music:/app/music |
元数据提取失败
扫描依赖 pkg/tag 纯 Go 库提取音频元数据(无外部依赖)。可选安装 ffprobe 以获取更精确的技术参数(时长、比特率、采样率)。Docker 镜像中已包含 ffprobe。
常见提取失败场景:
- 文件损坏或不完整:下载中断、传输错误导致的截断文件
- 非标准 tag 格式:某些软件写入的非标准 ID3v2 帧可能无法解析
- 嵌入封面过大:超大封面图片可能导致内存占用显著增加
支持的音频格式
默认支持:MP3、FLAC、WAV、APE、OGG、M4A、WMA、AIF/AIFF。可通过数据库配置 scan_config 的 SupportedFormats 字段自定义。
标题规则踩坑
扫描标题提取遵循以下规则:
- tag 中有
title字段 --> 直接使用tag.Title - tag 中无
title字段 --> 使用文件名去掉扩展名
注意:不会做"最长公共子串去重 + 拼接"处理。历史版本曾实现此逻辑,导致产生类似"艺术家 - 标题"的冗余标题,将艺术家信息错误混入标题字段。
播放问题
章节来源:internal/services/source/errors.go、internal/services/source/validator.go、AGENTS.md HLS 电台代理模式
远程歌曲 URL 过期
远程歌曲的播放 URL 可能因源站 token 过期而失效。系统通过源编排(Orchestrator)机制自动处理:
- 主源 URL 返回网络错误(
NetworkError)或下载文件校验失败(InvalidAudioError) - 系统判定错误是否可 fallback(
IsFallbackable函数) - 若可 fallback,Resolver 跨插件 fan-out 搜索备选音源
- 所有候选源都失败时,返回终态
AllSourcesFailedError
可 fallback 的错误类型:
InvalidAudioError:文件下载成功但未通过完整性校验(时长过短、码率过低、时长与预期不符)NetworkError:HTTP 层失败(DNS/连接/超时/非 2xx 状态码)PluginInvocationError:插件music/url接口调用失败
HLS 电台无法播放
| 症状 | 原因 | 解决方案 |
|---|---|---|
| 电台播放直接失败 | 源站 Referer/UA 防盗链 | 开启 HLS 代理模式 |
| Web 嵌入模式电台不播 | CORS 限制阻塞 | 开启 HLS 代理模式 |
| 播放器选错 MediaSource | URL 缺少 .m3u8 后缀 | 系统已强制 HLS 电台 URL 带 .m3u8 后缀 |
开启 HLS 代理模式:
curl -X PUT http://localhost:58091/api/v1/settings/hls-proxy \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'代理模式下所有切片通过服务端中转,注意带宽成本。默认关闭(false),仅在源站防盗链或 CORS 导致播放失败时开启。
音频校验失败
源编排的 Validate 函数在下载后校验文件完整性,以下情况会触发校验失败并尝试 fallback:
| 校验原因 | 含义 | 默认阈值 |
|---|---|---|
probe_failed | ffprobe / tag 无法读取文件 | -- |
too_short | 实测时长低于绝对下限 | 30 秒 |
duration_mismatch_low | 实测时长 < 预期 x 容忍比 | 预期 x 0.85 |
duration_mismatch_high | 实测时长 > 预期 x 上限 | 预期 x 1.5 |
bitrate_too_low | 平均码率过低 | 8 kbps |
可通过 source_validation 配置 key 调整阈值,或设置 enabled: false 灰度关闭校验。
认证问题
章节来源:internal/services/auth_service.go、internal/middleware/auth.go
Token 过期
| Token 类型 | 有效期 | 处理方式 |
|---|---|---|
| Access Token | 7 天 | 使用 Refresh Token 刷新 |
| Refresh Token | 30 天 | 重新登录获取 |
| 插件内部 Token | 100 年(永久) | 进程重启自动重新生成 |
刷新 Token:
curl -X POST http://localhost:58091/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token": "YOUR_REFRESH_TOKEN"}'默认凭证
默认账号密码为 admin / admin。修改方式:
- Docker:环境变量
ADMIN_USERNAME/ADMIN_PASSWORD - 二进制:命令行参数
-password your_password
JWT Secret 丢失恢复
JWT Secret 存储在数据库 configs 表中(key 为 jwt_secret),初始迁移时自动生成 32 字节随机密钥。如果数据库损坏导致 Secret 丢失:
- 启动时
NewAuthService会尝试从数据库读取jwt_secret,失败则服务无法启动 - 恢复方式:从备份恢复数据库,或删除
data/songloft.db重新初始化(会丢失所有用户数据和配置) - Secret 变更后所有已签发的 Token 自动失效,所有客户端需重新登录
插件问题
章节来源:internal/jsplugin/health.go、internal/jsplugin/manager.go、AGENTS.md JS 插件章节
插件加载失败
常见原因:
plugin.json格式错误或缺少必要字段- 文件 SHA256 指纹校验不通过(双层校验:manifest 校验 + 运行时加载校验)
- QuickJS 沙盒初始化失败(
onInit脚本执行错误) - 权限声明缺失:manifest 中的
permissions未声明所需能力(net、storage、fs:music等)
健康检查失败导致自动禁用
插件健康检查器每 60 秒执行一轮检查,直连 VM 探针(绕开 scheduler 串行队列,避免被长 fetch 阻塞导致假阳性):
| 状态 | 含义 | 处理 |
|---|---|---|
healthy | VM 存活且响应正常 | 重置失败计数 |
busy | VM 正在处理长请求(持锁中) | 不计失败;连续 5 轮(约 5 分钟)仍 busy 则升级为 unhealthy |
unhealthy | VM 无法响应或 env 已销毁 | 累计连续 3 次失败后标记为 error 状态并卸载 |
自动恢复机制:被标记为 error 的插件进入指数退避恢复序列:1 分钟 --> 5 分钟 --> 15 分钟 --> 30 分钟 --> 60 分钟。恢复成功后自动切回 active 状态。
手动恢复:通过 POST /api/v1/plugins/{id}/recover 手动触发恢复,会清空退避计数从头开始。
权限不足
插件运行时权限由 manifest 中的 permissions 字段声明,internal/jsplugin 在运行时校验。如果插件尝试未声明的操作(如未声明 net 权限却调用 songloft.net.udpBind),请求会被拒绝。
解决方案:检查插件 plugin.json 中的 permissions 数组,确保包含所需权限。
热更新不生效
插件使用文件指纹(SHA256)检测变更,自动触发热更新。如果更新不生效:
- 确认新版本的文件指纹确实发生了变化
- 检查插件目录写权限
- 查看日志中是否有热更新相关的错误信息
- 手动禁用再启用插件强制重新加载
空闲插件自动卸载
插件空闲超过 10 分钟后自动卸载 VM 释放资源(数据库状态保持 active,下次请求时懒加载恢复)。带有定时器的插件会在定时器触发前 2 分钟提前唤醒。拥有活跃 WebSocket 连接或运行中子进程的插件不会被休眠。
缓存问题
章节来源:AGENTS.md 音乐缓存章节、internal/services/file_move.go
磁盘空间不足
缓存服务使用 LRU 淘汰策略管理磁盘空间:
- 默认上限
max_size为 1 GB,超出时按最后访问时间淘汰最久未使用的文件 - 设置
max_size=0表示不限制缓存大小 - 查看缓存状态:
GET /api/v1/cache-manage/stats - 手动清理:
POST /api/v1/cache-manage/clean - 调整上限:
PUT /api/v1/cache-manage/config
缓存目录不可写
设置自定义缓存目录前,使用 validate-dir 端点预先验证:
curl -X POST http://localhost:58091/api/v1/cache-manage/validate-dir \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"cache_dir": "/mnt/data/music_cache"}'该端点执行三项检查:自动创建目录、可写性测试、返回磁盘剩余空间。
跨设备 rename 错误(EXDEV)
典型场景:临时文件创建在系统 /tmp(tmpfs),目标缓存目录挂载在独立磁盘或 Docker volume,os.Rename 返回 syscall.EXDEV(cross-device link)。
解决方案:系统内部统一使用 internal/services.moveFile(src, dst) 替代裸 os.Rename。该函数先尝试 rename,EXDEV 时自动回退到 copy + remove。
注意:pkg/tag 的原子写入不受此问题影响,因为它使用 os.CreateTemp(dir, ...) 在源文件同目录创建临时文件,rename 一定在同设备内。
数据库问题
章节来源:internal/database/sqlite.go、internal/database/errors.go、AGENTS.md 数据库规范
SQLITE_BUSY 错误
SQLite 并发写入时可能出现 SQLITE_BUSY。系统已通过以下措施缓解:
- WAL 模式:允许读写并发,读不被写阻塞
- busy_timeout(10000):遇到锁时最多等待 10 秒,避免直接
SQLITE_BUSY - 连接池限制:
MaxOpenConns=10、MaxIdleConns=5
如果仍然出现 SQLITE_BUSY,通常是因为跨表写入未正确使用事务:
# 正确做法:使用 RunInTx 获取同一 *sql.Tx 下的 UnitOfWork
db.RunInTx(ctx, func(ctx context.Context, uow *UnitOfWork) error {
// uow.Songs / uow.Playlists 共享同一事务
})
# 错误做法:service 层手动 BeginTx(会导致 SQLITE_BUSY)迁移失败
数据库迁移使用 goose,启动时自动执行 goose.Up。迁移失败的常见原因:
| 原因 | 解决方案 |
|---|---|
| 数据库文件被锁 | 确保没有其他进程在访问同一数据库文件 |
| 迁移 SQL 语法错误 | 检查 internal/database/migrations/ 中对应的 SQL 文件 |
| 磁盘空间不足 | 清理磁盘空间后重启 |
| 手动 ALTER 导致 schema 不一致 | 从备份恢复数据库;禁止手动 ALTER data/songloft.db |
错误语义
仓储层使用统一的哨兵错误:
database.ErrNotFound:记录不存在database.ErrConflict:UNIQUE 约束冲突或写入冲突
service 层应使用 errors.Is 判别并翻译为业务语义。
网络问题
章节来源:internal/services/whitelist.go、AGENTS.md HLS 电台代理模式 / HTTP Proxy 章节
CORS 问题
Web 嵌入模式下播放远程资源可能遇到 CORS 限制。解决方案:
- HLS 电台:开启 HLS 代理模式(
PUT /api/v1/settings/hls-proxy),服务端代理所有请求 - 远程歌曲:缓存服务自动将远程文件缓存到本地,缓存命中后通过本域返回
SSRF 拦截
HLS 代理和相关网络功能内置 SSRF 防护(services.IsHostnameAllowed),采用内网封禁策略:
被阻止的地址:
localhost、*.local域名- 回环地址:
127.0.0.0/8、::1 - 私有地址:
10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、fc00::/7 - 链路本地:
169.254.0.0/16、fe80::/10 - 未指定地址:
0.0.0.0、::
DNS 解析失败时放行,交由后续 HTTP 请求自行报错。
HLS 代理还额外执行同源校验(scheme + host + port 与 song.URL 严格相等),非同源 URL 保持原样不改写,避免成为开放代理。
HTTP Proxy 配置
配置后端外发请求通过 HTTP 代理转发:
curl -X PUT http://localhost:58091/api/v1/settings/http-proxy \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"proxy": "http://192.168.1.1:7890"}'支持 HTTP/HTTPS/SOCKS5 代理协议。设置即时生效,无需重启。
注意事项:
- loopback 地址(
localhost/127.0.0.1/::1)自动跳过代理 - 与 GitHub 镜像加速(
github_proxyURL 前缀拼接)共存:先拼接镜像前缀再经 HTTP Proxy 转发 - 影响范围:插件注册表拉取、插件下载/更新、系统升级检查/下载
Tag 写入问题
章节来源:pkg/tag/write.go、AGENTS.md tag 写入章节
格式支持状态
tag.WriteTag(filePath, opts) 按文件扩展名分发写入实现:
| 格式 | 写入方式 | 状态 |
|---|---|---|
.mp3 | ID3v2.3(TIT2, TPE1, TPE2, TALB, TYER, TCON, USLT, APIC) | 支持 |
.flac | Vorbis Comment + PICTURE block | 支持 |
.ape | APEv2 tag | 支持 |
.wav | RIFF INFO / ID3 | 支持 |
.m4a / .mp4 / .m4b | iTunes-style atoms | 支持 |
.ogg / .oga | Vorbis Comment (Ogg container) | 支持 |
.aif / .aiff | ID3v2.3 (ID3 chunk) + NAME/AUTH native chunks | 支持 |
| 其他格式 | -- | 返回 ErrUnsupportedWrite |
写入失败处理
- 不支持的格式返回
ErrUnsupportedWrite,调用方必须降级为日志记录,不得阻塞主流程 - 写入采用原子化策略:先写临时文件(在源文件同目录创建),再
os.Rename覆盖原文件 - 原子写入失败(如磁盘空间不足、权限不足)时,原文件保持不变
平台特定问题
章节来源:AGENTS.md 平台适配踩坑章节、docs/faq.md
macOS
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Token 存储报错 | secure_storage 在未签名沙盒下无法使用 Keychain | 系统自动降级到 SharedPreferences,无需处理 |
| 升级检查不可用 | 仅 Docker 部署支持在线升级 | 手动下载新版本替换 |
Android
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 构建失败 | SDK 许可证未接受 | 执行 sdkmanager --licenses |
| 通知不显示 | Android 13+ 需运行时通知权限 | 在应用设置中授予通知权限 |
| HyperOS3 后台被杀 | 系统激进回收前台服务 | 配置 androidStopForegroundOnPause: false |
Windows / Linux
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 音频播放无声/崩溃 | 音频后端依赖 libmpv | 安装 just_audio_media_kit 所需的 libmpv 库 |
| Windows 无法修改密码 | 双击 exe 无法传参 | 创建 songloft.bat,写入 songloft.exe -password your_password |
Docker
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 定时任务时间错误 | 容器时区与宿主不一致 | 设置环境变量 TZ=Asia/Shanghai |
| 音乐文件不可见 | 卷挂载路径非绝对路径 | 使用 -v /absolute/path:/app/music |
| 二进制未更新 | 热替换规则判定保留 data 版本 | 参见 Docker 热替换规则表 |
性能调优
章节来源:internal/database/sqlite.go、AGENTS.md 音乐缓存章节
SQLite 优化参数
系统启动时通过 DSN 参数自动配置以下优化,通常无需手动调整:
| 参数 | 值 | 作用 |
|---|---|---|
journal_mode | WAL | 读写并发,读不被写阻塞 |
busy_timeout | 10000 (10s) | 遇锁等待而非直接失败 |
synchronous | NORMAL | WAL 模式下已足够安全,减少 fsync |
cache_size | 10000 | 页缓存约 40 MB |
foreign_keys | 1 | 启用外键约束 |
MaxOpenConns | 10 | 连接池上限 |
MaxIdleConns | 5 | 空闲连接上限 |
ConnMaxLifetime | 30 min | 连接最大存活时间 |
扫描大量文件
大规模音乐库扫描建议:
- 使用
ExcludeDirs排除非音乐目录(如.git、node_modules、缩略图目录) - 使用
ExcludePaths精确排除特定路径 - 扫描是异步执行的,可通过进度 API 查看状态,也可取消正在进行的扫描
- 首次扫描后增量扫描速度显著提升
缓存 LRU 淘汰策略
| 配置项 | 默认值 | 说明 |
|---|---|---|
max_size | 1 GB | 缓存总大小上限;0 表示不限制 |
cache_dir | {data_dir}/music_cache/ | 可自定义为任意绝对路径 |
切换缓存目录时会自动重建 LRU 索引,但不迁移旧文件。inflight 去重机制确保同一歌曲的并发请求只下载一次。
调试技巧
章节来源:internal/handlers/log.go、internal/services/source/metrics.go、internal/jsplugin/health.go
运行时日志级别
支持运行时动态切换日志级别,无需重启:
# 查看当前级别
curl http://localhost:58091/api/v1/settings/log-level \
-H "Authorization: Bearer TOKEN"
# 切换到 debug 级别
curl -X PUT http://localhost:58091/api/v1/settings/log-level \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"level": "debug"}'支持的级别:debug、info(默认)、warn、error。修改后即时生效且持久化到数据库,重启后自动恢复。
访问日志
后端使用标准库 slog 输出结构化日志。排查问题时建议临时切换到 debug 级别以获取详细的请求处理信息,排查完毕后切回 info 减少日志量。
插件健康检查 API
查看所有插件的健康状态:
curl http://localhost:58091/api/v1/plugins/health \
-H "Authorization: Bearer TOKEN"返回每个插件的健康度分类(green/yellow/red)、成功率、采样数和最近失败记录。
源编排 Metrics API
查看各音源插件的 Fetch 成功率与健康度:
curl http://localhost:58091/api/v1/plugins/health \
-H "Authorization: Bearer TOKEN"返回的 PluginHealthSnapshot 包含:
| 字段 | 说明 |
|---|---|
class | 健康度分类:green(成功率>=80%) / yellow(中间态或样本不足) / red(成功率<40%) |
success_rate | 最近 200 次的成功率 |
samples | 当前采样数(<10 时不会被判定为 red,避免冷启动误杀) |
last_failures | 最近失败记录及原因(network_fail / probe_fail / validation_fail / plugin_invocation_fail) |
健康度影响 Resolver 排序权重:score = baseScore * (0.3 + 0.7 * successRate)。样本不足时取中性值 0.8,不会让冷启动插件被零优先级。
Swagger 文档
开发模式(make run)启动后,访问 http://localhost:58091/swagger/index.html 查看交互式 API 文档。生产版本不包含 Swagger。
pprof 性能分析
开发模式构建(-tags dev)包含 pprof 端点,可用于 CPU/内存/goroutine 分析。生产版本中此功能不可用。
