Browse Source

apps/client 首次入库(Flutter 移动端)

从 /Users/yunyanzhineng/work/eaimar_client-main 搬入,未带原 .git,历史留在原仓库。
3215 个文件 / 约 118MB,其中约 40MB 是构建必需的预编译 SDK(azure_speech 的
MicrosoftCognitiveServicesSpeech.xcframework、device_jieli 的 JL_BLEKit 等)。
build/ (5.2G)、ios/Pods/ (311M)、android/.gradle/ (120M) 已由 .gitignore 挡掉。

本次入库的代码包含两项刚做完的改动:

1) 游客登录入口接上服务端开关。此前 _checkGuestLoginConfig() 恒定写死 true,
   入口根本不读配置。现改为取服务端算好的结论,并把入口所在的 Builder 改成 Obx
   ——配置是异步拿到的,Builder 只渲染 onInit 时的初值 false,入口再也不会出现。
   拿不到配置时按隐藏处理:默认显示会在送审期把不该露的入口露给审核员。
   ⚠️ 已发布的老客户端关不掉(写死 true),只对本版及以后生效。

2) iOS 上架合规修复:
   - 移除 ATT/IDFA:删掉 Info.plist 的 NSUserTrackingUsageDescription、
     SKAdNetworkItems、NSAdvertisingAttributionReportEndpoint,删掉 splash 里的
     授权请求与 app_tracking_transparency 依赖。本 App 从不读取 IDFA,也没有任何
     广告/归因 SDK,弹框纯粹劝退用户,还要在 App Store Connect 申报追踪数据类型。
   - ios/Podfile 里那段 config.build_settings.delete('NSUserTrackingUsageDescription')
     是无效代码(该 key 在 Info.plist 里,不是 build setting),换成注释说明。
   - 通讯录符号(ITMS-90683 被拒原因)随之一并解决:permission_handler 走 SPM 后
     按 Info.plist 有无对应 key 决定编不编,判定结果被 SwiftPM 按内容哈希缓存,
     改完 plist 必须清 DerivedData 与 manifests 缓存才会重新求值。

另附 GOOGLE_PLAY_RELEASE_CHECKLIST.md:Google Play 上架阻塞项的实测梳理
(targetSdk 36 与 Billing 8 的 2026-08-31 期限、16KB 页对齐不合规的两个 .so、
支付回退逻辑的下架风险)。

⚠️ android/app/sign/ 下的 eaimar.keystore 与 Eaimar.properties(含明文签名密码)
随本次提交入库,与本仓库"私有仓库下接受真实凭据"的既有取舍一致。
仓库若要公开,这两个必须先摘出去。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main
Rodger-Wang 1 month ago
parent
commit
2d1ee0b783
  1. 74
      apps/client/.env.example
  2. 71
      apps/client/.gitignore
  3. 10
      apps/client/.mcp.json
  4. 30
      apps/client/.metadata
  5. 182
      apps/client/GOOGLE_PLAY_RELEASE_CHECKLIST.md
  6. 30
      apps/client/GoogleService-Info.plist
  7. 51
      apps/client/I18N_PLACEHOLDER_REMEDIATION.md
  8. 60
      apps/client/README.md
  9. 27
      apps/client/SpeexKit.xcframework/Info.plist
  10. 44
      apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/OpusTools.h
  11. 100
      apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/OpusUnit.h
  12. 21
      apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/SpeexKit.h
  13. 40
      apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/SpeexUnit.h
  14. BIN
      apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Info.plist
  15. 6
      apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Modules/module.modulemap
  16. BIN
      apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/SpeexKit
  17. 1
      apps/client/analysis_options.yaml
  18. 16
      apps/client/android/.gitignore
  19. 164
      apps/client/android/app/build.gradle.kts
  20. 29
      apps/client/android/app/google-services.json
  21. 15
      apps/client/android/app/proguard-logging.pro
  22. 87
      apps/client/android/app/proguard-rules.pro
  23. 10
      apps/client/android/app/sign/Eaimar.properties
  24. BIN
      apps/client/android/app/sign/eaimar.keystore
  25. 7
      apps/client/android/app/src/debug/AndroidManifest.xml
  26. 202
      apps/client/android/app/src/main/AndroidManifest.xml
  27. 541
      apps/client/android/app/src/main/kotlin/com/yimai/eaimar/MainActivity.kt
  28. 196
      apps/client/android/app/src/main/kotlin/com/yimai/eaimar/MyApplication.kt
  29. 12
      apps/client/android/app/src/main/res/drawable-v21/launch_background.xml
  30. 12
      apps/client/android/app/src/main/res/drawable/launch_background.xml
  31. BIN
      apps/client/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  32. BIN
      apps/client/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  33. BIN
      apps/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  34. BIN
      apps/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  35. BIN
      apps/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  36. BIN
      apps/client/android/app/src/main/res/raw/listening.mp3
  37. BIN
      apps/client/android/app/src/main/res/raw/sample.mp3
  38. BIN
      apps/client/android/app/src/main/res/raw/silence.mp3
  39. 18
      apps/client/android/app/src/main/res/values-night/styles.xml
  40. 3
      apps/client/android/app/src/main/res/values/colors.xml
  41. 6
      apps/client/android/app/src/main/res/values/strings.xml
  42. 18
      apps/client/android/app/src/main/res/values/styles.xml
  43. 13
      apps/client/android/app/src/main/res/xml/network_security_config.xml
  44. 7
      apps/client/android/app/src/profile/AndroidManifest.xml
  45. 44
      apps/client/android/build.gradle.kts
  46. 4
      apps/client/android/gradle.properties
  47. 5
      apps/client/android/gradle/wrapper/gradle-wrapper.properties
  48. 60
      apps/client/android/settings.gradle.kts
  49. 30
      apps/client/assets/config/GoogleService-Info.plist
  50. BIN
      apps/client/assets/fonts/NotoSansSC-Regular.ttf
  51. BIN
      apps/client/assets/images/DefaultPicture.jpg
  52. BIN
      apps/client/assets/images/agents/assistant.png
  53. BIN
      apps/client/assets/images/agents/doctor.png
  54. BIN
      apps/client/assets/images/agents/girlfriend.png
  55. BIN
      apps/client/assets/images/agents/interpreter.png
  56. BIN
      apps/client/assets/images/agents/lily.png
  57. BIN
      apps/client/assets/images/agents/love_guru.png
  58. BIN
      apps/client/assets/images/agents/mei_hua.png
  59. BIN
      apps/client/assets/images/agents/shopping.png
  60. BIN
      apps/client/assets/images/agents/socrates.png
  61. BIN
      apps/client/assets/images/agents/speaking_coach.png
  62. BIN
      apps/client/assets/images/agents/storyteller.png
  63. BIN
      apps/client/assets/images/agents/tarot.png
  64. BIN
      apps/client/assets/images/defaultPicture.png
  65. BIN
      apps/client/assets/images/devices/earphone.png
  66. BIN
      apps/client/assets/images/devices/record_card.jpg
  67. BIN
      apps/client/assets/images/devices/record_card.png
  68. BIN
      apps/client/assets/images/explore.png
  69. BIN
      apps/client/assets/images/explore1.png
  70. BIN
      apps/client/assets/images/explore2.jpg
  71. BIN
      apps/client/assets/images/glasses_light.png
  72. BIN
      apps/client/assets/images/mine-background.png
  73. BIN
      apps/client/assets/images/music.png
  74. BIN
      apps/client/assets/images/qqmusic.png
  75. BIN
      apps/client/assets/images/robot_light.png
  76. 2
      apps/client/assets/js/d3@7.js
  77. 10
      apps/client/assets/js/markmap-lib.js
  78. 9
      apps/client/assets/js/markmap-toolbar.js
  79. 10
      apps/client/assets/js/markmap-view.js
  80. 8
      apps/client/assets/js/saveSvgAsPng.min.js
  81. BIN
      apps/client/assets/listening.mp3
  82. 1
      apps/client/assets/lottie/audio_wave.json
  83. 1
      apps/client/assets/lottie/intro_foriday.json
  84. 1
      apps/client/assets/lottie/meeting_wait.json
  85. 1
      apps/client/assets/lottie/sound_effect.json
  86. 3
      apps/client/assets/weather/100-fill.svg
  87. 3
      apps/client/assets/weather/100.svg
  88. 3
      apps/client/assets/weather/1001.svg
  89. 5
      apps/client/assets/weather/1002.svg
  90. 3
      apps/client/assets/weather/1003.svg
  91. 3
      apps/client/assets/weather/1004.svg
  92. 4
      apps/client/assets/weather/1005.svg
  93. 4
      apps/client/assets/weather/1006.svg
  94. 4
      apps/client/assets/weather/1007.svg
  95. 5
      apps/client/assets/weather/1008.svg
  96. 4
      apps/client/assets/weather/1009.svg
  97. 3
      apps/client/assets/weather/101-fill.svg
  98. 3
      apps/client/assets/weather/101.svg
  99. 3
      apps/client/assets/weather/1010.svg
  100. 4
      apps/client/assets/weather/1011.svg

74
apps/client/.env.example

@ -0,0 +1,74 @@
# ============================================================
# apps/client 本机运行时配置(flutter_dotenv)
#
# cp .env.example .env 然后按需填值。
#
# ⚠️ .env 同时被 pubspec.yaml 声明为 asset,且 main.dart 里 `dotenv.load(fileName: ".env")`
# 是启动第一步 —— 文件不存在 App 直接起不来,不是可选项。
# ⚠️ .env 已在 .gitignore 中(源仓库也一样忽略),别提交。
#
# 注意区分两套 "env":
# - 本文件 = 本机/渠道级配置,编译进包,用 dotenv.env['X'] 读;
# - AppConfig.env('X') = 后端 user_getappconfig 下发的配置,由 console 后台维护。
# 下面那一堆 VOLCANO_* / OPENAI_* 在当前线上 .env 里**实际都是空的** —— 第三方能力已改由
# 后端下发,本文件只在需要绕过后端、直连调试时才填。真正必须填的只有 SERVER_URL 和 Channel。
# ============================================================
# ---------- 后端地址(联调唯一必改项)----------
# 客户端所有业务请求形如 POST {SERVER_URL}/api/home/<模块>_<方法>,
# 由 gateway 的 /api/:service/:method 路由(apps/services/modules/gateway/wservice_comp.go:66)
# 转发到对应服务的反射注册方法。
#
# ym 测试环境:https://ym-dev.ymaikj.com (阿龙环境,当前默认)
# 本机联调: http://<你的局域网IP>:7100 (gateway HTTP 端口,见 deploy/app/confs/gateway.yaml.example)
# 真机调试别写 127.0.0.1/localhost —— 那是手机自己。
SERVER_URL=https://ym-dev.ymaikj.com
# 非中国大陆地区 / GOOGLE 渠道走这个地址(dio_manager.dart 的 baseUrl 逻辑)。
# 留空会自动回退到 SERVER_URL,不会变成空 host。
HW_SERVER_URL=https://ym-dev.ymaikj.com
# ---------- 渠道 ----------
# 取值见 lib/data/models/channel_app_model.dart 的 UserChannel(必须是那里
# fromName 认识的名字,写 xiaomi 这种小写短名会被解析成 UNKNOWN)。
# iOS 恒为 GLOBAL_APPLE,本项只对 Android 生效。
#
# 国内 5 家商店(华为/荣耀/小米/OPPO/vivo)现在共用一个包:渠道在运行时按
# 设备品牌推导(version_update_service.dart 的 getAndroidStoreInfoByBrand),
# 这里的值只是**兜底**——只作用于品牌认不出来的设备(三星、魅族等),决定它们
# 归到哪一档渠道统计、拿哪一档的游客登录/跳过设备绑定开关。填任一国内渠道即可,
# 但后台 app_registry 里要给这一档配好。
#
# 两个值不参与品牌推导,别拿来当国内包的兜底:
# GLOBAL_GOOGLE —— 海外包,装在华为手机上也必须继续连 HW_SERVER_URL;
# OFFICIAL —— 官方/内测包,agent 页面会多出"工具打断"调试菜单项。
Channel=CHINA_XIAOMI
# ---------- OpenAI 兼容网关(留空则由后端下发)----------
OPENAI_BASE_URL=
OPENAI_API_KEY=
OPENAI_MODEL=
# ---------- 火山引擎 语音(ASR / TTS,留空则由后端下发)----------
VOLCANO_APP_KEY=
VOLCANO_SECRET_KEY=
VOLCANO_HOST_URL=
VOLCANO_SERVICE_ID=
VOLCANO_SPEECH_APP_ID=
VOLCANO_SPEECH_APP_TOKEN=
VOLCANO_ASR_RESOURCE_ID=
VOLCANO_TTS_APP_ID=
VOLCANO_TTS_APP_TOKEN=
VOLCANO_TTS_RESOURCE_ID=
# 只在 .env 里出现,Dart 代码未引用(疑似遗留)
VOLCANO_AI_API_KEY=
# ---------- 火山引擎 翻译(留空则由后端下发)----------
VOLCANO_TRANSLATION_ACCESS_KEY=
VOLCANO_TRANSLATION_SECRET_KEY=
VOLCANO_TRANSLATION_REGION=
# ---------- iOS 内购商品 ID 拼装 ----------
APP_IOS_IAP_PREFIX=
APP_IOS_IAP_SUFFIX=
APP_IOS_IAP_ID_MAP=

71
apps/client/.gitignore

@ -0,0 +1,71 @@
# ============================================================
# apps/client(Flutter,包名 eaimar)
# ⚠️ 首要目的:挡住 build/(5.2G)、ios/Pods(311M)、android/.gradle(120M)
# 这三坨构建产物——它们占了本目录 5.8G 中的 5.6G,误提交会直接毁掉仓库。
# ============================================================
# ---- 杂项 ----
.DS_Store
*.log
*.swp
*.pyc
__pycache__/
.history
migrate_working_dir/
# ---- 编辑器 ----
*.iml
*.ipr
*.iws
.idea/
.vscode/
# ---- Flutter / Dart / Pub ----
# build/ 与 .dart_tool/ 不加前缀斜杠,才能同时覆盖 local_plugins/*/ 下的同名产物
build/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub/
.pub-cache/
pubspec_overrides.yaml
**/doc/api/
**/ios/Flutter/.last_build_id
# 混淆符号表
app.*.symbols
app.*.map.json
# ---- Android ----
.gradle/
**/android/**/gradle-wrapper.jar
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
**/android/key.properties
*.jks
# ---- iOS ----
**/ios/Pods/
**/ios/.symlinks/
**/ios/Flutter/ephemeral/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/flutter_export_environment.sh
**/ios/Flutter/flutter_assets/
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
**/xcuserdata/
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
# ---- 本机私密配置 ----
# .env 含火山/OpenAI/COS 等第三方密钥,只提交 .env.example
.env

10
apps/client/.mcp.json

@ -0,0 +1,10 @@
{
"mcpServers": {
"web-search": {
"url": "http://mcp.ymaikj.com:8000/sse"
},
"mp3": {
"url": "https://api.ymaikj.com/sse"
}
}
}

30
apps/client/.metadata

@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "30ade5e937e6afbd2d2c38ec53175b89b556447e"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 30ade5e937e6afbd2d2c38ec53175b89b556447e
base_revision: 30ade5e937e6afbd2d2c38ec53175b89b556447e
- platform: ios
create_revision: 30ade5e937e6afbd2d2c38ec53175b89b556447e
base_revision: 30ade5e937e6afbd2d2c38ec53175b89b556447e
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

182
apps/client/GOOGLE_PLAY_RELEASE_CHECKLIST.md

@ -0,0 +1,182 @@
# Google Play 上架待办清单
> 排查时间:2026-08-29。基线产物:`build/app/outputs/flutter-apk/app-release.apk`(1.0.1+4,Channel=CHINA_XIAOMI)。
> 所有结论都是在**构建产物**上实测的(合并后的 AndroidManifest、APK 里的 ELF 段对齐),不是读源码猜的。
---
## 0. 先决定:要不要申请延期
有两条硬期限是 **2026-08-31**,也就是排查次日。两条都可以在 Play Console 申请延期到 **2026-11-01**。
**建议先去 Play Console 把延期申请了**,再按下面的顺序改。否则 8/31 之后新包传不上去,改了也白改。
---
## 1. 硬阻塞
### 1.1 targetSdk 35 → 36(期限 2026-08-31)
- 位置:[android/app/build.gradle.kts](android/app/build.gradle.kts) `defaultConfig.targetSdk = 35`
- `compileSdk` 已经是 36,改一个数字即可
- ⚠️ 但 Android 16 有行为变更,改完**必须真机回归**:
- 强制 edge-to-edge(状态栏/导航栏不再自动留白,页面可能被遮)
- 方向与尺寸可调整性 API 变更(大屏/折叠屏)
- 前台服务类型校验更严
### 1.2 Play Billing 7.1.1 → 8.0+(期限 2026-08-31)
- 当前:`in_app_purchase_android` 依赖 `com.android.billingclient:billing:7.1.1`
- 要升到带 Billing Library 8 的 `in_app_purchase_android` 版本
- ⚠️ BL8 有破坏性 API 变更,是这份清单里**改动量最大**的一项。来不及就务必申请延期。
- 迁移指南:https://developer.android.com/google/play/billing/migrate-gpblv8
### 1.3 16KB 页对齐(已于 2025-11-01 生效,无延期)
实测 `arm64-v8a` 共 26 个 `.so`,**3 个不合规**(LOAD 段 p_align = 0x1000,需 ≥ 0x4000):
| 库 | 来源 | 处理 |
|---|---|---|
| `libAMapSDK_NAVI_v10_0_800.so` | 高德导航(预编译) | ✅ 已处理——[build.gradle.kts](android/app/build.gradle.kts) 的 `jniLibs.excludes` 在 `Channel=GLOBAL_GOOGLE` 时排除 `**/libAMapSDK*.so` |
| `libg722_jni.so` | **自己的源码** | ❌ 待修 |
| `libmsbc_jni.so` | **自己的源码** | ❌ 待修 |
后两个来自 [local_plugins/bluetooth_manager/android/src/main/cpp](local_plugins/bluetooth_manager/android/src/main/cpp)(`g722_jni.cpp` / `msbc_jni.c`,CMake 工程),**源码在仓库里,好修**。
根因:该插件的 `android/build.gradle` **没有声明 `ndkVersion`**,所以没继承主工程的 NDK `28.2.13676358`。NDK r28 起默认就是 16KB 对齐,用了老 NDK 才退回 4KB。
两种改法(建议都加,双保险):
```gradle
// local_plugins/bluetooth_manager/android/build.gradle
android {
ndkVersion = "28.2.13676358" // 与主工程一致,r28 默认 16KB 对齐
defaultConfig {
externalNativeBuild {
cmake {
cppFlags ""
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" // NDK r27+ 支持
}
}
}
}
```
兜底(任何 NDK 版本都生效),在 `src/main/cpp/CMakeLists.txt` 里给两个 target 加:
```cmake
target_link_options(g722_jni PRIVATE "-Wl,-z,max-page-size=16384")
target_link_options(msbc_jni PRIVATE "-Wl,-z,max-page-size=16384")
```
⚠️ 改完要清 `local_plugins/bluetooth_manager/android/.cxx/`(里面有旧的构建缓存),否则可能直接复用旧产物。
**armeabi-v7a 的不合规项不用管**:16KB 只约束 64 位(arm64-v8a / x86_64)。v7a 里的 `libneonui_shared.so`、`libneonuijni_public.so` 是 32 位专有,不影响上架。
**验证方式**(仓库里已有现成脚本):
```bash
python3 check_16kb.py build/app/outputs/bundle/release/app-release.aab
```
---
## 2. 政策风险:支付回退逻辑(优先级仅次于硬阻塞)
**这是最容易被下架的一条。**
[lib/modules/goods/controllers/goods_base_controller.dart](lib/modules/goods/controllers/goods_base_controller.dart) 的 `_getEnabledPayMethods()`:当后端没下发 `paychannels` 时,回退成展示 **微信 + 支付宝 + PayPal + Google**。
```dart
// 若后端未下发或全部被平台过滤,则回退为展示所有平台支持的方式
if (methods.isEmpty) {
final fallback = <_PayMethod>[
_PayMethod.wechat,
_PayMethod.alipay,
_PayMethod.paypal
];
...
}
```
数字商品在 Google Play 必须走 Play 结算,摆出微信/支付宝是最典型的下架理由。而且这个回退**恰好在配置缺失时触发**——排查当天测试环境就正处于这个状态(渠道配置没建,`paychannels` 是空的)。
两件事要一起做:
1. **改回退逻辑**:安卓端兜底只留 `google`(iOS 只留 `apple`),不要「拿不到配置就全展示」
2. **后台配置**:console 后台给 `GLOBAL_GOOGLE` 渠道的 `paychannels` 只填 `google`
---
## 3. 权限瘦身
合并后的 AndroidManifest 共 44 条权限,下面这些会被 Play 拦或需要额外声明:
| 权限 | 处理建议 |
|---|---|
| `QUERY_ALL_PACKAGES` | 需提交声明表;通常要求改成 `<queries>` 精确列出微信/支付宝包名(来源应是 fluwx / tobias) |
| `ACCESS_BACKGROUND_LOCATION` | 需声明表 + 演示视频,审核最严。**先确认是否真需要后台定位**,不需要就删 |
| `SYSTEM_ALERT_WINDOW` | 悬浮窗(`floating_ui_plugin`),需说明用途 |
| `WRITE_SETTINGS` | 很少被批准,确认是否真需要 |
| `SCHEDULE_EXACT_ALARM` | 有闹钟功能可解释,准备好说明 |
| `MEDIA_CONTENT_CONTROL` | **建议直接删**——这是系统签名权限,普通应用根本拿不到,声明了也无效,反而显得可疑 |
| `GET_TASKS` | **建议直接删**——API 21 起已废弃并被系统忽略 |
源清单里的 `SEND_SMS`、`MANAGE_EXTERNAL_STORAGE` 已经是注释状态,**合并产物里没有**,不用担心。
查合并后清单的命令:
```bash
AAPT=$(ls ~/Library/Android/sdk/build-tools/*/aapt2 | tail -1)
$AAPT dump permissions build/app/outputs/flutter-apk/app-release.apk | grep '^uses-permission'
```
---
## 4. 构建流程的坑
- **`.env` 的 `Channel` 必须是 `GLOBAL_GOOGLE`**(当前是 `CHINA_XIAOMI`)。
`./build_channels.sh google` 会自动改;手工跑 `flutter build appbundle` **不会**,那样 AMap 的 `.so` 也不会被排除 → 16KB 检查直接挂。
- **[build_channels.sh](build_channels.sh) 的 `OUTPUT_DIR` 写死成 `/Users/liuxiamin/work/build_outputs`**,本机不存在,脚本会失败。改成本机路径或改成相对路径。
- Google 渠道排除了 AMap `.so`,但**调用代码还在**。要验证海外包点到地图/导航相关功能不会崩。
- 产物是 **AAB 不是 APK**(`build_channels.sh` 的 google 分支已经处理成 `aab`)。
---
## 5. Play Console 首次上架事项
- Play App Signing(上传密钥用 `android/app/sign/eaimar.keystore`)
- 数据安全表单(Data safety)
- 内容分级问卷
- 目标受众与内容声明
- 隐私政策 URL —— iOS 那边已在用 `https://yimai.games/privacy.html`,可复用
---
## 建议的动手顺序
1. **去 Play Console 申请 targetSdk 与 Billing 的延期**(两条都是 8/31 到期)
2. 16KB 对齐(改 1 个 gradle + 1 个 CMakeLists,最确定,改完用 `check_16kb.py` 验)
3. 支付回退逻辑 + 后台 `paychannels` 配置
4. 权限瘦身(先删 `MEDIA_CONTENT_CONTROL` / `GET_TASKS`,再处理 `QUERY_ALL_PACKAGES`)
5. `build_channels.sh` 的 `OUTPUT_DIR` 改本机路径
6. targetSdk 36 + 真机回归
7. Billing 8 迁移(改动最大,放最后)
---
## 附:本次会话的其他遗留(与 Google Play 无关)
- **iOS 包 `1.0.1 (5)` 已打好待上传**:`build/ios/ipa/EAIMAR.ipa`。已修掉通讯录缺用途说明(ITMS-90683),并移除了 ATT/IDFA(`NSUserTrackingUsageDescription` 等三个 key、`app_tracking_transparency` 依赖、splash 里的授权请求)。
⚠️ 打 iOS 包要带上自备的 ExportOptions(`manageAppVersionAndBuildNumber=false`),否则构建号会被 Xcode 自动改掉:
```bash
flutter build ipa --release --export-options-plist=<path>/ExportOptions.plist
```
⚠️ 只要动了 `ios/Runner/Info.plist` 里的 `*UsageDescription`,就必须清一次 DerivedData 与 SwiftPM 清单缓存,否则 `permission_handler` 会按缓存里的旧宏编译,留下无用途说明的系统 API 引用 → 又一次 ITMS-90683:
```bash
rm -rf ~/Library/Developer/Xcode/DerivedData/Runner-*
rm -rf ~/Library/Caches/org.swift.swiftpm/manifests ~/Library/org.swift.swiftpm/cache
```
- **游客登录版本开关已上线到阿龙测试环境**(app/console/admin 三个服务),但**后台渠道配置还没建**,所以入口目前一定是隐藏的。要验证需在 console 后台「全局默认」作用域下配 App Store 与 荣耀 两条,生效版本填 `1.0.1`。详见 [CLAUDE.md](../../CLAUDE.md) 的「游客登录入口的显隐」一节。
- **两台测试机装的是 `1.0.1 (4)`**(含 ATT 的旧版),需要的话要重装 `(5)`。

30
apps/client/GoogleService-Info.plist

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyBEQPL-2Qh6fERxrxLP9Pzcm95mTmTpBxE</string>
<key>GCM_SENDER_ID</key>
<string>733281995296</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.saitong.voitrans</string>
<key>PROJECT_ID</key>
<string>voitrans</string>
<key>STORAGE_BUCKET</key>
<string>voitrans.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:733281995296:ios:6a8351691312100bd70a45</string>
</dict>
</plist>

51
apps/client/I18N_PLACEHOLDER_REMEDIATION.md

@ -0,0 +1,51 @@
# I18N Placeholder Remediation
This document records the repeatable steps used to detect and fix leftover English placeholder text and invalid-quote issues in translation map files under `lib/core/translations/language/`.
## Problem Patterns
### 1) English placeholders inside non-English locales
Symptoms:
- UI shows English strings in a non-English locale.
- `tools/i18n_audit.py --mode englishy` reports items for a locale file.
Fix:
- Replace English values with accurate localized text.
- Keep placeholder syntax intact (examples: `@minutes`, `{count}`, `{filePath}`, `{error}`).
### 2) Invalid “smart quotes” used as Dart string delimiters
Symptoms:
- `flutter analyze` reports `illegal_character`, `expected_token`, `expression_in_map`, `undefined_identifier` in a language file.
Root cause:
- Curly quotes like `‘ ’` are not valid Dart string delimiters, so map entries stop being parsed as strings.
Fix:
- Replace `‘` and `’` with ASCII `'` and ensure every entry is in the form `'key': 'value',`.
## Verification Checklist
Run these commands from the repo root:
```bash
python3 tools/i18n_audit.py --mode englishy
flutter analyze lib/core/translations/language/<locale>.dart
dart format lib/core/translations/language/<locale>.dart
flutter test test/<locale>_translations_test.dart
```
Recommended additional checks:
```bash
rg "[‘’]" lib/core/translations/language/<locale>.dart
```
## Test Convention
For each locale file, add a `test/<locale>_translations_test.dart` test that asserts:
- The map can be imported and accessed.
- Its key set matches `enUS`.
- It contains no empty string values.

60
apps/client/README.md

@ -0,0 +1,60 @@
# Deep Voice
Flutter 应用程序,使用 GetX 状态管理和依赖注入。
## 项目目录结构
```
lib/
├─ core/
│ ├─ bindings/ # 全局或通用的 Bindings
│ ├─ constants/ # 常量、枚举、Key、全局配置等
│ ├─ errors/ # 异常/错误处理相关
│ ├─ themes/ # 主题(Color, TextStyle 等)
│ ├─ translations/ # 国际化、语言相关
│ ├─ utils/ # 通用工具类、Helper、扩展函数等
│ └─ widgets/ # 通用的无业务逻辑的Widgets
│
├─ data/
│ ├─ models/ # 数据模型(可与后端DTO对应)
│ ├─ repositories/ # 数据仓库(实际操作数据 CRUD, 远程或本地)
│ ├─ services/ # 第三方或原生平台封装,如网络请求、缓存、数据库等
│ └─ providers/ # 适配器层,比如把网络、数据库等转换为 Repository 可调用的接口
│
├─ modules/
│ ├─ login/
│ │ ├─ controllers/ # GetX Controller
│ │ ├─ views/ # 页面、Widget
│ │ ├─ bindings/ # 该模块的 Bindings
│ │ └─ ... # 其他文件
│ ├─ home/
│ │ ├─ controllers/
│ │ ├─ views/
│ │ ├─ bindings/
│ │ └─ ...
│ └─ ...
│
├─ routes/
│ ├─ app_pages.dart # 定义所有页面的 Route 列表
│ └─ app_routes.dart # 定义路由常量或路由字符串
│
└─ main.dart # 程序入口
```
### 本地调试内购(IAP)流程
不需要真实购买即可在本地跑通前端内购流程:
- 在项目根目录的环境变量文件(如 `.env`)里添加:
```
APP_IAP_MOCK=1
```
- 作用:在 `PayService` 中启用“本地内购模拟”,调用 `processGooglePayment` 或 `processApplePayment` 会:
- 立即触发“已发起内购(本地模拟)”;
- 短暂延时后触发“支付处理中…(本地模拟)”;
- 再触发“支付成功(本地模拟)”。
- 注意:该模式不依赖 Google Play / App Store 安装来源,也不会进行服务器收据校验,仅用于前端 UI 与状态机验证。
- 关闭模拟:将 `APP_IAP_MOCK` 移除或设为 `0/false`,恢复真实内购流程。

27
apps/client/SpeexKit.xcframework/Info.plist

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AvailableLibraries</key>
<array>
<dict>
<key>BinaryPath</key>
<string>SpeexKit.framework/SpeexKit</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>SpeexKit.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
</array>
<key>CFBundlePackageType</key>
<string>XFWK</string>
<key>XCFrameworkFormatVersion</key>
<string>1.0</string>
</dict>
</plist>

44
apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/OpusTools.h

@ -0,0 +1,44 @@
//
// OpusTools.h
// SpeexKit
//
// Created by EzioChan on 2024/1/17.
// Copyright © 2024 www.zh-jieli.com. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol OpusToolsDelegate <NSObject>
-(void)opusToolsPcmData:(NSData*)data;
-(void)opusToolsPcmFile:(NSData *)data;
-(void)opusToolsOpusData:(NSData *)data;
-(void)opusToolsOpusFile:(NSData *)data;
@end
@interface OpusTools : NSObject
@property(nonatomic,weak)id<OpusToolsDelegate> delegate;
-(void)opusDecodeInfo:(int)sampleRate Channel:(int)channel frameSize:(int) fs isHasHead:(BOOL)head;
-(void)opusEncodeInfo:(int)sampleRate Channel:(int)channel frameSize:(int)fs isHasHead:(BOOL)head;
-(void)decodeData:(NSData*)data;
-(void)decodeFile:(NSString*)path;
-(void)encodeData:(NSData*)data;
-(void)encodeFile:(NSString*)path;
@end
NS_ASSUME_NONNULL_END

100
apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/OpusUnit.h

@ -0,0 +1,100 @@
//
// OpusUnit.h
// QCY_Demo
//
// Created by 杰理科技 on 2021/7/5.
// Copyright © 2021 杰理科技. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
* 通知:流式编码回调Opus数据
* 通知数据类型:NSData(Opus)
*/
extern NSString *kOPUS_ENCODE_DATA;
/**
* 通知:流式解码回调PCM数据
* 通知数据类型:NSData(PCM)
*/
extern NSString *kOPUS_DECODE_DATA;
@interface OpusUnit : NSObject
+ (void)opusIsLog:(BOOL)log;
#pragma mark - Opus参数
/**
* 设置参数
* @param rate 采样率 例如:48000、24000、16000、12000、8000(默认 16000)
* @param frameRate 帧长度
* 16k :40
* 32K :80
* 64k :160
* 96k :320
* 128k:640
* @param channel 声道 例如:1、2 (默认 1)
* @param isHeader 是否带头
*/
+(void)opusSetSampleRate:(int)rate frameRate:(int)frameRate Channels:(int)channel isHeader:(BOOL)isHeader;
#pragma mark - Opus解码
/**
* 直接【opus文件】转换成【pcm文件】
* @param path_opus opus文件路径
* @param path_pcm pcm文件路径
*/
+ (int)opusDecodeOPUS:(NSString *)path_opus PCM:(NSString *)path_pcm;
/**
* 流式解码【开启】
* 注意:需要异步调用,放在工程单利类中,切勿频繁开启。
*/
+ (int)opusDecoderRun;
/**
* 输入Opus的数据
* 通知监听“kOPUS_DECODE_DATA”获得解码后数据
* 通知数据类型:NSNotification.object => NSData(PCM格式)
* @param data opus数据流(长度:1024)
*/
+ (void)opusWriteData:(NSData*)data;
/**
* 流式解码【关闭】
*/
+ (int)opusDecoderStop;
#pragma mark - Opus编码
/**
* 【pcm文件】转换成【opus文件】
*/
+ (int)opusEncodePCM:(NSString *)path_pcm OPUS:(NSString *)path_opus;
/**
* 流式编码【开启】
* 注意:需要异步调用,放在工程单利类中,切勿频繁开启。
*/
+ (int)opusEncoderRun;
/**
* 输入pcm的数据
* 通知监听“kOPUS_DECODE_DATA”获得解码后数据
* 通知数据类型:NSNotification.object => NSData(opus格式)
* @param data opus数据流(长度:1024)
*/
+ (void)pcmWriteData:(NSData*)data;
/**
* 流式编码【关闭】
*/
+ (int)opusEncoderStop;
@end
NS_ASSUME_NONNULL_END

21
apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/SpeexKit.h

@ -0,0 +1,21 @@
//
// SpeexKit.h
// SpeexKit
//
// Created by zhihui liang on 2018/8/20.
// Copyright © 2018年 www.zh-jieli.com. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for SpeexKit.
FOUNDATION_EXPORT double SpeexKitVersionNumber;
//! Project version string for SpeexKit.
FOUNDATION_EXPORT const unsigned char SpeexKitVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <SpeexKit/PublicHeader.h>
#import <SpeexKit/SpeexUnit.h>
#import <SpeexKit/OpusUnit.h>
#import <SpeexKit/OpusTools.h>

40
apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/SpeexUnit.h

@ -0,0 +1,40 @@
//
// SpeexUnit.h
// SpeexKit
//
// Created by zhihui liang on 2018/8/20.
// Copyright © 2018年 www.zh-jieli.com. All rights reserved.
//
#import <Foundation/Foundation.h>
/**
* 流式解码回调PCM数据
*/
extern NSString *kSPEEX_DECODE_DATA; //speex decode
@interface SpeexUnit : NSObject
+(void)speexIsLog:(BOOL)log;
/**
* 直接【speex文件】转换成【pcm文件】
*/
+(int)speexDecodeSPX:(NSString *)path_spx PCM:(NSString *)path_pcm;
/**
* 流式解码【开启】
*/
+(int)speexDecoderRun;
/**
* 输入Speex的数据
*/
+(void)speexWriteData:(NSData*)data;
/**
* 流式解码【关闭】
*/
+(int)speexDecoderStop;
@end

BIN
apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Info.plist

Binary file not shown.

6
apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Modules/module.modulemap

@ -0,0 +1,6 @@
framework module SpeexKit {
umbrella header "SpeexKit.h"
export *
module * { export * }
}

BIN
apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/SpeexKit

Binary file not shown.

1
apps/client/analysis_options.yaml

@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml

16
apps/client/android/.gitignore

@ -0,0 +1,16 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
# Exception: keep the Eaimar release keystore tracked in git (intentional)
!app/sign/eaimar.keystore

164
apps/client/android/app/build.gradle.kts

@ -0,0 +1,164 @@
import java.util.Properties
import java.io.FileInputStream
repositories {
google()
mavenCentral()
flatDir {
dirs(project(":qq_music").projectDir.resolve("libs")) // 添加这行
dirs(project(":device_jieli").projectDir.resolve("libs")) // 杰理 SDK 运行时 .aar
}
}
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")version "4.3.13"
}
val keystorePropertiesFile = rootProject.file("app/sign/Eaimar.properties")
val keystoreProperties =Properties().apply {
load(FileInputStream(keystorePropertiesFile)) // 使用 Kotlin 扩展方法
}
android {
namespace = "com.yimai.eaimar"
compileSdk = 36
ndkVersion = "28.2.13676358"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
// Enable core library desugaring
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
// 添加lint选项
lintOptions {
isCheckReleaseBuilds = false
}
// 添加packaging配置,排除冲突文件
packaging {
resources {
excludes.add("META-INF/DEPENDENCIES")
excludes.add("META-INF/LICENSE")
excludes.add("META-INF/LICENSE.txt")
excludes.add("META-INF/license.txt")
excludes.add("META-INF/NOTICE")
excludes.add("META-INF/NOTICE.txt")
excludes.add("META-INF/notice.txt")
excludes.add("META-INF/*.kotlin_module")
}
jniLibs {
useLegacyPackaging = true
// Google Play 渠道排除高德地图 .so(尚未支持 16KB 页面对齐)
val envFile = rootProject.file("../.env")
if (envFile.exists()) {
val channel = envFile.readLines()
.firstOrNull { it.trim().startsWith("Channel=") }
?.substringAfter("=")?.trim()
if (channel == "GLOBAL_GOOGLE") {
excludes += setOf("**/libAMapSDK*.so")
}
}
}
}
defaultConfig {
manifestPlaceholders.putAll(
mapOf(
"applicationName" to "com.yimai.eaimar.MyApp" // 新增:声明自定义 Application
)
)
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.yimai.eaimar"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 26 // 更新为26以满足ble_service库的要求
targetSdk = 35 // 设置为 Android 15 (API 35),以满足应用市场要求
versionCode = flutter.versionCode // 对应构建号,自动同步flutter构建号
versionName = flutter.versionName // 对应版本号,自动同步flutter版本号
}
signingConfigs {
create("release") {
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
storeFile = file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
enableV1Signing = true
enableV2Signing = true
enableV3Signing = false
enableV4Signing = false
}
getByName("debug") {
// debug 使用与 release 相同的签名配置
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
storeFile = file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
enableV1Signing = true
enableV2Signing = true
enableV3Signing = false
enableV4Signing = false
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
isMinifyEnabled = false
isShrinkResources = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("release")
// Release 版本只保留 arm64-v8a 和 armeabi-v7a(移除 x86/x86_64)
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a")
}
}
debug {
isMinifyEnabled = false
isShrinkResources = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
// Debug 版本保留所有 ABI(包括 x86/x86_64,用于模拟器)
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
}
}
}
}
dependencies {
// 添加核心库反糖化
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
// 添加 CardView 依赖
implementation("androidx.cardview:cardview:1.0.0")
// device_jieli 杰理 SDK 运行时依赖(.aar 在 device_jieli 模块只能 compileOnly,
// host app 必须自己引入,否则 RCSPController 等类在运行时找不到 → scanFeature 不会被初始化)
implementation(":jl_bluetooth_rcsp_V4.2.0_beta1_40255_20260324@aar")
implementation(":jl_audio_v2_V1.0.0_9-release@aar")
implementation(":jldecryption_v0.4-release@aar")
// jl_audio_decode 已经被 ble_service 通过 com.jieli.audio:jl_audio_decode:2.1.0 引入,无需重复
}
flutter {
source = "../.."
}

29
apps/client/android/app/google-services.json

@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "733281995296",
"project_id": "voitrans",
"storage_bucket": "voitrans.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:733281995296:android:fd51ec5604787ab7d70a45",
"android_client_info": {
"package_name": "com.yimai.eaimar"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyDPvThoWXxZ-LhE0gS9KH51jRE3f8YTmOE"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

15
apps/client/android/app/proguard-logging.pro

@ -0,0 +1,15 @@
# 移除系统组件的日志调用
-assumenosideeffects class android.util.Log {
public static int v(...);
public static int d(...);
public static int i(...);
}
# 针对特定类的日志移除
-assumenosideeffects class android.media.AudioTrack {
private static void log*(...);
}
-assumenosideeffects class android.app.AppOpsManager {
private static void log*(...);
}

87
apps/client/android/app/proguard-rules.pro

@ -0,0 +1,87 @@
# Flutter Framework
# 这些规则保留了 Flutter 引擎和插件系统正常运行所需的类。
-ignorewarnings
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.embedding.android.** { *; }
-keep class io.flutter.embedding.engine.** { *; }
-keep class io.flutter.embedding.engine.plugins.** { *; }
-keep class io.flutter.plugin.common.** { *; }
-keep class io.flutter.plugins.** { *; }
-keep class io.flutter.plugin.platform.** { *; }
# 保留你的应用程序的主 Activity。
# !!! 请将 com.example.deep_voice.MainActivity 替换为你的实际包名和 Activity 类名 !!!
-keep class com.yimai.eaimar.MainActivity { *; }
# 保留 open_ai_service 包下的所有类
-keep class com.yunqiinnovation.open_ai_service.** { *; }
# 保留 MCPClient 相关类
-keep class com.yunqiinnovation.open_ai_service.MCPClient { *; }
-keep class com.yunqiinnovation.open_ai_service.MCPSubClient { *; }
-keep class com.yunqiinnovation.open_ai_service.FunctionHandler { *; }
-keep class com.yunqiinnovation.open_ai_service.ToolType { *; }
-keep class com.yunqiinnovation.open_ai_service.mcp.** { *; }
-keep class com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport { *; }
-keep class com.yunqiinnovation.open_ai_service.SystemFunctionHandler { *; }
-keep class com.yunqiinnovation.open_ai_service.OpenAIService { *; }
-keep class com.yunqiinnovation.open_ai_service.OpenAIService$* { *; }
# 保留 OpenAIServicePlugin 类
-keep class com.yunqiinnovation.open_ai_service.OpenAIServicePlugin { *; }
# 保留 Jackson 相关类
-keep class com.fasterxml.jackson.** { *; }
-keep class com.fasterxml.jackson.databind.** { *; }
-keep class com.fasterxml.jackson.core.** { *; }
# 保留 Ktor 相关类
-keep class io.ktor.** { *; }
-keep class kotlinx.coroutines.** { *; }
-keep class kotlinx.serialization.** { *; }
# 如果你的应用直接使用了 AndroidX 的某些特定类(例如,通过原生代码或插件),可能需要保留它们。
# 例如,保留所有 Parcelable 实现类,如果它们被跨进程使用或序列化。
# -keep class * implements android.os.Parcelable {
# public static final android.os.Parcelable$Creator *;
# }
# 保留 MCP 相关依赖库
-keep class io.modelcontextprotocol.** { *; }
-keep class com.anthropic.** { *; }
# 保留 OkHttp 相关类
-keep class okhttp3.** { *; }
-keep class okio.** { *; }
# 如果你使用了特定的第三方库,并且这些库需要特定的 ProGuard 规则,请在此处添加。
# 例如,OkHttp/Retrofit, Gson, Glide 等通常需要保留模型类或特定方法。
# 查看库的官方文档通常会提供所需的 ProGuard 规则。
# -keepclasseswithmembers class * {
# @android.webkit.JavascriptInterface <methods>;
# }
# -keepattributes Signature
# -keepattributes *Annotation*
# 如果你有自定义的原生插件,需要保留它们的类。
# !!! 将 com.yourcompany.yourplugin.** 替换为你的插件的实际包名 !!!
# -keep class com.yourcompany.yourplugin.** { *; }
# 保留枚举类,防止 switch 语句因混淆出错
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保留 R 类及其内部类,防止资源访问出错
-keep class **.R$* { *; }
# 保留 native 方法
-keepclasseswithmembernames class * {
native <methods>;
}

10
apps/client/android/app/sign/Eaimar.properties

@ -0,0 +1,10 @@
storePassword=yimai_eaimar1122
keyPassword=yimai_eaimar1122
keyAlias=eaimar
storeFile=sign/eaimar.keystore
# MD5 07:3E:04:37:4D:0B:C2:7A:A7:DC:33:A0:6E:8C:08:CA
# SHA1 7C:4F:C6:2B:E5:B2:BC:5A:CB:30:DA:03:E3:CA:1A:00:F6:E3:A5:41
# SHA256 44:41:E5:05:02:81:7E:6D:A1:B6:55:D8:2F:0D:B8:51:9B:F7:67:C3:33:65:35:F4:32:38:F9:C8:14:89:00:64
# 公钥 24530696301395375919563393278066326007446934316660892176275416398340283109169195507263881320626083624969521312488884026257416528201144343730134714194063946324231679348705674394222445953519321358508742668928883457587496780325939337237867206468223139467797610758279245727661035400854264170911613957052856920151931760124241936992312636853354084771867201123929674809120813727834750033391729508797967210118449997339392146056063492805157692250413815879649405355201386251329870638240486310320693478258586362148980863435658045424690450494987908286055811654705667160237691756259350928613949331855852852146959540130564050790727

BIN
apps/client/android/app/sign/eaimar.keystore

Binary file not shown.

7
apps/client/android/app/src/debug/AndroidManifest.xml

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

202
apps/client/android/app/src/main/AndroidManifest.xml

@ -0,0 +1,202 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 添加必要的权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- Android 13+ 前台服务特定类型权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<!-- Android 14+ 蓝牙设备连接前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>
<!-- Android 15+ 数据同步前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<!-- Android 14+ 麦克风前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- Android 13+ 通知权限 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- 添加存储权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- 添加Android 11+的存储权限 -->
<!-- <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" /> -->
<!-- Android 13+ 媒体权限 -->
<!-- <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> -->
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- 添加悬浮窗权限,可能有助于解决前台服务启动问题 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 添加发送短信权限 -->
<!-- <uses-permission android:name="android.permission.SEND_SMS" /> -->
<!-- 添加查询应用包权限 -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<!--允许获取网络状态,用于网络定位(无gps情况下的定位),若需网络定位功能则必选-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--允许获取wifi网络信息,用于网络定位(无gps情况下的定位),若需网络定位功能则必选-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!--允许获取wifi状态改变,用于网络定位(无gps情况下的定位),若需网络定位功能则必选-->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!--后台获取位置信息,若需后台定位则必选-->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!--用于申请调用A-GPS模块,卫星定位加速-->
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<!--用于申请调用精确闹钟权限-->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!--允许写设备缓存,用于问题排查-->
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission
android:name="android.permission.RECEIVE_BOOT_COMPLETED"
tools:node="remove" />
<application
android:label="EAIMAR"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
android:extractNativeLibs="true"
android:enableOnBackInvokedCallback="true"
tools:replace="android:label">
<!-- 支持16KB内存页面大小,满足Android 15要求 -->
<property android:name="android.allow_16kb_memory_page" android:value="true" />
<receiver
android:name="androidx.work.impl.background.systemalarm.RescheduleReceiver"
tools:node="remove" />
<!-- ============ 自启动 / 关联启动收敛(应用商店合规) ============ -->
<!-- 下面这几个组件都是「系统扫到已绑定的蓝牙设备 → 唤醒本应用」链路上的,
插件自带的清单把它们声明成 exported=true,等于任何第三方应用都能把本应用
拉起来(关联启动)。它们实际只接收本应用自己创建的 PendingIntent,
同进程投递不需要 exported,这里统一收成 false。 -->
<receiver
android:name="com.recorder.holder.BleWakeReceiver"
android:exported="false"
tools:replace="android:exported" />
<service
android:name="com.recorder.holder.BleMediaService"
android:exported="false"
tools:replace="android:exported" />
<receiver
android:name="com.yunqiinnovation.ble_service.BleWakeReceiver"
android:exported="false"
tools:replace="android:exported" />
<!-- WorkManager 的诊断广播口子:exported=true,adb / 其他应用可据此拉起进程,
本项目没用到诊断能力,直接摘掉 -->
<receiver
android:name="androidx.work.impl.diagnostics.DiagnosticsReceiver"
tools:node="remove" />
<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
<meta-data android:name="com.facebook.sdk.ClientToken" android:value="@string/facebook_client_token"/>
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:screenOrientation="portrait"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="eaimar"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="content" android:mimeType="audio/*" />
<data android:scheme="file" android:mimeType="audio/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="audio/*" />
</intent-filter>
<!-- PayPal 网页回跳的深链:deepvoiceclient://paypal/return 与 /cancel -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="deepvoiceclient" android:host="paypal" android:pathPrefix="/return" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="deepvoiceclient" android:host="paypal" android:pathPrefix="/cancel" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data
android:name="com.amap.api.v2.apikey"
android:value="78e7946dd602778df82c6ae143a88184"/>
<service android:name="com.amap.api.location.APSService"></service>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
<queries>
<!-- 声明百度地图包名 -->
<package android:name="com.baidu.BaiduMap" />
<!-- 声明要处理的URL Scheme -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="baidumap" />
</intent>
</queries>
</manifest>

541
apps/client/android/app/src/main/kotlin/com/yimai/eaimar/MainActivity.kt

@ -0,0 +1,541 @@
package com.yimai.eaimar
import android.app.PendingIntent
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanSettings
import android.content.Intent
import android.os.Build
import android.os.Bundle
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.embedding.android.FlutterActivity
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.plugin.common.MethodChannel // 添加这行导入
import android.net.Uri
import android.provider.OpenableColumns
import java.io.File
import java.io.FileOutputStream
import org.json.JSONObject
class MainActivity: FlutterActivity() {
private val TAG = "MainActivity"
private var pendingDeepLink: Intent? = null
private var pendingNavigationIntent: Intent? = null
private var pendingShareIntent: Intent? = null
private var pendingSharedFilePath: String? = null
private var pendingDeepLinkUrl: String? = null
private val EXTRA_NAVIGATE_TO = "com.yimai.eaimar.extra.NAVIGATE_TO"
private val NAVIGATE_TO_MEETING_RECORD = "meetingRecord"
private val ROUTE_MEETING_RECORD = "/meeting/record"
private val DEEPLINK_CHANNEL = "com.yimai.eaimar/deeplink"
private val DELIVERY_RETRY_DELAY_MS = 300L
private val MAX_DELIVERY_RETRIES = 12
private val NATIVE_LOCALIZATION_CHANNEL = "com.yimai.eaimar/native_localization"
private val NATIVE_LOCALIZATION_PREF_NAME = "native_localization"
private val NATIVE_LOCALIZATION_PREF_KEY_LOCALE = "locale"
private val NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON = "strings_json"
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(TAG, "🚀 MainActivity onCreate 开始")
super.onCreate(savedInstanceState)
// 检查 Intent 信息
intent?.let {
Log.d(TAG, "Intent action: ${it.action}")
Log.d(TAG, "Intent data: ${it.data}")
Log.d(TAG, "Intent extras: ${it.extras}")
}
intent?.let {
if (it.action == Intent.ACTION_SEND) {
pendingShareIntent = it
Log.d(TAG, "📎 暂存分享文件意图,等待Flutter引擎准备完成")
} else if (it.action == Intent.ACTION_VIEW && it.data != null) {
val uri = it.data
if (uri != null && isLikelyAudio(it, uri)) {
pendingShareIntent = it
Log.d(TAG, "📎 暂存打开文件意图,等待Flutter引擎准备完成")
} else {
pendingDeepLink = it
Log.d(TAG, "🔗 暂存深链接,等待Flutter引擎准备完成")
}
}
}
if (intent?.hasExtra(EXTRA_NAVIGATE_TO) == true) {
pendingNavigationIntent = intent
Log.d(TAG, "🧭 暂存导航意图,等待Flutter引擎准备完成")
}
// 设置日志级别,减少系统级日志
suppressSystemLogs()
Log.d(TAG, "✅ MainActivity onCreate 完成")
}
// 添加 onNewIntent 方法处理深链接
override fun onNewIntent(intent: Intent) {
Log.d(TAG, "🔗 onNewIntent 被调用")
super.onNewIntent(intent)
setIntent(intent) // 更新当前 Intent
// 检查新的 Intent 信息
Log.d(TAG, "New Intent action: ${intent.action}")
Log.d(TAG, "New Intent data: ${intent.data}")
Log.d(TAG, "New Intent extras: ${intent.extras}")
intent.let {
if (it.action == Intent.ACTION_SEND) {
pendingShareIntent = it
Log.d(TAG, "📎 暂存分享文件意图,等待Flutter引擎准备完成")
} else if (it.action == Intent.ACTION_VIEW && it.data != null) {
val uri = it.data
if (uri != null && isLikelyAudio(it, uri)) {
pendingShareIntent = it
Log.d(TAG, "📎 暂存打开文件意图,等待Flutter引擎准备完成")
} else {
pendingDeepLink = it
Log.d(TAG, "🔗 暂存深链接,等待Flutter引擎准备完成")
}
}
}
if (intent.hasExtra(EXTRA_NAVIGATE_TO)) {
pendingNavigationIntent = intent
Log.d(TAG, "🧭 暂存导航意图,等待Flutter引擎准备完成")
}
deliverOrQueueIntent(intent)
}
override fun provideFlutterEngine(context: Context): FlutterEngine? {
Log.d(TAG, "🔍 provideFlutterEngine 被调用")
val engine = FlutterEngineCache.getInstance().get("main_engine")
if (engine == null) {
Log.w(TAG, "❌ FlutterEngine 缓存为空,这可能导致问题")
// 如果缓存为空,让 Flutter 框架创建默认 Engine
return null
}
// 检查 Engine 状态 - 使用更兼容的方式
try {
// 检查 Dart 执行器是否正在运行
val isExecutorRunning = engine.dartExecutor.isExecutingDart
Log.d(TAG, "FlutterEngine Dart 执行状态: $isExecutorRunning")
// 如果需要,可以检查其他状态
if (!isExecutorRunning) {
Log.w(TAG, "⚠️ FlutterEngine Dart 执行器未运行,可能需要重新初始化")
}
} catch (e: Exception) {
Log.w(TAG, "❌ 检查 FlutterEngine 状态时出错", e)
}
Log.d(TAG, "✅ 使用缓存的 FlutterEngine")
return engine
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
Log.d(TAG, "🔧 configureFlutterEngine 开始")
// 检查插件是否已经注册,避免重复注册
val pluginRegistry = flutterEngine.plugins
// 只有在插件未注册时才调用 GeneratedPluginRegistrant
@Suppress("UNCHECKED_CAST")
val pathProviderClass = try {
Class.forName("io.flutter.plugins.pathprovider.PathProviderPlugin")
as Class<out io.flutter.embedding.engine.plugins.FlutterPlugin>
} catch (e: ClassNotFoundException) {
null
}
if (pathProviderClass == null || !pluginRegistry.has(pathProviderClass)) {
Log.d(TAG, "📦 注册 Flutter 插件")
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
} else {
Log.d(TAG, "⚠️ 插件已注册,跳过重复注册")
}
setupNativeLocalizationChannel(flutterEngine)
// Flutter引擎配置完成后,处理待处理的深链接
Handler(Looper.getMainLooper()).postDelayed({
pendingShareIntent?.let { intent ->
Log.d(TAG, "📎 处理待处理的分享/打开文件意图")
deliverOrQueueIntent(intent)
pendingShareIntent = null
}
pendingDeepLink?.let { intent ->
Log.d(TAG, "🔗 处理待处理的深链接")
deliverOrQueueIntent(intent)
pendingDeepLink = null
}
pendingNavigationIntent?.let { intent ->
Log.d(TAG, "🧭 处理待处理的导航意图")
deliverOrQueueIntent(intent)
pendingNavigationIntent = null
}
}, 1000) // 延迟1秒确保Flutter完全准备好
Log.d(TAG, "✅ configureFlutterEngine 完成")
}
private fun setupNativeLocalizationChannel(flutterEngine: FlutterEngine) {
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, NATIVE_LOCALIZATION_CHANNEL)
channel.setMethodCallHandler { call, result ->
when (call.method) {
"updateNativeStrings" -> {
try {
val args = call.arguments as? Map<*, *>
val locale = args?.get("locale") as? String
val strings = args?.get("strings") as? Map<*, *>
val obj = JSONObject()
strings?.forEach { (k, v) ->
val key = k as? String ?: return@forEach
val value = v as? String ?: return@forEach
obj.put(key, value)
}
val prefs = getSharedPreferences(NATIVE_LOCALIZATION_PREF_NAME, Context.MODE_PRIVATE)
prefs.edit()
.putString(NATIVE_LOCALIZATION_PREF_KEY_LOCALE, locale)
.putString(NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON, obj.toString())
.apply()
result.success(null)
} catch (e: Exception) {
result.error("NATIVE_LOCALIZATION_ERROR", e.message, null)
}
}
else -> result.notImplemented()
}
}
}
private fun deliverOrQueueIntent(intent: Intent?) {
intent ?: return
val navigateTo = intent.getStringExtra(EXTRA_NAVIGATE_TO)
if (navigateTo == NAVIGATE_TO_MEETING_RECORD) {
pushRouteToFlutter(ROUTE_MEETING_RECORD)
return
}
if (intent.action == Intent.ACTION_SEND ||
intent.action == Intent.ACTION_SEND_MULTIPLE ||
intent.action == Intent.ACTION_VIEW
) {
try {
val path = extractAudioPathFromIntent(intent)
if (path != null) {
pendingSharedFilePath = path
deliverPendingSharedFile()
return
}
} catch (e: Exception) {
Log.e(TAG, "❌ 处理分享/打开文件失败", e)
}
}
if (intent.action == Intent.ACTION_VIEW && intent.data != null) {
val uri = intent.data
Log.d(TAG, "🔗 处理深链接: $uri")
pendingDeepLinkUrl = uri.toString()
deliverPendingDeepLink()
return
}
Log.d(TAG, "🔗 Intent 不是深链接或数据为空")
}
private fun getEngineForMessaging(): FlutterEngine? {
return flutterEngine ?: FlutterEngineCache.getInstance().get("main_engine")
}
private fun deliverPendingSharedFile(attempt: Int = 0) {
val filePath = pendingSharedFilePath ?: return
val engine = getEngineForMessaging()
val messenger = engine?.dartExecutor?.binaryMessenger
if (messenger == null) {
if (attempt < MAX_DELIVERY_RETRIES) {
Handler(Looper.getMainLooper()).postDelayed(
{ deliverPendingSharedFile(attempt + 1) },
DELIVERY_RETRY_DELAY_MS
)
} else {
Log.w(TAG, "❌ FlutterEngine 未就绪,放弃发送分享文件:$filePath")
}
return
}
val channel = MethodChannel(messenger, DEEPLINK_CHANNEL)
channel.invokeMethod(
"handleSharedFile",
mapOf("filePath" to filePath),
object : MethodChannel.Result {
override fun success(result: Any?) {
pendingSharedFilePath = null
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
Log.e(TAG, "❌ Flutter 处理分享文件返回错误:$errorCode, $errorMessage")
}
override fun notImplemented() {
if (attempt < MAX_DELIVERY_RETRIES) {
Handler(Looper.getMainLooper()).postDelayed(
{ deliverPendingSharedFile(attempt + 1) },
DELIVERY_RETRY_DELAY_MS
)
} else {
Log.w(TAG, "❌ Flutter 未注册分享处理器,放弃发送分享文件:$filePath")
}
}
}
)
}
private fun deliverPendingDeepLink(attempt: Int = 0) {
val url = pendingDeepLinkUrl ?: return
val engine = getEngineForMessaging()
val messenger = engine?.dartExecutor?.binaryMessenger
if (messenger == null) {
if (attempt < MAX_DELIVERY_RETRIES) {
Handler(Looper.getMainLooper()).postDelayed(
{ deliverPendingDeepLink(attempt + 1) },
DELIVERY_RETRY_DELAY_MS
)
} else {
Log.w(TAG, "❌ FlutterEngine 未就绪,放弃发送深链接:$url")
}
return
}
val channel = MethodChannel(messenger, DEEPLINK_CHANNEL)
val arguments = buildUrlArguments(url)
channel.invokeMethod(
"handleDeepLink",
arguments,
object : MethodChannel.Result {
override fun success(result: Any?) {
pendingDeepLinkUrl = null
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
Log.e(TAG, "❌ Flutter 处理深链接返回错误:$errorCode, $errorMessage")
}
override fun notImplemented() {
if (attempt < MAX_DELIVERY_RETRIES) {
Handler(Looper.getMainLooper()).postDelayed(
{ deliverPendingDeepLink(attempt + 1) },
DELIVERY_RETRY_DELAY_MS
)
} else {
Log.w(TAG, "❌ Flutter 未注册深链接处理器,放弃发送深链接:$url")
}
}
}
)
}
private fun extractAudioPathFromIntent(intent: Intent): String? {
val uri: Uri? = when (intent.action) {
Intent.ACTION_SEND -> {
val extraStream = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
extraStream ?: intent.clipData?.takeIf { it.itemCount > 0 }?.getItemAt(0)?.uri
}
Intent.ACTION_SEND_MULTIPLE -> {
val items = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
items?.firstOrNull() ?: intent.clipData?.takeIf { it.itemCount > 0 }?.getItemAt(0)?.uri
}
Intent.ACTION_VIEW -> intent.data
else -> null
}
if (uri == null) return null
if (!isLikelyAudio(intent, uri)) return null
return copyUriToCache(uri)
}
private fun isLikelyAudio(intent: Intent, uri: Uri): Boolean {
val type = intent.type ?: ""
if (type.startsWith("audio/")) return true
val scheme = uri.scheme ?: ""
if (scheme == "content" || scheme == "file") return true
val path = uri.toString().lowercase()
return path.endsWith(".wav") || path.endsWith(".mp3") || path.endsWith(".m4a") || path.endsWith(".aac")
}
private fun copyUriToCache(uri: Uri): String? {
return try {
val resolver = applicationContext.contentResolver
val cursor = resolver.query(uri, null, null, null, null)
var displayName = "shared_audio"
cursor?.use {
val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (nameIndex >= 0 && it.moveToFirst()) {
displayName = it.getString(nameIndex) ?: displayName
}
}
val ext = when {
displayName.endsWith(".wav", true) -> "wav"
displayName.endsWith(".mp3", true) -> "mp3"
displayName.endsWith(".m4a", true) -> "m4a"
displayName.endsWith(".aac", true) -> "aac"
else -> displayName.substringAfterLast('.', "audio")
}
val sharedDir = File(applicationContext.filesDir, "shared_audio")
if (!sharedDir.exists()) sharedDir.mkdirs()
val outFile = File(sharedDir, "audio_${System.currentTimeMillis()}.$ext")
val inStream = resolver.openInputStream(uri)
if (inStream == null) {
outFile.delete()
return null
}
inStream.use {
FileOutputStream(outFile).use { out ->
val buffer = ByteArray(8192)
var len: Int
while (true) {
len = it.read(buffer)
if (len <= 0) break
out.write(buffer, 0, len)
}
}
}
if (!outFile.exists() || outFile.length() <= 0) {
outFile.delete()
return null
}
outFile.absolutePath
} catch (e: Exception) {
Log.e(TAG, "复制分享文件到缓存失败: ${e.message}", e)
null
}
}
private fun pushRouteToFlutter(route: String) {
try {
val engine = flutterEngine
if (engine == null) {
Log.w(TAG, "❌ FlutterEngine 为空,无法 pushRoute")
return
}
Handler(Looper.getMainLooper()).post {
try {
engine.navigationChannel.pushRoute(route)
} catch (e: Exception) {
Log.e(TAG, "❌ pushRoute 失败", e)
}
}
} catch (e: Exception) {
Log.e(TAG, "❌ pushRouteToFlutter 出错", e)
}
}
private fun buildUrlArguments(url: String): Map<String, Any?> {
return try {
val uri = android.net.Uri.parse(url)
mapOf(
"url" to url,
"scheme" to uri.scheme,
"host" to uri.host,
"path" to uri.path,
"query" to uri.query,
"queryParameters" to uri.queryParameterNames.associateWith { uri.getQueryParameter(it) }
)
} catch (e: Exception) {
mapOf("url" to url)
}
}
override fun onDestroy() {
super.onDestroy()
}
/**
* 抑制系统级组件的日志输出
*/
private fun suppressSystemLogs() {
// 更全面的系统标签列表
val systemTags = arrayOf(
"MediaCodec",
"CCodec",
"CCodecBufferChannel",
"OpusManager",
"MediaCodecList",
"MediaPlayer",
"AudioTrack",
"AudioManager",
"AudioFlinger",
"AudioService",
"AudioEffect",
"AppOpsManager",
"AudioRecord",
"OpenGLRenderer",
"Choreographer",
"InputMethodManager",
"ViewRootImpl",
"Surface",
"BufferQueueProducer",
"ActivityManager",
"WindowManager",
"SensorManager"
)
// 尝试多种方式设置日志级别
systemTags.forEach { tag ->
try {
// 方式1:标准属性设置
System.setProperty("log.tag.$tag", "ERROR")
// 方式2:尝试使用反射设置(针对某些特殊标签)
setLogLevelViaReflection(tag, Log.ERROR)
} catch (e: Exception) {
// 忽略设置失败的情况
}
}
// 方式3:尝试设置全局日志级别
try {
// 设置更高的阈值,减少verbose和debug日志
System.setProperty("log.tag", "WARN")
} catch (e: Exception) {
// 忽略
}
}
/**
* 通过反射尝试设置日志级别
*/
private fun setLogLevelViaReflection(tag: String, level: Int) {
try {
val logClass = Class.forName("android.util.Log")
val isLoggableMethod = logClass.getDeclaredMethod(
"isLoggable",
String::class.java,
Int::class.javaPrimitiveType
)
isLoggableMethod.isAccessible = true
// 某些系统可能支持直接设置
val setLogLevelMethod = logClass.getDeclaredMethod(
"setLogLevel",
String::class.java,
Int::class.javaPrimitiveType
)
setLogLevelMethod.isAccessible = true
setLogLevelMethod.invoke(null, tag, level)
} catch (e: Exception) {
// 反射方法可能不存在,忽略
}
}
}

196
apps/client/android/app/src/main/kotlin/com/yimai/eaimar/MyApplication.kt

@ -0,0 +1,196 @@
package com.yimai.eaimar
import android.app.ActivityManager
import android.content.Context
import android.util.Log
import io.flutter.app.FlutterApplication
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugins.GeneratedPluginRegistrant
import java.io.OutputStream
import java.io.PrintStream
// Application.kt
class MyApp : FlutterApplication() {
companion object {
private const val TAG = "MyApp"
private const val ENGINE_ID = "main_engine"
// 标记 Engine 是否已经执行过 Dart 入口点
@Volatile
private var isDartEntrypointExecuted = false
// 记录应用启动时间,用于判断是否为新启动
@Volatile
private var appStartTime = 0L
}
override fun onCreate() {
super.onCreate()
appStartTime = System.currentTimeMillis()
Log.d(TAG, "MyApp onCreate - 进程启动")
// 屏蔽高德定位 SDK(CgiManager) 的 NumberFormatException 噪音日志
installAmapStderrFilter()
val cache = FlutterEngineCache.getInstance()
val existingEngine = cache.get(ENGINE_ID)
if (existingEngine == null) {
Log.d(TAG, "创建新的 FlutterEngine")
createAndCacheEngine(cache)
} else {
Log.d(TAG, "FlutterEngine 已存在,检查状态")
// 检查现有 Engine 的状态
try {
val isExecuting = existingEngine.dartExecutor.isExecutingDart
Log.d(TAG, "Engine Dart执行状态: $isExecuting")
if (!isExecuting && isMainProcess()) {
Log.d(TAG, "Engine 存在但Dart未执行,重新执行")
existingEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
isDartEntrypointExecuted = true
}
} catch (e: Exception) {
Log.e(TAG, "检查或重新执行 Dart 入口点失败", e)
// 如果失败,重新创建 Engine
cache.remove(ENGINE_ID)
createAndCacheEngine(cache)
}
}
}
private fun createAndCacheEngine(cache: FlutterEngineCache) {
// 在主进程中总是创建并执行Dart入口点
if (isMainProcess()) {
Log.d(TAG, "✅ 在主进程创建 FlutterEngine 并执行 Dart")
val flutterEngine = FlutterEngine(this)
// 注册插件(只在这里注册一次)
Log.d(TAG, "📦 注册 Flutter 插件")
GeneratedPluginRegistrant.registerWith(flutterEngine)
try {
Log.d(TAG, "🚀 开始执行 Dart 入口点")
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
isDartEntrypointExecuted = true
cache.put(ENGINE_ID, flutterEngine)
Log.d(TAG, "✅ FlutterEngine 创建并缓存成功")
} catch (e: Exception) {
Log.e(TAG, "❌ 执行 Dart 入口点失败", e)
// 如果执行失败,仍然缓存 Engine,但标记为未执行
isDartEntrypointExecuted = false
cache.put(ENGINE_ID, flutterEngine)
}
} else {
Log.d(TAG, "⚠️ 在非主进程,仅创建 FlutterEngine")
// 非主进程只创建 Engine,不执行 Dart 入口点
val flutterEngine = FlutterEngine(this)
cache.put(ENGINE_ID, flutterEngine)
}
}
private fun isMainProcess(): Boolean {
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val processes = am.runningAppProcesses ?: return true
val mainProcessName = packageName
val myPid = android.os.Process.myPid()
for (process in processes) {
if (process.pid == myPid) {
return process.processName == mainProcessName
}
}
return true
}
/**
* 过滤 System.err 中来自高德 SDK 的 NumberFormatException 栈(com.autonavi.aps.amapapi / CgiManager / ApsManager)。
* 仅丢弃这类已知噪音,其他 stderr 输出原样打印。
*/
private fun installAmapStderrFilter() {
val origin = System.err ?: return
val noiseKeywords = arrayOf(
"com.autonavi.aps.amapapi",
"CgiManager",
"ApsManager",
"onCellLocationChanged",
"PhoneStateListener",
)
val bufferRef = StringBuilder()
var suppressing = false
val filtered = PrintStream(object : OutputStream() {
override fun write(b: Int) {
val ch = b.toChar()
bufferRef.append(ch)
if (ch == '\n') {
val line = bufferRef.toString()
bufferRef.setLength(0)
val trimmed = line.trimStart()
if (trimmed.startsWith("java.lang.NumberFormatException")) {
suppressing = true
return
}
if (suppressing) {
// 只要是栈帧 (at xxx) 或 caused by,就继续丢弃
if (trimmed.startsWith("at ") ||
trimmed.startsWith("Caused by") ||
trimmed.startsWith("... ") ||
noiseKeywords.any { trimmed.contains(it) }
) {
return
}
suppressing = false
}
origin.print(line)
}
}
}, true)
System.setErr(filtered)
Log.d(TAG, "已安装高德 SDK stderr 过滤器")
}
private fun isAppInForeground(): Boolean {
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val processes = am.runningAppProcesses ?: return false
val pkg = packageName
for (p in processes) {
if (p.processName == pkg &&
p.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
return true
}
}
return false
}
private fun isAppBeingLaunched(): Boolean {
// 改进的启动检测逻辑
val currentTime = System.currentTimeMillis()
val isRecentStart = (currentTime - appStartTime) < 3000 // 3秒内认为是新启动
if (isRecentStart) {
Log.d(TAG, "检测到应用新启动")
return true
}
// 备用检测方法(兼容旧版本)
return try {
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val tasks = am.getRunningTasks(1)
val result = tasks.isNotEmpty() && tasks[0].topActivity?.packageName == packageName
Log.d(TAG, "通过 RunningTasks 检测启动状态: $result")
result
} catch (e: Exception) {
Log.w(TAG, "无法获取 RunningTasks,可能缺少权限", e)
// 如果无法获取任务信息,默认认为是启动状态
true
}
}
}

12
apps/client/android/app/src/main/res/drawable-v21/launch_background.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

12
apps/client/android/app/src/main/res/drawable/launch_background.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

BIN
apps/client/android/app/src/main/res/mipmap-hdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
apps/client/android/app/src/main/res/mipmap-mdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
apps/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
apps/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
apps/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
apps/client/android/app/src/main/res/raw/listening.mp3

Binary file not shown.

BIN
apps/client/android/app/src/main/res/raw/sample.mp3

Binary file not shown.

BIN
apps/client/android/app/src/main/res/raw/silence.mp3

Binary file not shown.

18
apps/client/android/app/src/main/res/values-night/styles.xml

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

3
apps/client/android/app/src/main/res/values/colors.xml

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

6
apps/client/android/app/src/main/res/values/strings.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="facebook_app_id">3829022037361855</string>
<string name="fb_login_protocol_scheme">fb3829022037361855</string>
<string name="facebook_client_token">193d0a2b1757c41cfa557ea8e8f21c33</string>
</resources>

18
apps/client/android/app/src/main/res/values/styles.xml

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

13
apps/client/android/app/src/main/res/xml/network_security_config.xml

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">127.0.0.1</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>

7
apps/client/android/app/src/profile/AndroidManifest.xml

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

44
apps/client/android/build.gradle.kts

@ -0,0 +1,44 @@
allprojects {
repositories {
mavenLocal()
google()
mavenCentral()
maven { url = uri("https://storage.googleapis.com/download.flutter.io") }
// 添加火山引擎Maven仓库
maven {
url = uri("https://artifact.bytedance.com/repository/Volcengine/")
}
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") }
// 修改为绝对路径,避免相对路径计算错误
flatDir {
dirs(file("${rootProject.projectDir}/../local_plugins/qq_music/android/libs"))
}
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20")
// 其他依赖...
}
}

4
apps/client/android/gradle.properties

@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
android.max.page.size=16384

5
apps/client/android/gradle/wrapper/gradle-wrapper.properties

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip

60
apps/client/android/settings.gradle.kts

@ -0,0 +1,60 @@
// This file configures the Flutter project
// Used by the Flutter tool to generate GeneratedPluginRegistrant.java
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
val localPropertiesFile = file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.inputStream().use { properties.load(it) }
}
val flutterSdkPath = properties.getProperty("flutter.sdk")
requireNotNull(flutterSdkPath) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
mavenLocal()
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader")
id("com.android.application") version "8.9.1" apply false
id("com.android.library") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.10" apply false
id("com.google.protobuf") version "0.9.4" apply false
}
include(":app")
include(":azure_speech")
include(":chat_storage")
include(":jieli")
include(":ble_service")
include(":classic_bluetooth")
include(":agent_service")
include(":speech")
include(":chat_api")
include(":bytedance_speech")
include(":qq_music") // 添加这行
include(":jl_opus")
include(":bluetooth_manager") // 恒玄(BES)经典蓝牙连接 + G722/mSBC 音频编解码
// 设置项目路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":chat_storage").projectDir = file("../local_plugins/chat_storage/android")
project(":jieli").projectDir = file("../local_plugins/jieli/android")
project(":ble_service").projectDir = file("../local_plugins/ble_service/android")
project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android")
project(":agent_service").projectDir = file("../local_plugins/agent_service/android")
project(":speech").projectDir = file("../local_plugins/speech/android")
project(":chat_api").projectDir = file("../local_plugins/chat_api/android")
project(":bytedance_speech").projectDir = file("../local_plugins/bytedance_speech/android")
project(":qq_music").projectDir = file("../local_plugins/qq_music/android") // 添加这行
project(":jl_opus").projectDir = file("../local_plugins/jl_opus/android")
project(":bluetooth_manager").projectDir = file("../local_plugins/bluetooth_manager/android")

30
apps/client/assets/config/GoogleService-Info.plist

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyBEQPL-2Qh6fERxrxLP9Pzcm95mTmTpBxE</string>
<key>GCM_SENDER_ID</key>
<string>733281995296</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.saitong.voitrans</string>
<key>PROJECT_ID</key>
<string>voitrans</string>
<key>STORAGE_BUCKET</key>
<string>voitrans.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:733281995296:ios:6a8351691312100bd70a45</string>
</dict>
</plist>

BIN
apps/client/assets/fonts/NotoSansSC-Regular.ttf

Binary file not shown.

BIN
apps/client/assets/images/DefaultPicture.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
apps/client/assets/images/agents/assistant.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
apps/client/assets/images/agents/doctor.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
apps/client/assets/images/agents/girlfriend.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 KiB

BIN
apps/client/assets/images/agents/interpreter.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
apps/client/assets/images/agents/lily.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
apps/client/assets/images/agents/love_guru.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
apps/client/assets/images/agents/mei_hua.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
apps/client/assets/images/agents/shopping.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
apps/client/assets/images/agents/socrates.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

BIN
apps/client/assets/images/agents/speaking_coach.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
apps/client/assets/images/agents/storyteller.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
apps/client/assets/images/agents/tarot.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
apps/client/assets/images/defaultPicture.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

BIN
apps/client/assets/images/devices/earphone.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
apps/client/assets/images/devices/record_card.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
apps/client/assets/images/devices/record_card.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

BIN
apps/client/assets/images/explore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

BIN
apps/client/assets/images/explore1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB

BIN
apps/client/assets/images/explore2.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

BIN
apps/client/assets/images/glasses_light.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
apps/client/assets/images/mine-background.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

BIN
apps/client/assets/images/music.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
apps/client/assets/images/qqmusic.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
apps/client/assets/images/robot_light.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

2
apps/client/assets/js/d3@7.js

File diff suppressed because one or more lines are too long

10
apps/client/assets/js/markmap-lib.js

File diff suppressed because one or more lines are too long

9
apps/client/assets/js/markmap-toolbar.js

File diff suppressed because one or more lines are too long

10
apps/client/assets/js/markmap-view.js

File diff suppressed because one or more lines are too long

8
apps/client/assets/js/saveSvgAsPng.min.js

File diff suppressed because one or more lines are too long

BIN
apps/client/assets/listening.mp3

Binary file not shown.

1
apps/client/assets/lottie/audio_wave.json

File diff suppressed because one or more lines are too long

1
apps/client/assets/lottie/intro_foriday.json

File diff suppressed because one or more lines are too long

1
apps/client/assets/lottie/meeting_wait.json

File diff suppressed because one or more lines are too long

1
apps/client/assets/lottie/sound_effect.json

File diff suppressed because one or more lines are too long

3
apps/client/assets/weather/100-fill.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="qi-100-fill" viewBox="0 0 16 16">
<path d="M8.005 3.5a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9Zm.004-.997a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 1 0v1.5a.5.5 0 0 1-.5.5ZM3.766 4.255a.498.498 0 0 1-.353-.147l-1.062-1.06a.5.5 0 0 1 .707-.707L4.122 3.4a.5.5 0 0 1-.355.854v.001ZM2.004 8.493h-1.5a.5.5 0 1 1 0-1h1.5a.5.5 0 1 1 0 1Zm.691 5.303a.5.5 0 0 1-.354-.854l1.062-1.06a.5.5 0 0 1 .708.707l-1.063 1.06a.497.497 0 0 1-.353.147Zm5.301 2.201a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 1 0v1.5a.5.5 0 0 1-.5.5Zm5.304-2.191a.496.496 0 0 1-.353-.147l-1.06-1.06a.5.5 0 1 1 .706-.707l1.06 1.06a.5.5 0 0 1-.353.854Zm2.203-5.299h-1.5a.5.5 0 0 1 0-1h1.5a.5.5 0 1 1 0 1ZM12.25 4.265a.5.5 0 0 1-.354-.854l1.06-1.06a.5.5 0 1 1 .708.707l-1.06 1.06a.498.498 0 0 1-.354.147Z"/>
</svg>

After

Width:  |  Height:  |  Size: 839 B

3
apps/client/assets/weather/100.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="qi-100" viewBox="0 0 16 16">
<path d="M7.655 2.357a.5.5 0 0 0 .854-.353v-1.5a.5.5 0 0 0-1 0v1.5a.5.5 0 0 0 .146.353Zm-4.08 1.861c.06.026.126.039.191.039l.001-.001a.5.5 0 0 0 .355-.855l-1.064-1.06a.5.5 0 0 0-.707.708l1.062 1.06a.498.498 0 0 0 .162.11ZM.503 8.496h1.5a.5.5 0 1 0 0-1h-1.5a.5.5 0 0 0 0 1Zm1.914 5.221a.501.501 0 0 0 .631-.063l1.063-1.06a.5.5 0 0 0-.708-.707l-1.062 1.06a.5.5 0 0 0 .076.77Zm5.225 2.14a.5.5 0 0 0 .854-.354v-1.5a.5.5 0 0 0-1 0v1.5a.5.5 0 0 0 .146.354Zm5.467-2.084a.5.5 0 0 0 .544-.816l-1.06-1.06a.498.498 0 0 0-.832.152.5.5 0 0 0 .126.555l1.06 1.06a.496.496 0 0 0 .162.109Zm.893-5.263h1.5a.5.5 0 1 0 0-1h-1.5a.5.5 0 0 0 0 1Zm-2.031-4.327a.5.5 0 0 0 .633-.063l1.06-1.06a.5.5 0 1 0-.708-.708l-1.06 1.06a.5.5 0 0 0 .075.77Zm-6.466.075a4.5 4.5 0 1 1 5 7.484 4.5 4.5 0 0 1-5-7.484Zm4.445.832a3.5 3.5 0 1 0-3.89 5.82 3.5 3.5 0 0 0 3.89-5.82Z"/>
</svg>

After

Width:  |  Height:  |  Size: 965 B

3
apps/client/assets/weather/1001.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="qi-1001" viewBox="0 0 16 16">
<path d="M9.21 1.491c-3.757-.066-6.613 2.537-6.88 5.705-.292 3.494 2.107 4.947 2.428 5.232C2.853 12.4.585 10.28.204 8.296a.104.104 0 0 0-.1-.085.103.103 0 0 0-.103.114c.403 3.526 3.405 6.2 6.79 6.186 3.604-.016 6.518-2.147 6.89-5.646.35-3.295-2.108-5.008-2.424-5.292 2.023.02 4.162 2.15 4.54 4.133.008.048.05.084.098.085a.102.102 0 0 0 .1-.071.103.103 0 0 0 .004-.043c-.406-3.521-3.405-6.126-6.788-6.185ZM8 9.503A1.502 1.502 0 1 1 8 6.5a1.502 1.502 0 0 1 0 3.004Z"/>
</svg>

After

Width:  |  Height:  |  Size: 595 B

5
apps/client/assets/weather/1002.svg

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="qi-1002" viewBox="0 0 16 16">
<path d="M2.653 4.48c-.105-.315-.105-.573.014-.791.53-.967 2.19-2.004 4.637-2.443 2.743-.492 4.582.057 5.1.746a.71.71 0 0 1 .033.837c.897-.528 1.158-1.081 1.014-1.58C13.166.246 10.41-.369 7.09.244c-3.417.632-5.456 2.22-5.362 3.229.031.337.38.804.925 1.006Z"/>
<path d="M11.568 3.056c-.178-.621-1.676-1.49-3.99-1.047-2.362.452-3.348 1.804-3.29 2.43.02.208.237.498.575.624-.066-.196-.065-.356.01-.492.327-.6 1.358-1.244 2.877-1.516 1.704-.306 2.845.035 3.166.463a.44.44 0 0 1 .022.52c.556-.329.718-.672.63-.982Z"/>
<path d="M15.583 1.905c.062.674-.27 1.59-1.036 2.482.365.215.778.548.828.978.11.964-.43 2.058-1.606 2.953.008.007.016.012.024.017a.15.15 0 0 1 .03.023 1.278 1.278 0 0 1 .34 1.041c-.058.378-.301.568-.598.8-.3.232-.654.508-.926 1.062-.097.198-.38.559-.564.729-.378.35-.722.4-1.007.44-.18.026-.336.049-.462.141-.294.216-.43.424-.553.612-.101.154-.193.294-.356.415a.915.915 0 0 1-.432.167c-.165.03-.326.06-.519.252-.073.073-.128.19-.188.317-.078.167-.165.351-.31.476-.103.089-.21.146-.306.197-.117.062-.216.115-.27.207-.093.16-.123.34-.147.487-.028.167-.048.292-.149.299-.106.007-.964-1.02-1.167-1.975-.045-.21-.102-.478-.018-.791.022-.085.049-.15.085-.238.031-.078.07-.175.12-.319l.016-.044a2.85 2.85 0 0 0 .134-.446c.034-.234-.267-.455-.457-.595a1.666 1.666 0 0 1-.12-.092c-.149-.138-1.019-.973-.964-1.71 0-.189.048-.376.139-.542-.958-.5-1.593-1.174-1.774-1.808a1.946 1.946 0 0 1-.05-.459C1.513 6.41.583 5.356.424 4.445c-.073-.427.173-.94.554-1.38 0 .001-.035.686.285 1.026.956 1.018 3.98 1.787 7.223 1.262 3.613-.585 5.97-2.628 5.763-3.967-.06-.39-.198-.651-.538-.972 0 0 1.784.537 1.873 1.491Zm-5.95 9.347c1.13-.1 2.536-.664 3.386-1.793.17-.241.273-.524.298-.818-.83.52-1.902.951-3.247 1.199-1.852.341-3.422.098-4.581-.408.03.104.077.201.142.287.54.625 1.978 1.714 4.002 1.533ZM4.122 7.37c1.124.775 3.08 1.268 5.706.85 2.927-.463 4.836-2.11 4.671-3.22a1.227 1.227 0 0 0-.15-.395c-1.082 1.143-2.899 2.206-5.571 2.61-2.004.302-3.616.223-4.876-.072.06.088.134.165.22.227Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

3
apps/client/assets/weather/1003.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="qi-1003" viewBox="0 0 16 16">
<path d="M7.9 10a4.99 4.99 0 0 0 3.827-1.783 3 3 0 1 0 .553-5.63A4.999 4.999 0 0 0 7.9 0a4.998 4.998 0 0 0-4.359 2.549 3 3 0 1 0 .586 5.732A4.988 4.988 0 0 0 7.9 10Zm-.791-6.398c-.057-.362.17-.8.496-.997.256-.153.551-.133.806.023l.07.042a.846.846 0 0 1 .409.853L8.532 5.8H7.454L7.11 3.602ZM8.599 6.9a.6.6 0 1 1-1.2 0 .6.6 0 0 1 1.2 0Zm.344 4.804a.494.494 0 0 0-.157-.641c-.205-.13-.467-.053-.586.172l-2.143 4.061a.494.494 0 0 0 .157.641c.205.13.467.053.586-.172l2.143-4.061ZM2.8 11.556c.191.115.257.37.146.57l-1.2 2.165a.391.391 0 0 1-.546.153.427.427 0 0 1-.146-.57l1.2-2.165a.39.39 0 0 1 .546-.153Zm3 0c.191.115.257.37.146.57l-1.2 2.165a.391.391 0 0 1-.546.153.427.427 0 0 1-.146-.57l1.2-2.165a.39.39 0 0 1 .546-.153Zm9 0c.191.115.257.37.146.57l-1.2 2.165a.391.391 0 0 1-.546.153.427.427 0 0 1-.146-.57l1.2-2.165a.39.39 0 0 1 .546-.153Zm-2.854.569a.427.427 0 0 0-.146-.57.391.391 0 0 0-.546.154l-1.2 2.166a.428.428 0 0 0 .146.57.391.391 0 0 0 .546-.154l1.2-2.166Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

3
apps/client/assets/weather/1004.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="qi-1004" viewBox="0 0 16 16">
<path d="M7.9 10a4.99 4.99 0 0 0 3.827-1.783 3 3 0 1 0 .553-5.63A4.999 4.999 0 0 0 7.9 0a4.998 4.998 0 0 0-4.359 2.549 3 3 0 1 0 .586 5.732A4.988 4.988 0 0 0 7.9 10Zm-.791-6.398c-.057-.362.17-.8.496-.997.256-.153.551-.133.806.023l.07.042a.846.846 0 0 1 .409.853L8.532 5.8H7.454L7.11 3.602ZM8.599 6.9a.6.6 0 1 1-1.2 0 .6.6 0 0 1 1.2 0ZM1.501 10a.35.35 0 0 0-.35.35v.544l-.47-.272a.35.35 0 1 0-.35.606l.47.272-.47.272a.35.35 0 1 0 .35.606l.47-.272v.544a.35.35 0 1 0 .7 0v-.544l.47.272a.35.35 0 1 0 .35-.606l-.47-.272.47-.272a.35.35 0 1 0-.35-.606l-.47.272v-.544a.35.35 0 0 0-.35-.35Zm12.65.35a.35.35 0 1 1 .7 0v.544l.47-.272a.35.35 0 1 1 .35.606l-.47.272.47.272a.35.35 0 1 1-.35.606l-.47-.272v.544a.35.35 0 1 1-.7 0v-.544l-.47.272a.35.35 0 1 1-.35-.606l.47-.272-.47-.272a.35.35 0 1 1 .35-.606l.47.272v-.544Zm-6.5 3a.35.35 0 1 1 .7 0v.544l.47-.272a.35.35 0 1 1 .35.606l-.47.272.47.272a.35.35 0 1 1-.35.606l-.47-.272v.544a.35.35 0 1 1-.7 0v-.544l-.47.272a.35.35 0 1 1-.35-.606l.47-.272-.47-.272a.35.35 0 1 1 .35-.606l.47.272v-.544Zm-3.5-1a.35.35 0 1 1 .7 0v.544l.47-.272a.35.35 0 1 1 .35.606l-.47.272.47.272a.35.35 0 1 1-.35.606l-.47-.272v.544a.35.35 0 1 1-.7 0v-.544l-.47.272a.35.35 0 1 1-.35-.606l.47-.272-.47-.272a.35.35 0 1 1 .35-.606l.47.272v-.544Zm7.35-.35a.35.35 0 0 0-.35.35v.544l-.47-.272a.35.35 0 1 0-.35.606l.47.272-.47.272a.35.35 0 1 0 .35.606l.47-.272v.544a.35.35 0 1 0 .7 0v-.544l.47.272a.35.35 0 1 0 .35-.606l-.47-.272.47-.272a.35.35 0 1 0-.35-.606l-.47.272v-.544a.35.35 0 0 0-.35-.35Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

4
apps/client/assets/weather/1005.svg

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="qi-1005" viewBox="0 0 16 16">
<path d="M8.5.5a.5.5 0 1 0-1 0v1.293l-.646-.647a.5.5 0 1 0-.708.708L7.5 3.207V5c0 .014 0 .028.002.041A2.997 2.997 0 0 0 5.687 6.09a.512.512 0 0 0-.035-.022l-1.553-.896-.495-1.85a.5.5 0 1 0-.966.26l.237.882-1.12-.646a.5.5 0 0 0-.5.866l1.12.646-.884.237a.5.5 0 0 0 .26.966l1.848-.495 1.553.896a.503.503 0 0 0 .036.019 2.994 2.994 0 0 0 0 2.096.508.508 0 0 0-.036.019l-1.553.896-1.849-.495a.5.5 0 0 0-.259.966l.884.237-1.12.646a.5.5 0 1 0 .5.866l1.12-.646-.237.883a.5.5 0 1 0 .966.258l.495-1.849 1.553-.896a.507.507 0 0 0 .035-.022 3 3 0 0 0 1.815 1.048.51.51 0 0 0-.002.04v1.793l-1.354 1.353a.5.5 0 0 0 .708.708l.646-.647V15.5a.5.5 0 0 0 1 0v-1.293l.646.647a.5.5 0 0 0 .708-.708L8.5 12.793V10.99c0-.01 0-.02-.002-.031H8.5V5.04h-.002l.002-.03V3.207l1.354-1.353a.5.5 0 1 0-.707-.708l-.647.647V.5Z"/>
<path d="M16 5.064 13.897 4l-1.946.985-1.722-.435-.229.928L12.061 6l1.836-.93 1.68.85.423-.856Zm0 3L13.897 7l-1.946.985-1.722-.435-.229.928L12.061 9l1.836-.93 1.68.85.423-.856ZM13.897 10 16 11.065l-.423.856-1.68-.85-1.836.929L10 11.478l.23-.928 1.721.435L13.897 10Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

4
apps/client/assets/weather/1006.svg

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="qi-1006" viewBox="0 0 16 16">
<path d="M15.497 3.077S7.525.722 4.845.023a.794.794 0 0 0-.956.568L.024 15a.81.81 0 0 0 .544.968.811.811 0 0 0 1-.554l1.671-6.23 12.373-4.817a.696.696 0 0 0-.115-1.291Z"/>
<path d="M11.756 7.71a.296.296 0 0 0-.512 0L7.04 15a.292.292 0 0 0 .256.438h8.41a.292.292 0 0 0 .256-.437L11.756 7.71Zm-.956 2.41c-.036-.317.287-.59.7-.59.412 0 .736.273.7.59l-.316 2.785h-.768L10.8 10.12Zm1.267 3.91a.563.563 0 1 1-1.125 0 .563.563 0 0 1 1.125 0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 568 B

4
apps/client/assets/weather/1007.svg

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="qi-1007" viewBox="0 0 16 16">
<path d="M15.859 8.141 14.34 9.66a.2.2 0 0 1-.34-.143V9H1a1 1 0 1 1 0-2h13v-.517a.2.2 0 0 1 .341-.142L15.86 7.86a.2.2 0 0 1 0 .282Z"/>
<path d="M10.462 2.99c.388.39 1.052.476 1.516.156.44-.303.536-.862.177-1.238a7.232 7.232 0 0 0-1.52-1.241C9.91.222 9.06 0 8.082 0 6.82 0 5.788.354 4.984 1.063c-.788.694-1.182 1.548-1.182 2.562 0 .472.102.944.307 1.417.205.472.528.916.97 1.333.191.174.447.383.768.625h3.366a24.36 24.36 0 0 0-.162-.104c-1.356-.89-2.223-1.528-2.6-1.917-.38-.389-.568-.84-.568-1.354 0-.528.197-.958.59-1.292.41-.333.907-.5 1.49-.5.6 0 1.12.132 1.561.396.276.156.588.41.938.761ZM11.725 9c.12.126.229.251.33.375.63.778.945 1.597.945 2.458 0 1.125-.497 2.104-1.49 2.938C10.533 15.59 9.39 16 8.082 16c-2.038 0-3.686-.887-4.946-2.662-.284-.4-.105-.918.36-1.154a1.175 1.175 0 0 1 1.43.328c.886 1.159 1.859 1.738 2.92 1.738.819 0 1.52-.23 2.104-.688.583-.458.875-.993.875-1.604 0-.61-.245-1.187-.733-1.729-.317-.36-.783-.77-1.398-1.229h3.031Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

5
apps/client/assets/weather/1008.svg

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="qi-1008" viewBox="0 0 16 16">
<path d="M11.5 7a.5.5 0 0 0-.5.5v2.063a2 2 0 1 0 1 0V7.5a.5.5 0 0 0-.5-.5Z"/>
<path d="m10.2 8.399-.532.356a3.3 3.3 0 1 0 3.665 0l-.533-.356V2.5a1.3 1.3 0 1 0-2.6 0v5.899ZM9 2.5a2.5 2.5 0 0 1 5 0v5.258a4.5 4.5 0 1 1-5 0V2.5Zm-5.465-.033a.467.467 0 0 1 .933 0v.725l.628-.363a.467.467 0 1 1 .467.808L4.935 4l.628.363a.467.467 0 0 1-.467.808l-.628-.363v.725a.467.467 0 1 1-.933 0v-.725l-.628.363a.467.467 0 0 1-.467-.808L3.068 4l-.628-.363a.467.467 0 1 1 .467-.808l.628.363v-.725Z"/>
<path d="M7.324 1.872 4.175.048a.347.347 0 0 0-.35 0L.675 1.872a.352.352 0 0 0-.175.304v3.648c0 .126.067.242.175.305l3.15 1.824a.344.344 0 0 0 .35 0l3.149-1.824a.351.351 0 0 0 .176-.305V2.176a.353.353 0 0 0-.176-.304Zm-.526 3.75L4 7.241l-2.798-1.62V2.38L4 .758 6.798 2.38v3.242Z"/>
</svg>

After

Width:  |  Height:  |  Size: 895 B

4
apps/client/assets/weather/1009.svg

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="qi-1009" viewBox="0 0 16 16">
<path d="M3 2a.6.6 0 0 0-.6.6v1.8H.6a.6.6 0 1 0 0 1.2h1.8v1.8a.6.6 0 0 0 1.2 0V5.6h1.8a.6.6 0 1 0 0-1.2H3.6V2.6A.6.6 0 0 0 3 2Zm8.5 1a.5.5 0 0 0-.5.5v6.063a2 2 0 1 0 1 0V3.5a.5.5 0 0 0-.5-.5Z"/>
<path d="m10.2 8.399-.532.356a3.3 3.3 0 1 0 3.665 0l-.533-.356V2.5a1.3 1.3 0 1 0-2.6 0v5.899ZM9 2.5a2.5 2.5 0 0 1 5 0v5.258a4.5 4.5 0 1 1-5 0V2.5Z"/>
</svg>

After

Width:  |  Height:  |  Size: 475 B

3
apps/client/assets/weather/101-fill.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="qi-101-fill" viewBox="0 0 16 16">
<path d="M4.745 1.777a.516.516 0 1 0 1.007-.224L5.496.403A.516.516 0 0 0 4.49.627l.255 1.15ZM1.023 3.535l.994.633a.516.516 0 0 0 .554-.87l-.994-.633a.516.516 0 0 0-.554.87ZM.628 8.043l1.15-.256a.516.516 0 0 0-.223-1.008l-1.15.256a.516.516 0 1 0 .223 1.008Zm10.238-2.28a.535.535 0 0 0 .112-.012l1.15-.256a.516.516 0 1 0-.224-1.008l-1.15.256a.516.516 0 0 0 .112 1.02ZM8.522 2.728a.516.516 0 0 0 .712-.158l.633-.994a.516.516 0 0 0-.87-.554l-.633.994a.516.516 0 0 0 .158.712ZM2.819 7.032c.071.303.182.596.331.87a3.13 3.13 0 0 0 .908-.486 2.453 2.453 0 0 1-.232-.608A2.504 2.504 0 0 1 8.714 5.72l.004.038a5.42 5.42 0 0 1 1.064.25 3.51 3.51 0 0 0-.061-.512 3.535 3.535 0 0 0-6.902 1.536Zm9.175 7.364A4.758 4.758 0 0 1 8.406 16a4.76 4.76 0 0 1-3.537-1.547 2.908 2.908 0 0 1-1.056.197C2.258 14.65 1 13.441 1 11.95s1.26-2.7 2.813-2.7c.173 0 .342.015.507.044C5.124 7.924 6.652 7 8.406 7c1.769 0 3.308.94 4.107 2.328a2.93 2.93 0 0 1 .675-.078c1.553 0 2.812 1.209 2.812 2.7s-1.26 2.7-2.813 2.7a2.9 2.9 0 0 1-1.193-.254Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

3
apps/client/assets/weather/101.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="qi-101" viewBox="0 0 16 16">
<path d="M8.406 15.985a4.766 4.766 0 0 0 3.446-1.449.323.323 0 0 1 .341-.074c.31.112.645.173.995.173 1.553 0 2.812-1.209 2.812-2.7s-1.26-2.7-2.813-2.7c-.157 0-.312.013-.463.037a.32.32 0 0 1-.32-.137c-.824-1.29-2.306-2.15-3.998-2.15-1.682 0-3.157.85-3.984 2.128a.318.318 0 0 1-.3.138 2.962 2.962 0 0 0-.31-.016C2.26 9.235 1 10.444 1 11.935s1.26 2.7 2.813 2.7c.302 0 .594-.046.867-.13a.322.322 0 0 1 .324.076 4.768 4.768 0 0 0 3.402 1.404Zm3.661-2.606c-.161-.116-.41-.088-.52.074a3.788 3.788 0 0 1-3.14 1.632 3.792 3.792 0 0 1-3.091-1.56c-.106-.147-.326-.178-.482-.08-.294.183-.645.29-1.021.29-1.036 0-1.876-.806-1.876-1.8s.84-1.8 1.875-1.8c.241 0 .471.044.683.123.173.065.383-.008.455-.172.569-1.293 1.902-2.2 3.456-2.2 1.575 0 2.924.931 3.48 2.253.075.18.314.254.496.17.244-.111.517-.174.806-.174 1.035 0 1.874.806 1.874 1.8s-.839 1.8-1.874 1.8c-.42 0-.808-.132-1.12-.356ZM4.995 1.762a.516.516 0 1 0 1.007-.224L5.746.388A.516.516 0 0 0 4.74.612l.255 1.15ZM1.273 3.52l.994.633a.516.516 0 0 0 .555-.87l-.995-.633a.516.516 0 0 0-.554.87ZM.878 8.028l1.15-.256a.516.516 0 0 0-.223-1.008l-1.15.256a.516.516 0 1 0 .223 1.008Zm10.238-2.28a.535.535 0 0 0 .112-.012l1.15-.256a.516.516 0 1 0-.224-1.008l-1.15.256a.516.516 0 0 0 .112 1.02ZM8.772 2.713a.516.516 0 0 0 .712-.158l.633-.994a.516.516 0 0 0-.87-.554l-.633.994a.516.516 0 0 0 .158.712ZM3.07 7.017c.07.303.182.596.33.87a3.13 3.13 0 0 0 .909-.486 2.453 2.453 0 0 1-.233-.608 2.504 2.504 0 0 1 4.888-1.088c.003.013.002.026.005.038a5.42 5.42 0 0 1 1.063.25 3.497 3.497 0 0 0-.061-.512A3.535 3.535 0 1 0 3.07 7.017Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

3
apps/client/assets/weather/1010.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="qi-1010" viewBox="0 0 16 16">
<path d="M8.01 2.503a.5.5 0 0 1-.501-.5v-1.5a.5.5 0 0 1 1 0v1.5a.5.5 0 0 1-.5.5ZM3.766 4.255a.498.498 0 0 1-.353-.147l-1.062-1.06a.5.5 0 0 1 .707-.707L4.122 3.4a.5.5 0 0 1-.355.854v.001ZM2.004 8.493h-1.5a.5.5 0 1 1 0-1h1.5a.5.5 0 1 1 0 1Zm.691 5.303a.5.5 0 0 1-.354-.854l1.062-1.06a.5.5 0 0 1 .708.707l-1.063 1.06a.498.498 0 0 1-.353.147Zm5.301 2.201a.5.5 0 0 1-.5-.5v-1.5a.5.5 0 0 1 1 0v1.5a.5.5 0 0 1-.5.5Zm-.869-3.583a4.5 4.5 0 0 0 1.373.059V3.527a4.503 4.503 0 0 0-4.652 2.75 4.5 4.5 0 0 0 3.28 6.137Zm8.876-4.407A.506.506 0 0 0 16 7.952v.04a.5.5 0 0 0-.2-.392 2.952 2.952 0 0 0-.846-.463A2.96 2.96 0 0 0 14 7c-.629 0-1.037.364-1.304.601l-.028.025C12.37 7.891 12.222 8 12 8c-.273 0-.468-.029-.638-.085A1.967 1.967 0 0 1 10.8 7.6a.5.5 0 0 0-.6.8c.276.207.544.363.846.463.301.1.61.137.954.137.629 0 1.038-.364 1.304-.601l.028-.025C13.63 8.109 13.778 8 14 8c.273 0 .468.029.638.085.17.057.344.151.562.315a.5.5 0 0 0 .8-.392v.054a.506.506 0 0 0 .003-.055ZM12 6c.629 0 1.038-.364 1.304-.601l.028-.025C13.63 5.109 13.778 5 14 5c.273 0 .468.029.638.085.17.057.344.151.562.315a.5.5 0 0 0 .6-.8 2.952 2.952 0 0 0-.846-.463A2.96 2.96 0 0 0 14 4c-.629 0-1.037.364-1.304.601l-.028.025C12.37 4.891 12.222 5 12 5c-.273 0-.468-.029-.638-.085A1.967 1.967 0 0 1 10.8 4.6a.5.5 0 0 0-.6.8c.276.207.544.363.846.463.301.1.61.137.954.137Zm0 6c.629 0 1.038-.364 1.304-.601l.028-.025c.298-.264.446-.374.668-.374.273 0 .468.029.638.085.17.057.344.151.562.315a.5.5 0 0 0 .6-.8 2.951 2.951 0 0 0-.846-.463c-.301-.1-.61-.137-.954-.137-.629 0-1.037.364-1.304.601l-.028.025c-.298.264-.446.374-.668.374-.273 0-.468-.029-.638-.085a1.968 1.968 0 0 1-.562-.315.5.5 0 0 0-.6.8c.276.207.544.363.846.463.301.1.61.137.954.137Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

4
apps/client/assets/weather/1011.svg

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="qi-1011" viewBox="0 0 16 16">
<path d="M4.9 1.5c-.9.3-1.2 1.1-1.1 1.6-.7-.8-.7-1.7-.6-3.1-2.1.8-1.6 3.2-1.7 4C1 3.5.9 2.5.9 2.5.3 2.8 0 3.6 0 4.3 0 5.9 1.3 7 2.8 7c1.5 0 2.7-1.2 2.7-2.7 0-1.1-.6-1.4-.6-2.8ZM2 9h6a1 1 0 1 0-.943-1.333.5.5 0 1 1-.943-.334A2 2 0 1 1 8 10H2a.5.5 0 0 1 0-1Z"/>
<path d="M11.079 8.375A2.5 2.5 0 0 1 16 9c0 1.397-1.24 2.5-2.5 2.5H.5a.5.5 0 0 1 0-1h13c.74 0 1.5-.688 1.5-1.5a1.5 1.5 0 0 0-2.953-.375.5.5 0 1 1-.968-.25ZM2.5 12.5A.5.5 0 0 1 3 12h8a2 2 0 1 1-1.886 2.667.5.5 0 1 1 .943-.334A1 1 0 1 0 11 13H3a.5.5 0 0 1-.5-.5Z"/>
</svg>

After

Width:  |  Height:  |  Size: 654 B

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save