- 抽出 lib/core 的日期/JSON 工具,收敛各页重复解析 - 主页与音乐风险页拆成组件,风险功能共用一套运行框架 - 修复设置页作为主页页签保存时弹掉根路由导致的黑屏 - 登录对齐 Astral aimedb 协议:84 位二维码校验、签名与请求头修正, 补 GlobalSign 中间证书;Windows 改用 WinHTTP 绕过服务端对 BoringSSL 的 TLS 指纹拦截(证书校验保持开启) - 补开发预览数据与测试
6.5 KiB
6.5 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Build & Development Commands
# Run the app (launches on connected device)
flutter run
# Run analyzer
flutter analyze
# Run tests
flutter test
# Run a single test file
flutter test test/widget_test.dart
# Build release artifacts
flutter build apk --release # Android
flutter build windows --release # Windows
flutter build macos --release # macOS
flutter build linux --release # Linux
flutter build ios --release --no-codesign # iOS(无证书时只验证可编译)
# Check for outdated dependencies
flutter pub outdated
# Upgrade dependencies
flutter pub upgrade --major-versions
Architecture
This is a Flutter app (Android / iOS / Windows / macOS / Linux) that serves as a QR-code-based login client for the maimai DX arcade game network.
Code Layout
lib/
├── main.dart # 入口:main() + MyApp(MaterialApp)
├── config/ # strings / responsive / TitleServerConfig
├── core/
│ ├── json_value.dart # 宽松 JSON 数值转换(jsonInt / jsonBool / jsonMapList …)
│ └── date_time_format.dart# UTC+8 时间戳与 yyyy-MM-dd HH:mm:ss.f 格式化
├── pages/
│ ├── login_page.dart # 登录页(QR 粘贴 / 图片识别)
│ ├── home_page.dart # 主页骨架(HomeTab 枚举 + 懒构建 IndexedStack)
│ ├── home/
│ │ ├── home_tab.dart # 六个页签的标题 / 图标 / 顺序
│ │ └── home_tab_view.dart # 首页内容(加载 / 错误 / preview / userData 分支)
│ ├── settings_page.dart # Title Server / 机器参数配置
│ └── ... # 各功能页(ticket / best50 / 风险操作等)
├── models/
│ ├── session_model.dart # 会话与 Cookie 的唯一持有者(ChangeNotifier 单例)
│ └── ...
├── widgets/
│ ├── cooldown_mixin.dart # CooldownMixin:登录后冷却倒计时
│ ├── step_progress.dart # RiskStep + StepProgressCard
│ ├── app_card.dart # AppCard / SectionTitle / AutoLogoutToggle
│ ├── app_notice.dart # AppNotice + context.showSnack
│ ├── home/ # 主页资料 / 统计 / 调试卡片
│ ├── risk/
│ │ ├── risk_list_widgets.dart # 待提交列表与表单控件
│ │ ├── music_risk_editors.dart # 解锁歌曲 / 收藏品编辑器
│ │ └── risk_run_mixin.dart # 风险执行阶段与错误上报
│ └── best50_poster.dart # B50 成绩图
└── services/
├── api_service.dart # Aime 登录 API 客户端
├── title_api_service.dart # 游戏服务器传输层(加解密 / 会话 / 各 Api)
├── user_all_payload_builder.dart # UpsertUserAll 组装与各补丁
├── api_log.dart # 传输层调试日志(仅 debug)
└── ...
Key Design Decisions
- Risk-flow scaffolding is shared: the feature pages (ticket / unlock music / collectibles /
travel partner / number patch / map traverse / kaleidx / risk hub) all use
CooldownMixin,RiskStep,StepProgressCard,AppCard,AppNoticeandAutoLogoutToggleinstead of copy-pasting the same timer / step / banner / card code. Keep new pages on these primitives. - QR decoding is native-only:
services/qr_service.dartcalls thedev.naominet.ichino/qr_scannerMethodChannel, implemented by Android'sMainActivityand iOS'sSceneDelegate. Desktop has no handler, so image-based QR scanning is mobile-only. - Auth flow:
ApiService.login()is a port of Astral'sapi/Chime.py. It normalizes the pasted input (84-charSGWCMAID…string, or awq.wahlap.net/qrcode/req/….htmllink) and POSTs{chipID, openGameID, key, qrCode, timestamp, titlekey}to the hardcoded aimedb endpointhttps://ai.sys-allnet.cn/wc_aime/api/get_data. ThekeyisSHA-256(完整 Keychip + timestamp + 内置签名盐)upper-hex;qrCodeis the 64-char payload (input[20:]), not the whole string. The request must not carry a Content-Type header — aimedb answerserrorID: 57when it does — so it is sent viahttp.Request+bodyBytes.chipIDuses the full hyphenated Keychip (TitleServerConfig.fullKeychipId), while the title server keeps using the 11-char shortclientId. A successful response haserrorID: 0and returnsuserID+token. - Title server transport:
TitleApiServicederives the URL path fromMD5(apiName + salt + obfuscateParam), JSON → ZLib → AES-CBC encodes/decodes, and keeps theJSESSIONIDcookie.getUserPreviewand every other API go through the same_callApihelper. - Session and cookies have one owner:
SessionModelholds the latestJSESSIONID; pages build services withSessionModel.createTitleService(cookies: …)instead of snapshotting the cookie themselves. Any response can rotate the cookie, soTitleApiServicewrites it back throughonCookiesChangedandSessionModel.updateCookiesnotifies listeners — visited tabs then rebuild with the fresh value. Directly-constructed page tests fall back to the cookie passed to the page.lastRawResponse(the homepage debug card) is per service instance, not static;SessionModelforwards the restored preview body throughSessionRestoreResult. - Home tabs are lazy: the
HomeTabenum owns order/title/icons;HomePagekeeps a_visitedTabsset and fills unvisitedIndexedStackslots withSizedBox.shrink(), so ticket / B50 / risk pages do not fire their startup requests until first opened. - User-facing API errors: call
describeApiError(e)—TitleApiExceptioncontributes only its message (protocol failures stay readable), anything else keepstoString()for debugging. Use thecore/json_value.darthelpers for loose server JSON instead of ad-hoc casts, andcore/date_time_format.dartfor UTC+8 timestamps. - The login timestamp is the device's local wall-clock time (
yymmddHHmmss), matching the arcade cabinet; it is not pinned to any fixed UTC offset.
Notable Dependencies
http— API callscrypto— MD5 URL hash + SHA-256 Aime keypointycastle/archive— AES-CBC + ZLib for the title-server wire formatimage_picker— picking QR images from the device galleryshared_preferences— persistedTitleServerConfigshare_plus— exporting the B50 PNG