环境
| 项目 |
信息 |
| 机型 |
荣耀畅玩 20(KOZ-AL00 等) |
| 系统 |
Magic UI 4.0(Android 10 系,系统更新长期停留在 4.0.x) |
| WebView |
通常为厂商内置 Huawei/Honor WebView(Chromium 版本偏旧,且难以切换为 Google WebView) |
| 应用 |
OpenLess Android APK(com.openless.app) |
现象
在 设置 → 服务(AI 提供商) 页面:
- 切换 LLM / ASR 供应商
- 保存 API Key / 密钥
- 保存 接口地址(endpoint)
- 保存 模型 ID
操作时右上角 SavedToast 先显示 「保存中」(common.saving),随即变为 「操作失败」(common.operationFailed)。几乎立即失败,无明显网络等待。
初步怀疑为 WebView 版本过旧导致前端兼容性问题,但需结合后端日志区分是 IPC 桥、偏好写入还是 Android Keystore 凭据加密链路。
前端行为(错误被吞掉)
所有上述操作最终都走 ProvidersSection.tsx 的 SavedToast 事件:
-
切换供应商(onLlmProviderChange / onAsrProviderChange)
emitSaved('saving') → setActiveLlmProvider / setActiveAsrProvider → updatePrefs → 可能再 setCredential 写入默认 endpoint/model
- 任一步
catch 后:emitSaved('failed', t('common.operationFailed'))(不展示 Rust 返回的具体错误)
- 相关代码:
openless-all/app/src/pages/settings/ProvidersSection.tsx(约 L239–340)
-
凭据字段保存(CredentialField.save)
setCredential(account, value) → Tauri set_credential
- 失败:
showTemporaryStatus('saveError') → 同样映射为 common.operationFailed
- 相关代码:同文件约 L807–821
-
IPC 调用链
openless-all/app/src/lib/ipc/asr-credentials.ts:invokeOrMock('set_credential' | 'set_active_*_provider', ...)
openless-all/app/src/lib/ipc/shared.ts:依赖 window.__TAURI_INTERNALS__ + @tauri-apps/api/core 的 invoke
若应用能正常打开设置页并显示当前偏好,说明 Tauri invoke 基础通路大概率可用;失败更可能发生在具体 command 的业务逻辑(尤其凭据落盘),而非 WebView 完全无法跑 JS。
后端可能失败点(Android 优先排查)
A. Android Keystore 加密凭据(高优先级)
自 #838 起,Android 凭据写入 credentials.enc.json 前经 Android Keystore AES-GCM 封装:
- Kotlin:
openless-all/app/android/kotlin/OpenLessCredentialVault.kt
- Rust:
openless-all/app/src-tauri/src/persistence/android_credentials.rs
- 持久化:
openless-all/app/src-tauri/src/persistence/credentials.rs → save_android_credentials / write_verified
切换供应商与保存密钥/地址/模型都会触发 CredentialsVault::set* → save_credentials → Keystore seal + 原子写文件。低端荣耀机型上 Keystore2 / TEE 实现不完整或繁忙时,JNI 可能返回 TEMPORARILY_UNAVAILABLE 等,最终到前端只剩「操作失败」。
B. 偏好写入 set_settings
切换供应商还会 updatePrefs → set_settings(preferences.json)。若仅凭据字段失败而切换供应商偶发成功,可缩小到 A;若全部失败,需同时查 B。
C. WebView / 前端兼容性(待验证)
Honor 畅玩 20 上常见旧 Huawei WebView(Chromium 90–99 量级)。若日志显示 invoke 未到达 Rust 或 JS 侧 Unhandled promise rejection,再查:
- 动态
import('@tauri-apps/api/core') 是否异常
CustomEvent / createPortal(SelectLite)等是否在特定 WebView 下触发同步异常
当前代码中 Chromium 102+ 才假设可用的能力主要在样式层(如 _atoms.tsx 注释),与保存链路无直接耦合,故 WebView 作为次要假设。
建议复现与采集日志
adb logcat -c
# 手动:打开设置 → 服务,切换供应商或保存密钥
adb logcat -d -v time > android-provider-save.log
Select-String -Path .\android-provider-save.log -Pattern `
"RustStdoutStderr","panicked","set_credential","set_settings",`
"keystore","credentials","Android credential","openless-keystore",`
"ProvidersSection","invoke","FATAL"
同时记录:
- 设置 → 应用 → Android System WebView / Huawei WebView 版本号
- 开发者选项中的 WebView 实现包名
- 失败是「首次配置」还是「已有凭据后修改」
建议修复方向
- 短期:Android 设置页保存失败时,将
invoke 返回的 String 错误(或映射后的可读文案)写入 SavedToast / logClientError,避免一律 operationFailed。
- 中期:针对 Keystore
TEMPORARILY_UNAVAILABLE 做有限重试;失败时给出「凭据加密服务不可用」类明确提示。
- 若确认为 WebView:评估最低 Chromium 版本、在启动时检测 WebView UA 并提示升级系统 WebView 包;避免在旧 WebView 上使用未 polyfill 的 API。
关联代码索引
| 文件 |
说明 |
ProvidersSection.tsx |
保存 UI + 泛化「操作失败」 |
lib/savedEvent.ts |
SavedToast 事件 |
commands/credentials.rs |
set_credential / set_active_*_provider |
persistence/credentials.rs |
Android save_android_credentials |
persistence/android_credentials.rs |
Keystore 信封读写 |
android/kotlin/OpenLessCredentialVault.kt |
Keystore JNI 后端 |
验收
环境
com.openless.app)现象
在 设置 → 服务(AI 提供商) 页面:
操作时右上角 SavedToast 先显示 「保存中」(
common.saving),随即变为 「操作失败」(common.operationFailed)。几乎立即失败,无明显网络等待。初步怀疑为 WebView 版本过旧导致前端兼容性问题,但需结合后端日志区分是 IPC 桥、偏好写入还是 Android Keystore 凭据加密链路。
前端行为(错误被吞掉)
所有上述操作最终都走
ProvidersSection.tsx的 SavedToast 事件:切换供应商(
onLlmProviderChange/onAsrProviderChange)emitSaved('saving')→setActiveLlmProvider/setActiveAsrProvider→updatePrefs→ 可能再setCredential写入默认 endpoint/modelcatch后:emitSaved('failed', t('common.operationFailed'))(不展示 Rust 返回的具体错误)openless-all/app/src/pages/settings/ProvidersSection.tsx(约 L239–340)凭据字段保存(
CredentialField.save)setCredential(account, value)→ Tauriset_credentialshowTemporaryStatus('saveError')→ 同样映射为common.operationFailedIPC 调用链
openless-all/app/src/lib/ipc/asr-credentials.ts:invokeOrMock('set_credential' | 'set_active_*_provider', ...)openless-all/app/src/lib/ipc/shared.ts:依赖window.__TAURI_INTERNALS__+@tauri-apps/api/core的invoke若应用能正常打开设置页并显示当前偏好,说明 Tauri invoke 基础通路大概率可用;失败更可能发生在具体 command 的业务逻辑(尤其凭据落盘),而非 WebView 完全无法跑 JS。
后端可能失败点(Android 优先排查)
A. Android Keystore 加密凭据(高优先级)
自 #838 起,Android 凭据写入
credentials.enc.json前经 Android Keystore AES-GCM 封装:openless-all/app/android/kotlin/OpenLessCredentialVault.ktopenless-all/app/src-tauri/src/persistence/android_credentials.rsopenless-all/app/src-tauri/src/persistence/credentials.rs→save_android_credentials/write_verified切换供应商与保存密钥/地址/模型都会触发
CredentialsVault::set*→save_credentials→ Keystoreseal+ 原子写文件。低端荣耀机型上 Keystore2 / TEE 实现不完整或繁忙时,JNI 可能返回TEMPORARILY_UNAVAILABLE等,最终到前端只剩「操作失败」。B. 偏好写入
set_settings切换供应商还会
updatePrefs→set_settings(preferences.json)。若仅凭据字段失败而切换供应商偶发成功,可缩小到 A;若全部失败,需同时查 B。C. WebView / 前端兼容性(待验证)
Honor 畅玩 20 上常见旧 Huawei WebView(Chromium 90–99 量级)。若日志显示 invoke 未到达 Rust 或 JS 侧
Unhandled promise rejection,再查:import('@tauri-apps/api/core')是否异常CustomEvent/createPortal(SelectLite)等是否在特定 WebView 下触发同步异常当前代码中 Chromium 102+ 才假设可用的能力主要在样式层(如
_atoms.tsx注释),与保存链路无直接耦合,故 WebView 作为次要假设。建议复现与采集日志
同时记录:
建议修复方向
invoke返回的String错误(或映射后的可读文案)写入 SavedToast /logClientError,避免一律operationFailed。TEMPORARILY_UNAVAILABLE做有限重试;失败时给出「凭据加密服务不可用」类明确提示。关联代码索引
ProvidersSection.tsxlib/savedEvent.tscommands/credentials.rsset_credential/set_active_*_providerpersistence/credentials.rssave_android_credentialspersistence/android_credentials.rsandroid/kotlin/OpenLessCredentialVault.kt验收