开发指南
AI 任务的连续执行、验证证据、跨仓库 Issue 排查和经验复盘见 AI 开发流程,经验保存在仓库根 LESSONS.md。提交、推送、发布和 Issue 写操作按明确授权执行。
导航与插件
入口:设置 → 扩展 → 导航与插件。页面按「插件管理 → 内置导航 → 插件导航排序」排列;曲库入口开关位于插件列表下方,外观设置不再提供重复入口。数量、首页/设置固定显示和移动端折叠提示统一放在页尾。
「显示曲库入口」只改变导航显示,不影响音乐数据。复用 /settings/tab-config 和同一保存状态;修改曲库开关保留全部插件显示偏好和顺序,停用插件不占导航名额。无插件时也能修改曲库入口;读取失败、保存期间或新增入口超过 12 项上限时禁用对应操作。
验证:flutter analyze、完整 656 项测试和嵌入 Web 构建通过;Docker Chromium 中文 1280px/390px 与英文宽屏实际操作并目测截图,真实临时后端确认曲库开关只改变入口、插件偏好与顺序完整保留,外观页无重复开关。中英西文案及生成产物同步,自审额外将外观分类说明改为「主题和显示」,避免仍指向已迁走的菜单设置。证据为 /tmp/issue-501-navigation-flutter-{zh,en}.json 和同前缀 PNG;原生 Flutter 设备未执行。
GitHub 社区插件发现
入口:设置 → 扩展 → 插件商店 → 选源菜单 →「GitHub 发现」。这是独立发现页面,不作为订阅源保存;返回后保留商店的源和搜索状态,源设置按钮继续管理订阅源。
客户端直接搜索公开 songloft-plugin topic,校验根 plugin.json、同仓库更新链、哈希格式和对应稳定 Release 的 .jsplugin.zip 资产。支持搜索、排序、分页、刷新、GitHub 代理与限流提示;不合规和暂未验证分别计数。GitHub 请求不携带服务器登录凭据。
发现页顶部的 GitHub Token 入口支持保存、更换和清除。通过已认证的 GET/PUT /settings/github-discovery-token 将 Token 保存在当前服务器,客户端进入页面和主动刷新时读取;GitHub 发现仍由客户端请求。配置 Token 后,搜索和 Release API 绕过 GitHub 加速代理,直连 https://api.github.com 并携带 Authorization 头;代理和公开清单请求不带此 Token,认证失败不降级到代理。未配置时保留原有匿名代理流程。保存或清除会取消旧请求并重新发现,保留搜索和排序;失效 Token 显示专门提示。配置接口的请求/响应内容不进入 Flutter 调试日志。
本次 Token 与商店验证:完整 682 项测试通过;最后空源状态和取消断言修正后,flutter analyze 无问题、商店组件定向 15 项与嵌入 Web 构建通过。覆盖输入隐藏、保存/清除、失败保留配置、凭据日志隔离、渐进加载、凭据优先级与缓存边界。真实临时 Go 后端验证重启持久化、清除和 no-store 响应;后端完整检查通过。本批 Flutter 原生设备与浏览器尚未实测;下方 539 项与浏览器结果是此前发现功能的验证记录。
商店客户端请求与凭据优先级
提交前自审修正了源删除/停用后的回退、全部源删除时的旧请求取消、首条安装后迟到版本的更新提示、匿名 401 的错误分类,以及有效空源刷新后的缓存替换。完整 682 项回归通过;最后空源状态和取消断言修正后,15 项商店组件回归及静态检查通过。隔离副本撤掉保护后,6 条回归按预期失败,副本已恢复。自审阶段保持未提交;原生设备与 Flutter 浏览器未实测。
商店由独立公共 Dio 拉取注册表、includes 和清单,逐个解析、逐个显示,最多并发 8 个请求;完整清单在客户端缓存 5 分钟,搜索与分页复用缓存,主动刷新重新读源配置。取消、搜索、换源、切换服务器和卸载会作废旧进度;安装成功会就地更新行状态。已安装列表和实际安装仍走服务器,Web 跨域或连接失败保留服务器兼容回退。
每个源的 Token 优先,共享 GitHub Token 仅在没有源凭据且访问 https://api.github.com 时使用。共享 Token 不传给 raw 清单、第三方源、服务器回退或安装;带任一 Token 的请求绕过 GitHub 前缀代理。源清单中明确列出的插件 URL 继承源凭据,includes 仅同 origin 继承,后续 updateUrl 仅同初始清单或配置源 origin 继承。安装提交 source_url,由服务器读取当前源 Token;客户端不把旧的源凭据覆盖回服务器。服务器 HTTP 代理仅作用于回退和下载;首条结果更早可见不代表总耗时一定更短。
URL 校验在 Dart URI 规范化之前拒绝原始及编码的点路径段、反斜杠,与 Lynx 原生校验保持一致。安装成功的会话标记只用于刷新期间的状态同步;服务器列表刷新成功后会核对标记,防止其他页面卸载或替换插件后仍显示旧安装状态。
发布标签用于定位下载 URL 对应的真实 Release,不要求标签与清单版本相同;例如 v0.17 发布标签可配合 0.17.0 清单版本。仍核对下载 URL、实际 Release 标签与已上传资产,更新清单链的版本一致性检查保持生效。
详情显示版本、权限和最低宿主版本,安装前确认「未经 Songloft 审核」;同入口冲突需确认替换,宿主版本未知或不足、已安装信息未就绪时禁用安装。复用服务器现有插件安装接口,校验格式不代表安全审核。窄屏使用详情底部弹层,宽屏使用对话框。
服务端明确返回 dev 时,所有插件跳过最低宿主版本检查;正式版继续比较最低版本,版本请求失败或其他无法识别的版本仍需重试。dev 不跳过插件校验、已安装信息检查或安装确认。
验证:静态检查、完整 539 项测试及 Web 构建通过;中英西三语在窄屏/宽屏和大字体下的组件测试通过。Docker Chromium 实跑真实 GitHub 查询(7 个仓库:3 个有效、4 个过滤)、确认前零请求、确认后单次安装及返回商店;临时真实 Go 后端查询确认安装的版本和 Release 地址。原生 Flutter 设备尚未验证。
环境搭建
基础要求
- Flutter >= 3.41.0
- Dart SDK >= 3.7.0
平台额外依赖
Linux:
sudo apt-get install clang cmake ninja-build pkg-config libgtk-3-devAndroid:
sdkmanager --licenses需要 Android SDK、NDK、JDK 17+。
macOS / iOS:
- Xcode 15+
- CocoaPods:
sudo gem install cocoapods
Windows:
- Visual Studio 2022(带 C++ 桌面开发工作负载)
- Windows 10 SDK
首次运行
flutter pub get
flutter run -d chrome --no-web-resources-cdn # Web standalone
flutter run -d macos # macOS后端需要先启动:在父仓库执行 make run(默认 http://localhost:58091,账号 admin/admin)。
新增 Feature 模块
项目采用 Feature-First 架构,每个功能模块遵循三层结构:
features/<name>/
├── data/ # API 类(HTTP 调用)+ Repository
│ ├── <name>_api.dart
│ └── <name>_repository.dart (可选)
├── domain/ # 状态模型 + 业务逻辑
│ └── <name>_state.dart
└── presentation/ # UI 层
├── <name>_page.dart
├── providers/
│ └── <name>_provider.dart
└── widgets/
└── <name>_xxx.dartProvider 命名惯例
手写 Riverpod Provider(不使用 code generation / build_runner):
// API 实例(无状态)
final xxxApiProvider = Provider<XxxApi>((ref) {
final dio = ref.watch(dioProvider);
return XxxApi(dio);
});
// 异步数据加载
final xxxDetailProvider = FutureProvider.family<Xxx, int>((ref, id) async {
final api = ref.watch(xxxApiProvider);
return api.getDetail(id);
});
// 有状态的业务逻辑
final xxxProvider = NotifierProvider<XxxNotifier, XxxState>(XxxNotifier.new);
// 异步有状态
final xxxListProvider = AsyncNotifierProvider<XxxListNotifier, XxxListState>(
XxxListNotifier.new,
);路由添加
路由配置在 lib/core/router/app_router.dart:
- 在
AppRoutes类中添加路径常量:
class AppRoutes {
static const String myFeature = '/my-feature';
}- 在
routerProvider的GoRouter中添加路由(Shell 内或独立):
GoRoute(
path: AppRoutes.myFeature,
builder: (context, state) => const MyFeaturePage(),
),- 认证守卫自动工作:
redirect检查authStateProvider,未登录时重定向到/login,/login路径不受守卫保护。
API 对接
HTTP 客户端
Dio 封装在 lib/core/network/api_client.dart:
dioProvider— 带认证拦截器的 Dio 实例publicDioProvider— 无认证的 Dio(用于登录等公开接口)apiClientProvider— 高级封装(较少直接使用)
JWT 双 Token
AuthInterceptor(lib/core/network/auth_interceptor.dart)自动处理:
- 请求时自动注入
Authorization: Bearer <accessToken> - 401 响应时自动用 refreshToken 刷新
- 并发刷新保护(多个 401 只触发一次刷新)
- 刷新失败时回调
onTokenExpired触发登出
资源 URL
UrlHelper(lib/core/utils/url_helper.dart)构建封面、音频等资源 URL,自动拼接 baseUrl + token。
新增 API 类
class MyFeatureApi {
final Dio _dio;
MyFeatureApi(this._dio);
Future<MyModel> getData(int id) async {
final response = await _dio.get('/api/v1/my-feature/$id');
return MyModel.fromJson(response.data);
}
}条件导入
Web 平台不支持某些原生功能(如 WebView),使用条件导入 stub 文件模式:
// plugin_webview_page.dart(入口文件)
export 'plugin_webview_page_stub.dart'
if (dart.library.io) 'plugin_webview_page_native.dart';// plugin_webview_page_stub.dart(Web 平台 — 显示占位或替代 UI)
class PluginWebViewPage extends StatelessWidget { ... }
// plugin_webview_page_native.dart(原生平台 — 使用 WebView)
class PluginWebViewPage extends StatelessWidget { ... }项目中使用此模式的文件:
plugin_webview_page.dart/_native.dart/_stub.dartplugin_tab_page.dart/_native.dart/_stub.dartweb_cache_clearer.dart/_stub.dart
Lint 规则
analysis_options.yaml 中的关键规则:
| 规则 | 说明 |
|---|---|
prefer_const_constructors | Widget 实例化时尽量加 const |
prefer_const_declarations | 可以为 const 的变量声明为 const |
prefer_single_quotes | 统一使用单引号 |
avoid_print | 禁止 print(),使用 debugPrint() |
排除规则:*.g.dart 和 *.freezed.dart 不检查。
测试
flutter test # 运行全部测试
flutter test test/core/env/tv_detector_test.dart # 运行单个文件测试文件放在 test/ 下,对应 lib/ 的路径结构。
GitHub 发现:跨仓库发布包(2026-10-08)
允许清单的下载地址和公开更新清单来自其他 GitHub 仓库。客户端按下载地址验证目标仓库的稳定 Release 和真实非空资产,保留版本、入口、哈希和 URL 校验。详情显示下载地址,安装确认显示发布包来源;已安装与更新状态按实际发布包仓库判断。
GitHub 发现:根清单空哈希(2026-10-08)
根目录 plugin.json 的 entryHash、zipHash 可省略或留空;非空值继续校验格式。发现页不要求构建产物哈希回写到源码根清单,实际安装包仍由服务器校验。
