diff --git a/apps/client/assets/images/devices/default.png b/apps/client/assets/images/devices/default.png index 8c08abf1..6bdfc705 100644 Binary files a/apps/client/assets/images/devices/default.png and b/apps/client/assets/images/devices/default.png differ diff --git a/apps/client/assets/images/devices/default_icon.png b/apps/client/assets/images/devices/default_icon.png index 98bb312f..b742a89b 100644 Binary files a/apps/client/assets/images/devices/default_icon.png and b/apps/client/assets/images/devices/default_icon.png differ diff --git a/apps/client/lib/core/translations/language/ar_sa.dart b/apps/client/lib/core/translations/language/ar_sa.dart index 0b0ac548..b8a4c387 100644 --- a/apps/client/lib/core/translations/language/ar_sa.dart +++ b/apps/client/lib/core/translations/language/ar_sa.dart @@ -1814,6 +1814,7 @@ const Map arSA = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'لديك أحدث إصدار', + 'otaNoServerVersion': 'لا توجد نسخة برنامج ثابت لهذا الجهاز على الخادم', 'otaCurrentVersion': 'الإصدار الحالي', 'otaDone': 'تم', 'otaDownloading': 'جارٍ تنزيل البرنامج الثابت...', diff --git a/apps/client/lib/core/translations/language/bg_bg.dart b/apps/client/lib/core/translations/language/bg_bg.dart index 9b789b04..3a9c38f2 100644 --- a/apps/client/lib/core/translations/language/bg_bg.dart +++ b/apps/client/lib/core/translations/language/bg_bg.dart @@ -1877,6 +1877,7 @@ const Map bgBG = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Вече е актуален', + 'otaNoServerVersion': 'Няма версия на фърмуера за това устройство на сървъра', 'otaCurrentVersion': 'Текуща версия', 'otaDone': 'Готово', 'otaDownloading': 'Изтегляне на фърмуера...', diff --git a/apps/client/lib/core/translations/language/cs_cz.dart b/apps/client/lib/core/translations/language/cs_cz.dart index 166713f9..ec485a7d 100644 --- a/apps/client/lib/core/translations/language/cs_cz.dart +++ b/apps/client/lib/core/translations/language/cs_cz.dart @@ -1845,6 +1845,7 @@ const Map csCZ = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Již aktuální', + 'otaNoServerVersion': 'Na serveru není k dispozici verze firmwaru pro toto zařízení', 'otaCurrentVersion': 'Aktuální verze', 'otaDone': 'Hotovo', 'otaDownloading': 'Stahování firmwaru...', diff --git a/apps/client/lib/core/translations/language/da_dk.dart b/apps/client/lib/core/translations/language/da_dk.dart index 15d8da9b..539b90df 100644 --- a/apps/client/lib/core/translations/language/da_dk.dart +++ b/apps/client/lib/core/translations/language/da_dk.dart @@ -1847,6 +1847,7 @@ const Map daDK = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Allerede opdateret', + 'otaNoServerVersion': 'Ingen firmwareversion til denne enhed på serveren', 'otaCurrentVersion': 'Nuværende version', 'otaDone': 'Færdig', 'otaDownloading': 'Downloader firmware...', diff --git a/apps/client/lib/core/translations/language/de_de.dart b/apps/client/lib/core/translations/language/de_de.dart index 9b4bca76..92993cbe 100644 --- a/apps/client/lib/core/translations/language/de_de.dart +++ b/apps/client/lib/core/translations/language/de_de.dart @@ -2091,6 +2091,7 @@ const Map deDE = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Bereits aktuell', + 'otaNoServerVersion': 'Keine Firmware-Version für dieses Gerät auf dem Server', 'otaCurrentVersion': 'Aktuelle Version', 'otaDone': 'Fertig', 'otaDownloading': 'Firmware wird heruntergeladen...', diff --git a/apps/client/lib/core/translations/language/el_gr.dart b/apps/client/lib/core/translations/language/el_gr.dart index 216bc0f7..55e9b07a 100644 --- a/apps/client/lib/core/translations/language/el_gr.dart +++ b/apps/client/lib/core/translations/language/el_gr.dart @@ -1881,6 +1881,7 @@ const Map elGR = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Ήδη ενημερωμένο', + 'otaNoServerVersion': 'Δεν υπάρχει έκδοση υλικολογισμικού για αυτή τη συσκευή στον διακομιστή', 'otaCurrentVersion': 'Τρέχουσα έκδοση', 'otaDone': 'Τέλος', 'otaDownloading': 'Λήψη υλικολογισμικού...', diff --git a/apps/client/lib/core/translations/language/en_us.dart b/apps/client/lib/core/translations/language/en_us.dart index 47d5c750..4502b50b 100644 --- a/apps/client/lib/core/translations/language/en_us.dart +++ b/apps/client/lib/core/translations/language/en_us.dart @@ -2059,6 +2059,7 @@ const Map enUS = { // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Already up to date', + 'otaNoServerVersion': 'No firmware version for this device on the server', 'otaCurrentVersion': 'Current version', 'otaDone': 'Done', 'otaDownloading': 'Downloading firmware...', diff --git a/apps/client/lib/core/translations/language/es_es.dart b/apps/client/lib/core/translations/language/es_es.dart index 4962439a..16248043 100644 --- a/apps/client/lib/core/translations/language/es_es.dart +++ b/apps/client/lib/core/translations/language/es_es.dart @@ -2085,6 +2085,7 @@ const Map esES = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Ya está actualizado', + 'otaNoServerVersion': 'No hay versión de firmware para este dispositivo en el servidor', 'otaCurrentVersion': 'Versión actual', 'otaDone': 'Listo', 'otaDownloading': 'Descargando firmware...', diff --git a/apps/client/lib/core/translations/language/et_ee.dart b/apps/client/lib/core/translations/language/et_ee.dart index cf6bf38e..574fc48b 100644 --- a/apps/client/lib/core/translations/language/et_ee.dart +++ b/apps/client/lib/core/translations/language/et_ee.dart @@ -1841,6 +1841,7 @@ const Map etEE = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Juba ajakohane', + 'otaNoServerVersion': 'Serveris pole selle seadme püsivara versiooni', 'otaCurrentVersion': 'Praegune versioon', 'otaDone': 'Valmis', 'otaDownloading': 'Püsivara allalaadimine...', diff --git a/apps/client/lib/core/translations/language/fa_ir.dart b/apps/client/lib/core/translations/language/fa_ir.dart index aed4c419..04b4d44a 100644 --- a/apps/client/lib/core/translations/language/fa_ir.dart +++ b/apps/client/lib/core/translations/language/fa_ir.dart @@ -1832,6 +1832,7 @@ const Map faIR = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'هم‌اکنون به‌روز است', + 'otaNoServerVersion': 'هیچ نسخه میان‌افزاری برای این دستگاه روی سرور موجود نیست', 'otaCurrentVersion': 'نسخهٔ فعلی', 'otaDone': 'انجام شد', 'otaDownloading': 'در حال دانلود میان‌افزار...', diff --git a/apps/client/lib/core/translations/language/fi_fi.dart b/apps/client/lib/core/translations/language/fi_fi.dart index 52f99be9..2f29ea8f 100644 --- a/apps/client/lib/core/translations/language/fi_fi.dart +++ b/apps/client/lib/core/translations/language/fi_fi.dart @@ -1851,6 +1851,7 @@ const Map fiFI = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Jo ajan tasalla', + 'otaNoServerVersion': 'Palvelimella ei ole laiteohjelmistoversiota tälle laitteelle', 'otaCurrentVersion': 'Nykyinen versio', 'otaDone': 'Valmis', 'otaDownloading': 'Ladataan laiteohjelmistoa...', diff --git a/apps/client/lib/core/translations/language/fr_fr.dart b/apps/client/lib/core/translations/language/fr_fr.dart index d96af653..f51b2700 100644 --- a/apps/client/lib/core/translations/language/fr_fr.dart +++ b/apps/client/lib/core/translations/language/fr_fr.dart @@ -2104,6 +2104,7 @@ const Map frFR = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Déjà à jour', + 'otaNoServerVersion': 'Aucune version de micrologiciel pour cet appareil sur le serveur', 'otaCurrentVersion': 'Version actuelle', 'otaDone': 'Terminé', 'otaDownloading': 'Téléchargement du firmware...', diff --git a/apps/client/lib/core/translations/language/hi_in.dart b/apps/client/lib/core/translations/language/hi_in.dart index 30ee74b8..63e75ae4 100644 --- a/apps/client/lib/core/translations/language/hi_in.dart +++ b/apps/client/lib/core/translations/language/hi_in.dart @@ -1850,6 +1850,7 @@ const Map hiIN = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'पहले से नवीनतम है', + 'otaNoServerVersion': 'सर्वर पर इस डिवाइस के लिए कोई फ़र्मवेयर संस्करण नहीं है', 'otaCurrentVersion': 'वर्तमान संस्करण', 'otaDone': 'पूर्ण', 'otaDownloading': 'फ़र्मवेयर डाउनलोड हो रहा है...', diff --git a/apps/client/lib/core/translations/language/hr_hr.dart b/apps/client/lib/core/translations/language/hr_hr.dart index 26262c95..9205eebd 100644 --- a/apps/client/lib/core/translations/language/hr_hr.dart +++ b/apps/client/lib/core/translations/language/hr_hr.dart @@ -1866,6 +1866,7 @@ const Map hrHR = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Već je ažurno', + 'otaNoServerVersion': 'Na poslužitelju nema verzije firmvera za ovaj uređaj', 'otaCurrentVersion': 'Trenutačna verzija', 'otaDone': 'Gotovo', 'otaDownloading': 'Preuzimanje firmvera...', diff --git a/apps/client/lib/core/translations/language/hu_hu.dart b/apps/client/lib/core/translations/language/hu_hu.dart index e7233371..76dc0173 100644 --- a/apps/client/lib/core/translations/language/hu_hu.dart +++ b/apps/client/lib/core/translations/language/hu_hu.dart @@ -1869,6 +1869,7 @@ const Map huHU = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Már naprakész', + 'otaNoServerVersion': 'A kiszolgálón nincs firmware-verzió ehhez az eszközhöz', 'otaCurrentVersion': 'Jelenlegi verzió', 'otaDone': 'Kész', 'otaDownloading': 'Firmware letöltése...', diff --git a/apps/client/lib/core/translations/language/id_id.dart b/apps/client/lib/core/translations/language/id_id.dart index 7d0a0c85..f0a69442 100644 --- a/apps/client/lib/core/translations/language/id_id.dart +++ b/apps/client/lib/core/translations/language/id_id.dart @@ -1850,6 +1850,7 @@ const Map idID = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Sudah versi terbaru', + 'otaNoServerVersion': 'Tidak ada versi firmware untuk perangkat ini di server', 'otaCurrentVersion': 'Versi saat ini', 'otaDone': 'Selesai', 'otaDownloading': 'Mengunduh firmware...', diff --git a/apps/client/lib/core/translations/language/is_is.dart b/apps/client/lib/core/translations/language/is_is.dart index 5676fdeb..73aba74a 100644 --- a/apps/client/lib/core/translations/language/is_is.dart +++ b/apps/client/lib/core/translations/language/is_is.dart @@ -1847,6 +1847,7 @@ const Map isIS = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Þegar uppfært', + 'otaNoServerVersion': 'Engin fastbúnaðarútgáfa fyrir þetta tæki á þjóninum', 'otaCurrentVersion': 'Núverandi útgáfa', 'otaDone': 'Lokið', 'otaDownloading': 'Sæki fastbúnað...', diff --git a/apps/client/lib/core/translations/language/it_it.dart b/apps/client/lib/core/translations/language/it_it.dart index b9a6b4a7..b1acd342 100644 --- a/apps/client/lib/core/translations/language/it_it.dart +++ b/apps/client/lib/core/translations/language/it_it.dart @@ -1888,6 +1888,7 @@ const Map itIT = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Già aggiornato', + 'otaNoServerVersion': 'Nessuna versione del firmware per questo dispositivo sul server', 'otaCurrentVersion': 'Versione attuale', 'otaDone': 'Fatto', 'otaDownloading': 'Download del firmware...', diff --git a/apps/client/lib/core/translations/language/ja_jp.dart b/apps/client/lib/core/translations/language/ja_jp.dart index a85b8d2a..a3216602 100644 --- a/apps/client/lib/core/translations/language/ja_jp.dart +++ b/apps/client/lib/core/translations/language/ja_jp.dart @@ -1886,6 +1886,7 @@ const Map jaJP = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': '最新の状態です', + 'otaNoServerVersion': 'サーバーにこのデバイスのファームウェアがありません', 'otaCurrentVersion': '現在のバージョン', 'otaDone': '完了', 'otaDownloading': 'ファームウェアをダウンロード中...', diff --git a/apps/client/lib/core/translations/language/ko_kr.dart b/apps/client/lib/core/translations/language/ko_kr.dart index 5483accd..a3315182 100644 --- a/apps/client/lib/core/translations/language/ko_kr.dart +++ b/apps/client/lib/core/translations/language/ko_kr.dart @@ -1879,6 +1879,7 @@ const Map koKR = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': '이미 최신 버전입니다', + 'otaNoServerVersion': '서버에 이 기기의 펌웨어 버전이 없습니다', 'otaCurrentVersion': '현재 버전', 'otaDone': '완료', 'otaDownloading': '펌웨어 다운로드 중...', diff --git a/apps/client/lib/core/translations/language/lt_lt.dart b/apps/client/lib/core/translations/language/lt_lt.dart index 903c5f43..2751a4ba 100644 --- a/apps/client/lib/core/translations/language/lt_lt.dart +++ b/apps/client/lib/core/translations/language/lt_lt.dart @@ -1865,6 +1865,7 @@ const Map ltLT = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Jau naujausia versija', + 'otaNoServerVersion': 'Serveryje nėra šio įrenginio programinės įrangos versijos', 'otaCurrentVersion': 'Dabartinė versija', 'otaDone': 'Atlikta', 'otaDownloading': 'Atsisiunčiama programinė aparatinė įranga...', diff --git a/apps/client/lib/core/translations/language/lv_lv.dart b/apps/client/lib/core/translations/language/lv_lv.dart index d379f54e..6f86c369 100644 --- a/apps/client/lib/core/translations/language/lv_lv.dart +++ b/apps/client/lib/core/translations/language/lv_lv.dart @@ -1870,6 +1870,7 @@ const Map lvLV = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Jau ir jaunākā versija', + 'otaNoServerVersion': 'Serverī nav šīs ierīces aparātprogrammatūras versijas', 'otaCurrentVersion': 'Pašreizējā versija', 'otaDone': 'Gatavs', 'otaDownloading': 'Notiek programmaparatūras lejupielāde...', diff --git a/apps/client/lib/core/translations/language/ms_my.dart b/apps/client/lib/core/translations/language/ms_my.dart index 49f0272b..febc1137 100644 --- a/apps/client/lib/core/translations/language/ms_my.dart +++ b/apps/client/lib/core/translations/language/ms_my.dart @@ -1854,6 +1854,7 @@ const Map msMY = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Sudah versi terkini', + 'otaNoServerVersion': 'Tiada versi perisian tegar untuk peranti ini pada pelayan', 'otaCurrentVersion': 'Versi semasa', 'otaDone': 'Selesai', 'otaDownloading': 'Memuat turun perisian tegar...', diff --git a/apps/client/lib/core/translations/language/nb_no.dart b/apps/client/lib/core/translations/language/nb_no.dart index 4a4e8821..a386e0fa 100644 --- a/apps/client/lib/core/translations/language/nb_no.dart +++ b/apps/client/lib/core/translations/language/nb_no.dart @@ -1848,6 +1848,7 @@ const Map nbNO = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Allerede oppdatert', + 'otaNoServerVersion': 'Ingen fastvareversjon for denne enheten på serveren', 'otaCurrentVersion': 'Gjeldende versjon', 'otaDone': 'Ferdig', 'otaDownloading': 'Laster ned fastvare...', diff --git a/apps/client/lib/core/translations/language/nl_nl.dart b/apps/client/lib/core/translations/language/nl_nl.dart index efacfdf4..2ca36e3a 100644 --- a/apps/client/lib/core/translations/language/nl_nl.dart +++ b/apps/client/lib/core/translations/language/nl_nl.dart @@ -1747,6 +1747,7 @@ const Map nlNL = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Al up-to-date', + 'otaNoServerVersion': 'Geen firmwareversie voor dit apparaat op de server', 'otaCurrentVersion': 'Huidige versie', 'otaDone': 'Klaar', 'otaDownloading': 'Firmware downloaden...', diff --git a/apps/client/lib/core/translations/language/pl_pl.dart b/apps/client/lib/core/translations/language/pl_pl.dart index db72b09b..fe665506 100644 --- a/apps/client/lib/core/translations/language/pl_pl.dart +++ b/apps/client/lib/core/translations/language/pl_pl.dart @@ -1876,6 +1876,7 @@ const Map plPL = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Masz najnowszą wersję', + 'otaNoServerVersion': 'Brak wersji oprogramowania układowego dla tego urządzenia na serwerze', 'otaCurrentVersion': 'Bieżąca wersja', 'otaDone': 'Gotowe', 'otaDownloading': 'Pobieranie oprogramowania...', diff --git a/apps/client/lib/core/translations/language/pt_pt.dart b/apps/client/lib/core/translations/language/pt_pt.dart index 4d835731..f6105579 100644 --- a/apps/client/lib/core/translations/language/pt_pt.dart +++ b/apps/client/lib/core/translations/language/pt_pt.dart @@ -2091,6 +2091,7 @@ const Map ptPT = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Já está atualizado', + 'otaNoServerVersion': 'Nenhuma versão de firmware para este dispositivo no servidor', 'otaCurrentVersion': 'Versão atual', 'otaDone': 'Concluído', 'otaDownloading': 'A transferir firmware...', diff --git a/apps/client/lib/core/translations/language/ro_ro.dart b/apps/client/lib/core/translations/language/ro_ro.dart index a7b8222f..71ec74d5 100644 --- a/apps/client/lib/core/translations/language/ro_ro.dart +++ b/apps/client/lib/core/translations/language/ro_ro.dart @@ -1878,6 +1878,7 @@ const Map roRO = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Deja la zi', + 'otaNoServerVersion': 'Nu există o versiune de firmware pentru acest dispozitiv pe server', 'otaCurrentVersion': 'Versiunea curentă', 'otaDone': 'Gata', 'otaDownloading': 'Se descarcă firmware-ul...', diff --git a/apps/client/lib/core/translations/language/ru_ru.dart b/apps/client/lib/core/translations/language/ru_ru.dart index b0994fdb..2663a5aa 100644 --- a/apps/client/lib/core/translations/language/ru_ru.dart +++ b/apps/client/lib/core/translations/language/ru_ru.dart @@ -2066,6 +2066,7 @@ const Map ruRU = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Установлена последняя версия', + 'otaNoServerVersion': 'На сервере нет версии прошивки для этого устройства', 'otaCurrentVersion': 'Текущая версия', 'otaDone': 'Готово', 'otaDownloading': 'Загрузка прошивки...', diff --git a/apps/client/lib/core/translations/language/sk_sk.dart b/apps/client/lib/core/translations/language/sk_sk.dart index 6efa4718..c2db8b33 100644 --- a/apps/client/lib/core/translations/language/sk_sk.dart +++ b/apps/client/lib/core/translations/language/sk_sk.dart @@ -1859,6 +1859,7 @@ const Map skSK = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Už je aktuálny', + 'otaNoServerVersion': 'Na serveri nie je verzia firmvéru pre toto zariadenie', 'otaCurrentVersion': 'Aktuálna verzia', 'otaDone': 'Hotovo', 'otaDownloading': 'Sťahovanie firmvéru...', diff --git a/apps/client/lib/core/translations/language/sl_si.dart b/apps/client/lib/core/translations/language/sl_si.dart index f411afd9..a6bda45a 100644 --- a/apps/client/lib/core/translations/language/sl_si.dart +++ b/apps/client/lib/core/translations/language/sl_si.dart @@ -1862,6 +1862,7 @@ const Map slSI = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Že posodobljeno', + 'otaNoServerVersion': 'Na strežniku ni različice vdelane programske opreme za to napravo', 'otaCurrentVersion': 'Trenutna različica', 'otaDone': 'Končano', 'otaDownloading': 'Prenašanje vdelane programske opreme...', diff --git a/apps/client/lib/core/translations/language/sr_rs.dart b/apps/client/lib/core/translations/language/sr_rs.dart index a8afcee5..cd07a221 100644 --- a/apps/client/lib/core/translations/language/sr_rs.dart +++ b/apps/client/lib/core/translations/language/sr_rs.dart @@ -1858,6 +1858,7 @@ const Map srRS = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Već je ažurno', + 'otaNoServerVersion': 'Na serveru nema verzije firmvera za ovaj uređaj', 'otaCurrentVersion': 'Trenutna verzija', 'otaDone': 'Gotovo', 'otaDownloading': 'Preuzimanje firmvera...', diff --git a/apps/client/lib/core/translations/language/sv_se.dart b/apps/client/lib/core/translations/language/sv_se.dart index f888067e..32ca578d 100644 --- a/apps/client/lib/core/translations/language/sv_se.dart +++ b/apps/client/lib/core/translations/language/sv_se.dart @@ -1854,6 +1854,7 @@ const Map svSE = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Redan uppdaterad', + 'otaNoServerVersion': 'Ingen fast programvaruversion för den här enheten på servern', 'otaCurrentVersion': 'Nuvarande version', 'otaDone': 'Klar', 'otaDownloading': 'Laddar ner fast programvara...', diff --git a/apps/client/lib/core/translations/language/th_th.dart b/apps/client/lib/core/translations/language/th_th.dart index 9bdd6000..07ca643a 100644 --- a/apps/client/lib/core/translations/language/th_th.dart +++ b/apps/client/lib/core/translations/language/th_th.dart @@ -1807,6 +1807,7 @@ const Map thTH = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'เป็นเวอร์ชันล่าสุดแล้ว', + 'otaNoServerVersion': 'ไม่มีเวอร์ชันเฟิร์มแวร์สำหรับอุปกรณ์นี้บนเซิร์ฟเวอร์', 'otaCurrentVersion': 'เวอร์ชันปัจจุบัน', 'otaDone': 'เสร็จสิ้น', 'otaDownloading': 'กำลังดาวน์โหลดเฟิร์มแวร์...', diff --git a/apps/client/lib/core/translations/language/tl_ph.dart b/apps/client/lib/core/translations/language/tl_ph.dart index 4f1d504b..2b4e27ed 100644 --- a/apps/client/lib/core/translations/language/tl_ph.dart +++ b/apps/client/lib/core/translations/language/tl_ph.dart @@ -1892,6 +1892,7 @@ const Map tlPH = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Pinakabago na', + 'otaNoServerVersion': 'Walang bersyon ng firmware para sa device na ito sa server', 'otaCurrentVersion': 'Kasalukuyang bersyon', 'otaDone': 'Tapos na', 'otaDownloading': 'Dina-download ang firmware...', diff --git a/apps/client/lib/core/translations/language/tr_tr.dart b/apps/client/lib/core/translations/language/tr_tr.dart index abea3bfd..5fc9e7d7 100644 --- a/apps/client/lib/core/translations/language/tr_tr.dart +++ b/apps/client/lib/core/translations/language/tr_tr.dart @@ -1846,6 +1846,7 @@ const Map trTR = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Zaten güncel', + 'otaNoServerVersion': 'Sunucuda bu cihaz için bellenim sürümü yok', 'otaCurrentVersion': 'Geçerli sürüm', 'otaDone': 'Tamam', 'otaDownloading': 'Ürün yazılımı indiriliyor...', diff --git a/apps/client/lib/core/translations/language/uk_ua.dart b/apps/client/lib/core/translations/language/uk_ua.dart index 43c7db08..e8f38c77 100644 --- a/apps/client/lib/core/translations/language/uk_ua.dart +++ b/apps/client/lib/core/translations/language/uk_ua.dart @@ -1866,6 +1866,7 @@ const Map ukUA = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Установлено найновішу версію', + 'otaNoServerVersion': 'На сервері немає версії прошивки для цього пристрою', 'otaCurrentVersion': 'Поточна версія', 'otaDone': 'Готово', 'otaDownloading': 'Завантаження прошивки...', diff --git a/apps/client/lib/core/translations/language/vi_vn.dart b/apps/client/lib/core/translations/language/vi_vn.dart index 529ea6f0..998828f2 100644 --- a/apps/client/lib/core/translations/language/vi_vn.dart +++ b/apps/client/lib/core/translations/language/vi_vn.dart @@ -1826,6 +1826,7 @@ const Map viVN = { 'genderLockedByIdVerify': 'Gender is set from your verified ID and cannot be changed', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': 'Đã là phiên bản mới nhất', + 'otaNoServerVersion': 'Máy chủ chưa có phiên bản phần mềm cho thiết bị này', 'otaCurrentVersion': 'Phiên bản hiện tại', 'otaDone': 'Xong', 'otaDownloading': 'Đang tải phần mềm...', diff --git a/apps/client/lib/core/translations/language/zh_cn.dart b/apps/client/lib/core/translations/language/zh_cn.dart index 605078bf..997fcda6 100644 --- a/apps/client/lib/core/translations/language/zh_cn.dart +++ b/apps/client/lib/core/translations/language/zh_cn.dart @@ -1873,6 +1873,7 @@ const Map zhCN = { // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': '已是最新版本', + 'otaNoServerVersion': '服务端暂无该设备的固件版本', 'otaCurrentVersion': '当前版本', 'otaDone': '完成', 'otaDownloading': '正在下载固件...', diff --git a/apps/client/lib/core/translations/language/zh_hk.dart b/apps/client/lib/core/translations/language/zh_hk.dart index 24337960..1b76f84c 100644 --- a/apps/client/lib/core/translations/language/zh_hk.dart +++ b/apps/client/lib/core/translations/language/zh_hk.dart @@ -1856,6 +1856,7 @@ const Map zhHK = { 'genderLockedByIdVerify': '性别已根据实名认证信息确定,不可修改', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': '已是最新版本', + 'otaNoServerVersion': '伺服器暫無此裝置的韌體版本', 'otaCurrentVersion': '目前版本', 'otaDone': '完成', 'otaDownloading': '正在下載韌體...', diff --git a/apps/client/lib/core/translations/language/zh_tw.dart b/apps/client/lib/core/translations/language/zh_tw.dart index 29b4747b..6ecb171b 100644 --- a/apps/client/lib/core/translations/language/zh_tw.dart +++ b/apps/client/lib/core/translations/language/zh_tw.dart @@ -1856,6 +1856,7 @@ const Map zhTW = { 'genderLockedByIdVerify': '性别已根据实名认证信息确定,不可修改', // ---- 恒玄(BES)固件 OTA 升级页 ---- 'otaAlreadyLatest': '已是最新版本', + 'otaNoServerVersion': '伺服器暫無此裝置的韌體版本', 'otaCurrentVersion': '目前版本', 'otaDone': '完成', 'otaDownloading': '正在下載韌體...', diff --git a/apps/client/lib/data/models/device_images.dart b/apps/client/lib/data/models/device_images.dart index 45499c62..4f29b42e 100644 --- a/apps/client/lib/data/models/device_images.dart +++ b/apps/client/lib/data/models/device_images.dart @@ -16,6 +16,13 @@ class DeviceImages { DeviceImages._(); static const String _tag = 'DeviceImages'; + /// ⚠️ 目录里的图**必须是去背的透明 PNG**,`default.png` / `default_icon.png` + /// 也不例外。设备页的展示区是横宽的(约 1.14:1),而出图习惯是竖版海报 + /// (892×1152,0.77:1)——带不透明底色的竖图用 `BoxFit.contain` 塞进去, + /// 会变成中间一条窄的色块卡片、左右大片留白,且底色与页面背景(尤其深色模式) + /// 硬碰硬。2026-09-14 之前的默认图正是这样,表现为「图片和显示框对不上」。 + /// 去背之后主体自己铺满框、周围就是页面背景,不需要任何代码适配。 + static const String dir = 'assets/images/devices/'; static const String defaultImage = '${dir}default.png'; static const String defaultIcon = '${dir}default_icon.png'; diff --git a/apps/client/lib/data/services/agent_module_defaults.dart b/apps/client/lib/data/services/agent_module_defaults.dart index 563afba4..7edc7216 100644 --- a/apps/client/lib/data/services/agent_module_defaults.dart +++ b/apps/client/lib/data/services/agent_module_defaults.dart @@ -37,12 +37,31 @@ class AgentDefaults { static void bind(DeviceHub hub) { _worker?.dispose(); - _worker = ever>(hub.sessions, (list) { + _worker = ever>(hub.sessions, (_) => _apply(hub)); + // ⚠️ 还要对**当前状态**跑一次:GetX 的 ever 只在之后的赋值上触发, + // 拿不到绑定那一刻已经在的值。设备是被动回连上来的(原生扫到广播就接), + // 完全可能赶在 bind 之前就绪 —— 那样 AI 页这一整轮都不会按设备铺编排, + // 而且不报任何错,表现就是「设备明明连着,功能却是全量的」。 + _apply(hub); + } + + /// 按「当前连着的设备,没有就按最近连过的那台」铺默认编排。 + /// + /// ⚠️ 断开后必须回退到 [DeviceHub.lastKnownVendor],不能什么都不做: + /// 设备页的产品图和「设备管理」在断连后仍然显示最近那台设备,AI 页却停在 + /// 上一台设备铺下的编排里 —— 于是出现「图是 Echo-one 耳机、功能却是车载香薰 + /// 那三个」。三处必须同一个口径,见 DeviceHub._lastKnown。 + static void _apply(DeviceHub hub) { + final list = hub.sessions; + if (list.isNotEmpty) { for (final s in list) { final cat = _vendorCategory[s.vendor]; if (cat != null) applyAgentDefaultsFor(cat); } - }); + return; + } + final cat = _vendorCategory[hub.lastKnownVendor]; + if (cat != null) applyAgentDefaultsFor(cat); } static void applyAgentDefaultsFor(String categoryId) { diff --git a/apps/client/lib/data/services/agent_module_service.dart b/apps/client/lib/data/services/agent_module_service.dart index 22279da8..397c1b8b 100644 --- a/apps/client/lib/data/services/agent_module_service.dart +++ b/apps/client/lib/data/services/agent_module_service.dart @@ -40,14 +40,93 @@ class AgentModuleService extends GetxService { .where((id) => AgentModules.byId(id) != null) .toList(); enabledIds.assignAll(list); + + // ⚠️ 当前这份编排如果是**设备默认铺下来的**(用户没动过),三个一次性迁移 + // 一律跳过。它们是给「用户自己的编排」补新功能用的,而设备编排本来就该 + // 只有那几项 —— 尤其 _migrateDefaultLayout 会把 defaultEnabled 里缺的全部 + // 追加回来,等于把「连上香薰只显示 Smartcar/现场录音/语音纪要」直接抹掉。 + // + // 2026-09-14 线上就是这么坏的:香薰编排铺成 3 项 → 重启 App → 迁移追加成 + // 11 项 → 而基线 _lastAppliedKey 还停在 3 项 → 之后 applyDeviceDefaults + // 一比较发现「对不上」,判成用户动过编排,**从此再也不铺**。表现就是 + // 连着香薰却显示全量功能,且怎么重连都恢复不了。 + if (_matchesLastApplied()) return; + _migrateEmaiAssistant(); _migrateDefaultLayout(); _migrateDefaultOrder(); + _rebaseDeviceDefaultsBaseline(); } else { enabledIds.assignAll(AgentModules.defaultEnabled); } } + /// 当前编排是否与「上一次自动铺下去的那份」逐项相同 —— 即用户没动过。 + bool _matchesLastApplied() { + if (!_storage.hasData(_lastAppliedKey)) return false; + final last = (_storage.read(_lastAppliedKey) ?? const []) + .map((e) => e.toString()) + .toList(); + return last.isNotEmpty && _sameOrder(enabledIds, last); + } + + /// 一次性纠正:把已经被 [_migrateDefaultLayout] 追加过的设备编排重新认作基线。 + /// + /// 修上面那条时序 bug 之前装过的机器,storage 里已经是「enabledIds=11 项、 + /// 基线=3 项」的坏状态,而迁移带 migratedKey 不会再跑一次,光修 _load 救不回来。 + /// + /// 判据收得很紧,必须**同时**满足:基线存在、当前编排以基线为前缀、多出来的项 + /// 全部来自 defaultEnabled —— 这正是 _migrateDefaultLayout「保持原序、把缺的 + /// append 到尾部」留下的形态。 + /// + /// 纠正的是**基线**不是编排:把基线抬到当前值,界面此刻不变,等下次连上设备时 + /// 由 applyDeviceDefaults 正常铺成该设备的编排 —— 那本来就是用户预期的时机。 + /// (代价:用户如果**恰好**手动加的也全是 defaultEnabled 里的项、且加在尾部, + /// 会被误判成迁移污染,下次连设备时编排被重铺一次。两种误判范围相同, + /// 选这个是因为它不会在启动瞬间凭空改变用户看到的界面。) + void _rebaseDeviceDefaultsBaseline() { + const rebaseKey = 'ai_tab_device_defaults_rebase_v1'; + if (_storage.read(rebaseKey) == true) return; + _storage.write(rebaseKey, true); + if (!_storage.hasData(_lastAppliedKey)) return; + final last = (_storage.read(_lastAppliedKey) ?? const []) + .map((e) => e.toString()) + .toList(); + if (!isLayoutMigrationArtifact( + enabled: enabledIds, + lastApplied: last, + defaults: AgentModules.defaultEnabled, + )) { + return; + } + _storage.write(_lastAppliedKey, enabledIds.toList()); + } + + /// 当前编排看起来是不是「设备默认编排 + 被一次性迁移追加的全量项」。 + /// + /// 形态要求([_migrateDefaultLayout] 的行为特征): + /// 1. [lastApplied] 非空,且原样是 [enabled] 的**前缀**(顺序都没动); + /// 2. [enabled] 确实更长; + /// 3. 多出来的项**全部**出自 [defaults] —— 迁移只会补这些。 + /// + /// 抽成纯函数是为了能脱离 GetStorage 测试,见 test/agent_module_defaults_test.dart。 + static bool isLayoutMigrationArtifact({ + required List enabled, + required List lastApplied, + required List defaults, + }) { + if (lastApplied.isEmpty || enabled.length <= lastApplied.length) { + return false; + } + for (var i = 0; i < lastApplied.length; i++) { + if (enabled[i] != lastApplied[i]) return false; + } + for (final id in enabled.sublist(lastApplied.length)) { + if (!defaults.contains(id)) return false; + } + return true; + } + /// 一次性迁移:默认排序调整(语音记录助手→现场录音→通话录音→…)。 /// /// 只在用户**没自己排过**时才动——即当前启用集合与默认集合完全一致, diff --git a/apps/client/lib/data/utils/emai_audio_session.dart b/apps/client/lib/data/utils/emai_audio_session.dart index a4684c9e..de6b79c4 100644 --- a/apps/client/lib/data/utils/emai_audio_session.dart +++ b/apps/client/lib/data/utils/emai_audio_session.dart @@ -5,6 +5,19 @@ import 'package:flutter/foundation.dart' show kIsWeb; import '../../core/utils/logger.dart'; +/// 这条语音链路要用手机的哪些能力。决定 AVAudioSession 的类别, +/// 而类别直接决定**声音从哪儿出来**。 +enum EmaiAudioProfile { + /// 手机麦克风 + 手机放音(EMAI 页「按住说话」)。必须 playAndRecord。 + phoneMic, + + /// 只放音,麦克风在**设备**上(Smartcar 车载香薰按键唤醒:采音走 BLE opus, + /// 手机一个字节都不录)。 + /// + /// 这一档必须用 playback 而不是 playAndRecord —— 见 [ensure] 里的说明。 + playbackOnly, +} + /// EMAI 语音链路(麦克风上行 + TTS 下行)共用的一份 AVAudioSession 配置。 /// /// 为什么要抽出来:麦克风和播放器是两个模块,各自配一次会话就会互相覆盖。 @@ -21,47 +34,87 @@ class EmaiAudioSession { static const String _tag = 'EmaiAudioSession'; - static bool _configured = false; + /// 当前生效的档位;换档要重新 configure,不能像原来那样只配一次。 + static EmaiAudioProfile? _configured; static bool get _apply => !kIsWeb && (Platform.isIOS || Platform.isAndroid); - /// 配置(只做一次)并激活会话。失败只记日志,不抛—— + /// 按档位配置并激活会话。失败只记日志,不抛—— /// 会话配不上时让调用方继续尝试,总比整条链路直接不启动好。 - static Future ensure() async { + /// + /// ⚠️ **playbackOnly 这一档是有来历的**(2026-09-14 修):车载香薰唤醒 AI 后 + /// 回复声音从**手机扬声器**出来,而不是从香薰喇叭出来。香薰的下行本来就设计成 + /// 走经典蓝牙 A2DP(`_SmartcarPhoneSpeaker` 的注释写的就是「A2DP 连着时从设备 + /// 喇叭出来」),坏在这份会话配置上,两处叠加: + /// 1. `defaultToSpeaker` 把输出钉在内置扬声器上; + /// 2. 选项里只有 `allowBluetooth`(那是 HFP 免提),**没有 allowBluetoothA2dp** + /// —— playAndRecord 类别下 A2DP 输出默认是关的。 + /// 而这条链路手机压根不录音,用 playAndRecord 本身就是错配。改成 playback 之后 + /// 输出走系统当前路由,A2DP 连着就从设备喇叭出。 + /// + /// phoneMic 那一档保留 defaultToSpeaker(去掉会退回听筒,那是更糟的退化), + /// 只补上 allowBluetoothA2dp。 + static Future ensure( + {EmaiAudioProfile profile = EmaiAudioProfile.phoneMic}) async { if (!_apply) return; try { final session = await AudioSession.instance; - if (!_configured) { - _configured = true; - await session.configure(AudioSessionConfiguration( - // iOS 必须 playAndRecord:上行要录音、下行要外放,缺一不可。 - avAudioSessionCategory: AVAudioSessionCategory.playAndRecord, - avAudioSessionCategoryOptions: - AVAudioSessionCategoryOptions.defaultToSpeaker | - AVAudioSessionCategoryOptions.allowBluetooth, - // ⚠️ iOS 固定 voiceChat:record 开了 echoCancel,它会打开 - // voice-processing,系统随之把 mode 改成 voiceChat 并一直持有 - // (真机 dump 确认)。播放这边如果写回 default,就是两边互相改会话。 - // defaultToSpeaker 在 voiceChat 下仍然走扬声器,实测不会被拽到听筒。 - avAudioSessionMode: Platform.isIOS - ? AVAudioSessionMode.voiceChat - : AVAudioSessionMode.defaultMode, - // ⚠️ Android 的 usage 千万别用 voiceCommunication:华为 - // (EMUI/HarmonyOS) 对通话流卡得很严,不在 MODE_IN_COMMUNICATION 下建的 - // VOICE_COMMUNICATION 音轨会被直接静音,logcat 只留一行 - // `AudioTrack: set playback slient`,音轨建得出来、数据也在写,就是没声音。 - // ALN-AL80 上实测复现过,用 media 就没这问题。 - androidAudioAttributes: const AndroidAudioAttributes( - contentType: AndroidAudioContentType.speech, - usage: AndroidAudioUsage.media, - ), - // 助手说话时把背景音乐暂停,而不是压低——压低了在嘈杂环境还是听不清 - androidAudioFocusGainType: AndroidAudioFocusGainType.gainTransient, - )); + if (_configured != profile) { + _configured = profile; + await session.configure(profile == EmaiAudioProfile.playbackOnly + ? _playbackOnlyConfig() + : _phoneMicConfig()); } await session.setActive(true); } catch (e) { Logger.w(_tag, '音频会话配置失败(继续尝试): $e'); } } + + /// 只放音:输出跟随系统路由(蓝牙 A2DP / AirPlay / 扬声器)。 + static AudioSessionConfiguration _playbackOnlyConfig() => + AudioSessionConfiguration( + avAudioSessionCategory: AVAudioSessionCategory.playback, + // ⚠️ 不要写 defaultToSpeaker:它只在 playAndRecord 下合法,而且正是 + // 「声音不从蓝牙设备出来」的那个元凶。 + avAudioSessionCategoryOptions: + AVAudioSessionCategoryOptions.allowBluetoothA2dp | + AVAudioSessionCategoryOptions.allowAirPlay, + avAudioSessionMode: AVAudioSessionMode.defaultMode, + androidAudioAttributes: const AndroidAudioAttributes( + contentType: AndroidAudioContentType.speech, + usage: AndroidAudioUsage.media, + ), + androidAudioFocusGainType: AndroidAudioFocusGainType.gainTransient, + ); + + static AudioSessionConfiguration _phoneMicConfig() => + AudioSessionConfiguration( + // iOS 必须 playAndRecord:上行要录音、下行要外放,缺一不可。 + avAudioSessionCategory: AVAudioSessionCategory.playAndRecord, + // allowBluetooth 是 HFP(免提,双向窄带);allowBluetoothA2dp 才是高音质 + // 单向输出。两个都给,让系统按当前是否真的在录音去挑。 + avAudioSessionCategoryOptions: + AVAudioSessionCategoryOptions.defaultToSpeaker | + AVAudioSessionCategoryOptions.allowBluetooth | + AVAudioSessionCategoryOptions.allowBluetoothA2dp, + // ⚠️ iOS 固定 voiceChat:record 开了 echoCancel,它会打开 + // voice-processing,系统随之把 mode 改成 voiceChat 并一直持有 + // (真机 dump 确认)。播放这边如果写回 default,就是两边互相改会话。 + // defaultToSpeaker 在 voiceChat 下仍然走扬声器,实测不会被拽到听筒。 + avAudioSessionMode: Platform.isIOS + ? AVAudioSessionMode.voiceChat + : AVAudioSessionMode.defaultMode, + // ⚠️ Android 的 usage 千万别用 voiceCommunication:华为 + // (EMUI/HarmonyOS) 对通话流卡得很严,不在 MODE_IN_COMMUNICATION 下建的 + // VOICE_COMMUNICATION 音轨会被直接静音,logcat 只留一行 + // `AudioTrack: set playback slient`,音轨建得出来、数据也在写,就是没声音。 + // ALN-AL80 上实测复现过,用 media 就没这问题。 + androidAudioAttributes: const AndroidAudioAttributes( + contentType: AndroidAudioContentType.speech, + usage: AndroidAudioUsage.media, + ), + // 助手说话时把背景音乐暂停,而不是压低——压低了在嘈杂环境还是听不清 + androidAudioFocusGainType: AndroidAudioFocusGainType.gainTransient, + ); } diff --git a/apps/client/lib/data/utils/pcm_stream_player.dart b/apps/client/lib/data/utils/pcm_stream_player.dart index 3ec4aeea..920eef46 100644 --- a/apps/client/lib/data/utils/pcm_stream_player.dart +++ b/apps/client/lib/data/utils/pcm_stream_player.dart @@ -28,6 +28,7 @@ class PcmStreamPlayer { PcmStreamPlayer({ this.sampleRate = 24000, this.channels = 1, + this.playbackOnly = false, }); static const String _tag = 'PcmStreamPlayer'; @@ -38,6 +39,13 @@ class PcmStreamPlayer { final int sampleRate; final int channels; + /// 这条链路手机**不录音**(麦克风在设备上)。 + /// + /// true 时音频会话走 [EmaiAudioProfile.playbackOnly]:输出跟随系统路由, + /// 蓝牙 A2DP 连着就从设备喇叭出来。默认 false 是手机麦克风那条链路, + /// 必须 playAndRecord,不能改。 + final bool playbackOnly; + /// iOS 专用的原生 PCM 播放通道,见 [_iosNative] 的说明 static const MethodChannel _iosChannel = MethodChannel('azure_speech/pcm_player'); @@ -85,7 +93,11 @@ class PcmStreamPlayer { /// /// 显式 setActive(true) 是把会话按本方的配置重新激活一次, /// 相当于「录音刚放开,播放把它拿回来」。 - Future _ensureSession() => EmaiAudioSession.ensure(); + Future _ensureSession() => EmaiAudioSession.ensure( + profile: playbackOnly + ? EmaiAudioProfile.playbackOnly + : EmaiAudioProfile.phoneMic, + ); /// 正在进行中的 [begin]。[end] 要等它落地才能判断这一轮有没有音频—— /// 回复很短时(音频首包和 RespondingEnded 几乎同时到)`_active` 还是 false, diff --git a/apps/client/lib/devices/device_hub.dart b/apps/client/lib/devices/device_hub.dart index da8d1d90..a682dec5 100644 --- a/apps/client/lib/devices/device_hub.dart +++ b/apps/client/lib/devices/device_hub.dart @@ -108,17 +108,36 @@ class DeviceHub extends GetxService { return null; } - /// 最近连过(或正连着)的设备名,用于断连后仍显示「我的那台设备」。 - /// 从没连过任何设备返回空串。 - String get lastKnownName { - if (primaryName.value.isNotEmpty) return primaryName.value; - for (final p in _plugins.values) { - final d = p.lastKnownDevice; - if (d != null && d.name.isNotEmpty) return d.name; + /// 最近连过(或正连着)的那台设备:名字 + 出自哪家插件。 + /// + /// ⚠️ 名字和厂商**必须同源**,不能各算各的:设备页的产品图按名字取、 + /// AI 页的默认编排按厂商取,两者指向不同设备就会出现「图是耳机、 + /// 功能却是香薰的」这种自相矛盾的界面。 + /// + /// ⚠️ 已知口径局限:没有活跃会话时按**插件注册顺序**取第一个连过的, + /// 不是按「最近连接时间」。同时连过耳机和香薰、然后都断开时, + /// 显示的是注册在前的那家(当前是恒玄),而不是真正最近用的那台。 + ({String name, String vendor}) get _lastKnown { + final p = primary; + if (p != null && p.info.name.isNotEmpty) { + return (name: p.info.name, vendor: p.vendor); + } + for (final plugin in _plugins.values) { + final d = plugin.lastKnownDevice; + if (d != null && d.name.isNotEmpty) { + return (name: d.name, vendor: plugin.vendorKey); + } } - return ''; + return (name: '', vendor: ''); } + /// 最近连过(或正连着)的设备名,用于断连后仍显示「我的那台设备」。 + /// 从没连过任何设备返回空串。 + String get lastKnownName => _lastKnown.name; + + /// 同一台设备出自哪家厂商插件。AI 页按它铺该品类的默认功能编排。 + String get lastKnownVendor => _lastKnown.vendor; + bool get hasKnownDevice => lastKnownName.isNotEmpty; // ---------------- 动作 ---------------- diff --git a/apps/client/lib/devices/smartcar/smartcar_device_plugin.dart b/apps/client/lib/devices/smartcar/smartcar_device_plugin.dart index 0a9e9f85..c62fd66e 100644 --- a/apps/client/lib/devices/smartcar/smartcar_device_plugin.dart +++ b/apps/client/lib/devices/smartcar/smartcar_device_plugin.dart @@ -936,8 +936,12 @@ class _SmartcarMic implements DeviceAudioSource { class _SmartcarPhoneSpeaker implements DeviceAudioSink { _SmartcarPhoneSpeaker(); + /// ⚠️ `playbackOnly: true` 是这条链路能从**香薰喇叭**出声的关键: + /// 采音在设备上(BLE opus),手机一个字节都不录,会话就该用 playback 类别。 + /// 用 playAndRecord + defaultToSpeaker 会把声音钉死在手机扬声器上, + /// 表现就是「连着香薰、AI 却从手机外放说话」。见 EmaiAudioSession.ensure。 final PcmStreamPlayer _player = - PcmStreamPlayer(sampleRate: 16000, channels: 1); + PcmStreamPlayer(sampleRate: 16000, channels: 1, playbackOnly: true); @override AudioRoute get route => AudioRoute.ai; diff --git a/apps/client/lib/modules/ota_upgrade/controllers/device_ota_controller.dart b/apps/client/lib/modules/ota_upgrade/controllers/device_ota_controller.dart index 0ef9913e..f31065ef 100644 --- a/apps/client/lib/modules/ota_upgrade/controllers/device_ota_controller.dart +++ b/apps/client/lib/modules/ota_upgrade/controllers/device_ota_controller.dart @@ -51,6 +51,13 @@ class DeviceOtaController extends GetxController { // ---- 响应式状态 ---- final RxString currentVersion = ''.obs; final RxString latestVersion = ''.obs; + + /// 压根没找到这台设备对应的产品配置(设备名对不上、也没有绑定关系)。 + /// + /// 与「找到了产品、但后台还没给它传过固件」是两回事,UI 上必须分开说: + /// 前者是配置/绑定的问题,后者是运营还没上传。两种都显示成绿色的 + /// 「已是最新」会让人以为固件是新的 —— 2026-09-14 车载香薰就这么误导过一次。 + final RxBool productMissing = false.obs; final RxString updateDescription = ''.obs; final RxBool isUpdateAvailable = false.obs; final RxBool isConnected = true.obs; @@ -153,10 +160,12 @@ class DeviceOtaController extends GetxController { final product = _findProduct(deviceName.value); if (product == null) { Logger.w(_tag, '未找到设备对应的产品配置: ${deviceName.value}'); + productMissing.value = true; latestVersion.value = ''; updateDescription.value = ''; _downloadUrl = ''; } else { + productMissing.value = false; latestVersion.value = product.version?.split('+').first.trim() ?? ''; updateDescription.value = product.updatedescription ?? ''; _downloadUrl = product.updatepackageaddress ?? ''; @@ -166,6 +175,14 @@ class DeviceOtaController extends GetxController { '服务端最新=${product.version} → 解析后=${latestVersion.value} ' '设备当前=${currentVersion.value} ' '产品表共${AppConfig.config.products.length}个'); + if (latestVersion.value.isEmpty) { + // 拉到产品了、只是后台没传固件。和「拉取失败」是两回事,写清楚, + // 免得又去查网络和接口。 + Logger.w( + _tag, + '产品 ${product.devicename}(${product.id}) 后台没有配置固件版本,' + '去后台「产品管理 → 版本」上传并设为当前使用版本'); + } } _refreshUpdateAvailable(); } diff --git a/apps/client/lib/modules/ota_upgrade/views/device_ota_view.dart b/apps/client/lib/modules/ota_upgrade/views/device_ota_view.dart index 63977810..07372d20 100644 --- a/apps/client/lib/modules/ota_upgrade/views/device_ota_view.dart +++ b/apps/client/lib/modules/ota_upgrade/views/device_ota_view.dart @@ -165,6 +165,23 @@ class DeviceOtaView extends GetView { 12.verticalSpace, Obx(() { final available = controller.isUpdateAvailable.value; + // ⚠️ 三态,别塌成「有新版本 / 已是最新」两态:服务端没给出版本时 + // 报「已是最新」是**假话**——固件可能压根没上传过,用户会以为 + // 自己手上是最新固件而不再去查。2026-09-14 车载香薰踩过。 + final String text; + final IconData icon; + if (controller.productMissing.value) { + // 设备名对不上产品表、又没有绑定关系,这是配置/绑定问题 + text = 'otaProductInfoUnavailable'.tr; + icon = Icons.help_outline; + } else if (controller.latestVersion.value.trim().isEmpty) { + // 产品找到了,只是后台还没传固件 + text = 'otaNoServerVersion'.tr; + icon = Icons.info_outline; + } else { + text = available ? 'otaUpdateAvailable'.tr : 'otaAlreadyLatest'.tr; + icon = available ? Icons.system_update : Icons.check_circle; + } return Container( width: double.infinity, padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 12.w), @@ -176,17 +193,20 @@ class DeviceOtaView extends GetView { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - available ? Icons.system_update : Icons.check_circle, + icon, size: 16.w, color: available ? _accent : Colors.grey[500], ), 6.horizontalSpace, - Text( - available ? 'otaUpdateAvailable'.tr : 'otaAlreadyLatest'.tr, - style: TextStyle( - fontSize: 13.sp, - color: available ? _accent : Colors.grey[600], - fontWeight: FontWeight.w500, + Flexible( + child: Text( + text, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13.sp, + color: available ? _accent : Colors.grey[600], + fontWeight: FontWeight.w500, + ), ), ), ], diff --git a/apps/client/test/agent_module_defaults_test.dart b/apps/client/test/agent_module_defaults_test.dart new file mode 100644 index 00000000..e623403d --- /dev/null +++ b/apps/client/test/agent_module_defaults_test.dart @@ -0,0 +1,81 @@ +import 'package:eaimar/data/models/agent_module.dart'; +import 'package:eaimar/data/services/agent_module_service.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + // 车载香薰连上时铺的那三个 + const carAroma = ['smartcarChat', 'fieldRecording', 'meeting']; + const defaults = ['call', 'simultaneous', 'meeting', 'fieldRecording']; + + group('isLayoutMigrationArtifact', () { + // 这正是 2026-09-14 线上那台机的状态:香薰编排铺过 3 项, + // 重启后被 _migrateDefaultLayout 把默认全量追加到尾部, + // 于是基线对不上、从此再也不按设备铺编排。 + test('设备编排被追加了默认项 → 认得出来', () { + expect( + AgentModuleService.isLayoutMigrationArtifact( + enabled: [...carAroma, 'call', 'simultaneous'], + lastApplied: carAroma, + defaults: defaults, + ), + isTrue, + ); + }); + + test('用户把设备编排里的项删掉/换过顺序 → 不是迁移造成的,不能碰', () { + expect( + AgentModuleService.isLayoutMigrationArtifact( + enabled: ['fieldRecording', 'smartcarChat', 'meeting', 'call'], + lastApplied: carAroma, + defaults: defaults, + ), + isFalse, + ); + }); + + test('多出来的项不在默认全量里(用户从广场加的)→ 不认', () { + expect( + AgentModuleService.isLayoutMigrationArtifact( + enabled: [...carAroma, 'pictureTranslation'], + lastApplied: carAroma, + defaults: defaults, + ), + isFalse, + ); + }); + + test('编排与基线完全一致 → 没被追加过', () { + expect( + AgentModuleService.isLayoutMigrationArtifact( + enabled: carAroma, + lastApplied: carAroma, + defaults: defaults, + ), + isFalse, + ); + }); + + test('从没自动铺过(基线为空)→ 不认', () { + expect( + AgentModuleService.isLayoutMigrationArtifact( + enabled: [...carAroma, 'call'], + lastApplied: const [], + defaults: defaults, + ), + isFalse, + ); + }); + }); + + // 设备编排的内容本身也守一下:香薰就该是这三个,别被顺手改掉。 + test('车载香薰的默认编排是 Smartcar / 现场录音 / 语音纪要', () { + expect( + AgentModules.defaultsForCategory('carAroma'), + [ + AgentModules.smartcarChat, + AgentModules.fieldRecording, + AgentModules.meeting, + ], + ); + }); +}