diff --git a/Sources/CodexBar/Providers/Ollama/OllamaUIErrorMapper.swift b/Sources/CodexBar/Providers/Ollama/OllamaUIErrorMapper.swift new file mode 100644 index 0000000000..702cc9bd59 --- /dev/null +++ b/Sources/CodexBar/Providers/Ollama/OllamaUIErrorMapper.swift @@ -0,0 +1,36 @@ +import CodexBarCore +import Foundation + +struct OllamaUIErrorMapper { + static func userFacingMessage( + _ raw: String?, + localize: (String) -> String = L) -> String? + { + guard let raw else { return nil } + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + + if trimmed == OllamaUsageError.safariCookieAccessDenied.localizedDescription { + return localize("ollama_safari_cookie_access_hint") + } + if let browserName = self.browserName( + in: trimmed, + suffix: " cookie decryption was declined in Keychain; retry with a manual refresh.") + { + return String(format: localize("ollama_browser_cookie_decryption_denied"), browserName) + } + if let browserName = self.browserName( + in: trimmed, + suffix: " cookie decryption is disabled in CodexBar; enable Keychain access and refresh.") + { + return String(format: localize("ollama_browser_cookie_decryption_disabled"), browserName) + } + return trimmed + } + + private static func browserName(in message: String, suffix: String) -> String? { + guard message.hasSuffix(suffix) else { return nil } + let name = String(message.dropLast(suffix.count)).trimmingCharacters(in: .whitespacesAndNewlines) + return name.isEmpty ? nil : name + } +} diff --git a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings index 9fdbf60f0f..c379e785ce 100644 --- a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Arabic localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "تحتاج ملفات تعريف ارتباط Safari إلى وصول كامل إلى القرص لتطبيق CodexBar (إعدادات النظام > الخصوصية والأمان)."; +"ollama_browser_cookie_decryption_denied" = "تم رفض فك تشفير ملفات تعريف ارتباط %@ في سلسلة المفاتيح؛ أعد المحاولة بتحديث يدوي."; +"ollama_browser_cookie_decryption_disabled" = "فك تشفير ملفات تعريف ارتباط %@ معطل في CodexBar؛ فعّل الوصول إلى سلسلة المفاتيح ثم حدّث."; + " providers" = " providers"; "(System)" = "(النظام)"; "30d" = "30 يومًا"; diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings index 015ec26bd3..701a793912 100644 --- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Catalan localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "Les galetes de Safari necessiten accés complet al disc per a CodexBar (Configuració del Sistema > Privadesa i seguretat)."; +"ollama_browser_cookie_decryption_denied" = "S'ha denegat el desxifratge de les galetes de %@ al Clauer; torneu-ho a provar amb una actualització manual."; +"ollama_browser_cookie_decryption_disabled" = "El desxifratge de les galetes de %@ està desactivat a CodexBar; activeu l'accés al Clauer i actualitzeu."; + " providers" = " proveïdors"; "(System)" = "(Sistema)"; "30d" = "30 d"; diff --git a/Sources/CodexBar/Resources/de.lproj/Localizable.strings b/Sources/CodexBar/Resources/de.lproj/Localizable.strings index da34973999..57338f44af 100644 --- a/Sources/CodexBar/Resources/de.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/de.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* English localization for CodexBar (base/fallback) */ +"ollama_safari_cookie_access_hint" = "Safari-Cookies benötigen vollen Festplattenzugriff für CodexBar (Systemeinstellungen > Datenschutz & Sicherheit)."; +"ollama_browser_cookie_decryption_denied" = "Die Entschlüsselung der %@-Cookies wurde im Schlüsselbund abgelehnt; versuchen Sie es mit einer manuellen Aktualisierung erneut."; +"ollama_browser_cookie_decryption_disabled" = "Die Entschlüsselung der %@-Cookies ist in CodexBar deaktiviert; aktivieren Sie den Schlüsselbundzugriff und aktualisieren Sie."; + " providers" = "Anbieter"; "(System)" = "(System)"; "30d" = "30d"; diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index 27437fd608..176ff795a0 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* English localization for CodexBar (base/fallback) */ +"ollama_safari_cookie_access_hint" = "Safari cookies need Full Disk Access for CodexBar (System Settings > Privacy & Security)."; +"ollama_browser_cookie_decryption_denied" = "%@ cookie decryption was declined in Keychain; retry with a manual refresh."; +"ollama_browser_cookie_decryption_disabled" = "%@ cookie decryption is disabled in CodexBar; enable Keychain access and refresh."; + " providers" = " providers"; "(System)" = "(System)"; "30d" = "30d"; diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings index 1cc5823804..dfd3a25fda 100644 --- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Spanish localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "Las cookies de Safari necesitan acceso total al disco para CodexBar (Ajustes del Sistema > Privacidad y seguridad)."; +"ollama_browser_cookie_decryption_denied" = "Se rechazó en el Llavero el descifrado de las cookies de %@; vuelve a intentarlo con una actualización manual."; +"ollama_browser_cookie_decryption_disabled" = "El descifrado de las cookies de %@ está desactivado en CodexBar; activa el acceso al Llavero y actualiza."; + " providers" = " proveedores"; "(System)" = "(Sistema)"; "30d" = "30 d"; diff --git a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings index b524d9bb27..8b58a2e23f 100644 --- a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Persian localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "کوکی‌های Safari برای CodexBar به دسترسی کامل دیسک نیاز دارند (تنظیمات سیستم > حریم خصوصی و امنیت)."; +"ollama_browser_cookie_decryption_denied" = "رمزگشایی کوکی‌های %@ در Keychain رد شد؛ با یک تازه‌سازی دستی دوباره تلاش کنید."; +"ollama_browser_cookie_decryption_disabled" = "رمزگشایی کوکی‌های %@ در CodexBar غیرفعال است؛ دسترسی Keychain را فعال و تازه‌سازی کنید."; + " providers" = " providers"; "(System)" = "(سیستم)"; "30d" = "30 روز"; diff --git a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings index 72b0aaef22..93a69e83c6 100644 --- a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* English localization for CodexBar (base/fallback) */ +"ollama_safari_cookie_access_hint" = "Les cookies Safari nécessitent l’accès complet au disque pour CodexBar (Réglages Système > Confidentialité et sécurité)."; +"ollama_browser_cookie_decryption_denied" = "Le déchiffrement des cookies %@ a été refusé dans le Trousseau ; réessayez avec une actualisation manuelle."; +"ollama_browser_cookie_decryption_disabled" = "Le déchiffrement des cookies %@ est désactivé dans CodexBar ; activez l’accès au Trousseau et actualisez."; + " providers" = " fournisseurs"; "(System)" = "(System)"; "30d" = "30d"; diff --git a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings index 761318953f..7e02b5e6a6 100644 --- a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Galician localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "As cookies de Safari precisan acceso total ao disco para CodexBar (Axustes do Sistema > Privacidade e seguridade)."; +"ollama_browser_cookie_decryption_denied" = "Rexeitouse no Chaveiro o descifrado das cookies de %@; téntao de novo cunha actualización manual."; +"ollama_browser_cookie_decryption_disabled" = "O descifrado das cookies de %@ está desactivado en CodexBar; activa o acceso ao Chaveiro e actualiza."; + " providers" = " provedores"; "(System)" = "(Sistema)"; "30d" = "30d"; diff --git a/Sources/CodexBar/Resources/id.lproj/Localizable.strings b/Sources/CodexBar/Resources/id.lproj/Localizable.strings index 7dd2d7d286..bf6b25181a 100644 --- a/Sources/CodexBar/Resources/id.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/id.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Indonesian localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "Cookie Safari memerlukan Akses Disk Penuh untuk CodexBar (Pengaturan Sistem > Privasi & Keamanan)."; +"ollama_browser_cookie_decryption_denied" = "Dekripsi cookie %@ ditolak di Rantai Kunci; coba lagi dengan penyegaran manual."; +"ollama_browser_cookie_decryption_disabled" = "Dekripsi cookie %@ dinonaktifkan di CodexBar; aktifkan akses Rantai Kunci lalu segarkan."; + " providers" = " penyedia"; "(System)" = "(Sistem)"; "30d" = "30 hari"; diff --git a/Sources/CodexBar/Resources/it.lproj/Localizable.strings b/Sources/CodexBar/Resources/it.lproj/Localizable.strings index 34519457f7..02d89d7f6d 100644 --- a/Sources/CodexBar/Resources/it.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/it.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Italian localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "I cookie di Safari richiedono l’accesso completo al disco per CodexBar (Impostazioni di Sistema > Privacy e sicurezza)."; +"ollama_browser_cookie_decryption_denied" = "La decrittografia dei cookie di %@ è stata rifiutata nel Portachiavi; riprova con un aggiornamento manuale."; +"ollama_browser_cookie_decryption_disabled" = "La decrittografia dei cookie di %@ è disabilitata in CodexBar; abilita l’accesso al Portachiavi e aggiorna."; + " providers" = " provider"; "(System)" = "(Sistema)"; "30d" = "30 g"; diff --git a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings index 490b2b4c73..7dbbc9db4a 100644 --- a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Japanese localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "Safari Cookie を読み込むには、CodexBar にフルディスクアクセスが必要です(システム設定 > プライバシーとセキュリティ)。"; +"ollama_browser_cookie_decryption_denied" = "%@ Cookie の復号がキーチェーンで拒否されました。手動で更新して再試行してください。"; +"ollama_browser_cookie_decryption_disabled" = "%@ Cookie の復号が CodexBar で無効です。キーチェーンへのアクセスを有効にして更新してください。"; + " providers" = " 件のプロバイダ"; "(System)" = "(システム)"; "30d" = "30日"; diff --git a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings index c12a84440a..be87d3be73 100644 --- a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Korean (한국어) localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "Safari 쿠키를 읽으려면 CodexBar에 전체 디스크 접근 권한이 필요합니다(시스템 설정 > 개인정보 보호 및 보안)."; +"ollama_browser_cookie_decryption_denied" = "%@ 쿠키 복호화가 키체인에서 거부되었습니다. 수동 새로 고침으로 다시 시도하세요."; +"ollama_browser_cookie_decryption_disabled" = "%@ 쿠키 복호화가 CodexBar에서 비활성화되어 있습니다. 키체인 접근을 활성화하고 새로 고침하세요."; + " providers" = " 공급자"; "(System)" = "(시스템)"; "30d" = "30일"; diff --git a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings index e4d70185f9..9bd481d3ae 100644 --- a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Dutch localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "Safari-cookies vereisen volledige schijftoegang voor CodexBar (Systeeminstellingen > Privacy en beveiliging)."; +"ollama_browser_cookie_decryption_denied" = "Het ontsleutelen van %@-cookies is geweigerd in Sleutelhanger; probeer opnieuw met handmatig vernieuwen."; +"ollama_browser_cookie_decryption_disabled" = "Het ontsleutelen van %@-cookies is uitgeschakeld in CodexBar; schakel Sleutelhangertoegang in en vernieuw."; + " providers" = " providers"; "(System)" = "(Systeem)"; "30d" = "30d"; diff --git a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings index 279e8a5023..430df48b26 100644 --- a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* English localization for CodexBar (base/fallback) */ +"ollama_safari_cookie_access_hint" = "Pliki cookie Safari wymagają pełnego dostępu do dysku dla CodexBar (Ustawienia systemowe > Prywatność i ochrona)."; +"ollama_browser_cookie_decryption_denied" = "Odszyfrowanie plików cookie %@ zostało odrzucone w Pęku kluczy; spróbuj ponownie przez ręczne odświeżenie."; +"ollama_browser_cookie_decryption_disabled" = "Odszyfrowanie plików cookie %@ jest wyłączone w CodexBar; włącz dostęp do Pęku kluczy i odśwież."; + " providers" = " providers"; "(System)" = "(System)"; "30d" = "30d"; diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings index 2295844532..854b0eff68 100644 --- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Brazilian Portuguese localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "Os cookies do Safari precisam de Acesso Total ao Disco para o CodexBar (Ajustes do Sistema > Privacidade e Segurança)."; +"ollama_browser_cookie_decryption_denied" = "A descriptografia dos cookies do %@ foi recusada nas Chaves; tente novamente com uma atualização manual."; +"ollama_browser_cookie_decryption_disabled" = "A descriptografia dos cookies do %@ está desativada no CodexBar; ative o acesso às Chaves e atualize."; + " providers" = " provedores"; "(System)" = "(Sistema)"; "30d" = "30d"; diff --git a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings index 745e217c7e..a971897486 100644 --- a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Russian localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "Для файлов cookie Safari приложению CodexBar нужен полный доступ к диску (Системные настройки > Конфиденциальность и безопасность)."; +"ollama_browser_cookie_decryption_denied" = "Расшифровка файлов cookie %@ была отклонена в Связке ключей; повторите попытку с помощью ручного обновления."; +"ollama_browser_cookie_decryption_disabled" = "Расшифровка файлов cookie %@ отключена в CodexBar; включите доступ к Связке ключей и обновите."; + " providers" = " провайдеров"; "(System)" = "(Система)"; "30d" = "30 дн."; diff --git a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings index 7241af848d..784403c4c5 100644 --- a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Swedish localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "Safari-cookies kräver full skivåtkomst för CodexBar (Systeminställningar > Integritet och säkerhet)."; +"ollama_browser_cookie_decryption_denied" = "Dekryptering av %@-cookies nekades i Nyckelhanteraren; försök igen med en manuell uppdatering."; +"ollama_browser_cookie_decryption_disabled" = "Dekryptering av %@-cookies är inaktiverad i CodexBar; aktivera åtkomst till Nyckelhanteraren och uppdatera."; + " providers" = " leverantörer"; "(System)" = "(System)"; "30d" = "30 d"; diff --git a/Sources/CodexBar/Resources/th.lproj/Localizable.strings b/Sources/CodexBar/Resources/th.lproj/Localizable.strings index 8fe4f65d57..cc97926d75 100644 --- a/Sources/CodexBar/Resources/th.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/th.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Thai localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "คุกกี้ Safari ต้องใช้สิทธิ์เข้าถึงดิสก์แบบเต็มสำหรับ CodexBar (การตั้งค่าระบบ > ความเป็นส่วนตัวและความปลอดภัย)"; +"ollama_browser_cookie_decryption_denied" = "การถอดรหัสคุกกี้ %@ ถูกปฏิเสธในพวงกุญแจ โปรดลองอีกครั้งด้วยการรีเฟรชด้วยตนเอง"; +"ollama_browser_cookie_decryption_disabled" = "การถอดรหัสคุกกี้ %@ ถูกปิดใช้งานใน CodexBar ให้เปิดการเข้าถึงพวงกุญแจแล้วรีเฟรช"; + " providers" = "ผู้ให้บริการ "; "(System)" = "(ระบบ)"; "30d" = "30 วัน"; diff --git a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings index ce86e7092f..2d875924a8 100644 --- a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Turkish localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "Safari çerezleri için CodexBar’a Tam Disk Erişimi gerekir (Sistem Ayarları > Gizlilik ve Güvenlik)."; +"ollama_browser_cookie_decryption_denied" = "%@ çerezlerinin şifresini çözme Anahtar Zinciri’nde reddedildi; manuel yenilemeyle tekrar deneyin."; +"ollama_browser_cookie_decryption_disabled" = "%@ çerezlerinin şifresini çözme CodexBar’da devre dışı; Anahtar Zinciri erişimini etkinleştirip yenileyin."; + " providers" = " sağlayıcı"; "(System)" = "(Sistem)"; "30d" = "30 gün"; diff --git a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings index 77770d861f..6c3e4461e6 100644 --- a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Ukrainian localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "Для файлів cookie Safari програмі CodexBar потрібен повний доступ до диска (Системні параметри > Конфіденційність і безпека)."; +"ollama_browser_cookie_decryption_denied" = "Розшифрування файлів cookie %@ було відхилено у В’язці ключів; повторіть спробу за допомогою ручного оновлення."; +"ollama_browser_cookie_decryption_disabled" = "Розшифрування файлів cookie %@ вимкнено в CodexBar; увімкніть доступ до В’язки ключів і оновіть."; + " providers" = "провайдерів"; "(System)" = "(Система)"; "30d" = "30д"; diff --git a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings index 765c9d31cb..ec03dacbbc 100644 --- a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* English localization for CodexBar (base/fallback) */ +"ollama_safari_cookie_access_hint" = "Cookie Safari cần Quyền truy cập toàn bộ ổ đĩa cho CodexBar (Cài đặt hệ thống > Quyền riêng tư & Bảo mật)."; +"ollama_browser_cookie_decryption_denied" = "Việc giải mã cookie %@ đã bị từ chối trong Chuỗi khóa; hãy thử lại bằng cách làm mới thủ công."; +"ollama_browser_cookie_decryption_disabled" = "Việc giải mã cookie %@ bị tắt trong CodexBar; hãy bật quyền truy cập Chuỗi khóa rồi làm mới."; + " providers" = "nhà cung cấp"; "(System)" = "(Hệ thống)"; "30d" = "30d"; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index 73f1b63937..630f4a3f46 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Chinese (Simplified) localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "CodexBar 需要“完全磁盘访问权限”才能读取 Safari Cookie(系统设置 > 隐私与安全性)。"; +"ollama_browser_cookie_decryption_denied" = "钥匙串拒绝解密 %@ Cookie;请通过手动刷新重试。"; +"ollama_browser_cookie_decryption_disabled" = "CodexBar 中已停用 %@ Cookie 解密;请启用钥匙串访问权限并刷新。"; + " providers" = " 提供商"; "(System)" = "(System)"; "30d" = "30 天"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index 2fca662b20..eb0ddfd1fe 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -1,5 +1,9 @@ /* Chinese (Traditional) localization for CodexBar */ +"ollama_safari_cookie_access_hint" = "CodexBar 需要「完整磁碟存取權」才能讀取 Safari Cookie(系統設定 > 隱私權與安全性)。"; +"ollama_browser_cookie_decryption_denied" = "鑰匙圈拒絕解密 %@ Cookie;請透過手動重新整理再試一次。"; +"ollama_browser_cookie_decryption_disabled" = "CodexBar 中已停用 %@ Cookie 解密;請啟用鑰匙圈存取權並重新整理。"; + " providers" = " 提供者"; "(System)" = "(系統)"; "30d" = "30 天"; diff --git a/Sources/CodexBar/UsageStore+Accessors.swift b/Sources/CodexBar/UsageStore+Accessors.swift index a54aeb1fd3..73914cc661 100644 --- a/Sources/CodexBar/UsageStore+Accessors.swift +++ b/Sources/CodexBar/UsageStore+Accessors.swift @@ -44,8 +44,14 @@ extension UsageStore { func userFacingError(for provider: UsageProvider) -> String? { if let raw = self.errors[provider] { - guard provider == .codex else { return raw } - return CodexUIErrorMapper.userFacingMessage(raw) + switch provider { + case .codex: + return CodexUIErrorMapper.userFacingMessage(raw) + case .ollama: + return OllamaUIErrorMapper.userFacingMessage(raw) + default: + return raw + } } if let diagnostic = self.diagnostics[provider] { return diagnostic diff --git a/Sources/CodexBarCore/BrowserCookieAccessGate.swift b/Sources/CodexBarCore/BrowserCookieAccessGate.swift index 569cf74fba..754044de60 100644 --- a/Sources/CodexBarCore/BrowserCookieAccessGate.swift +++ b/Sources/CodexBarCore/BrowserCookieAccessGate.swift @@ -204,6 +204,19 @@ public enum BrowserCookieAccessGate { ]) } + static func hasActiveDenial(for browser: Browser, now: Date = Date()) -> Bool { + guard browser.usesKeychainForCookieDecryption else { return false } + return self.lock.withLock { state in + self.loadIfNeeded(&state) + guard let blockedUntil = state.deniedUntilByBrowser[browser.rawValue] else { return false } + guard blockedUntil <= now else { return true } + state.deniedUntilByBrowser.removeValue(forKey: browser.rawValue) + state.chromiumFamilyDeniedUntil = state.deniedUntilByBrowser.values.max() + self.persist(state) + return false + } + } + static func recordAllowed(for browser: Browser) { guard browser.usesKeychainForCookieDecryption, ProviderInteractionContext.current == .userInitiated, @@ -356,6 +369,10 @@ public enum BrowserCookieAccessGate { public static func recordIfNeeded(_ error: Error, now: Date = Date()) {} public static func recordDenied(for browser: Browser, now: Date = Date()) {} + static func hasActiveDenial(for browser: Browser, now: Date = Date()) -> Bool { + false + } + public static func resetForTesting() {} } #endif diff --git a/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift b/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift index 6b93969d0d..c66af2bfd1 100644 --- a/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift @@ -18,7 +18,9 @@ private let ollamaSessionCookieNames: Set = [ ] private func isRecognizedOllamaSessionCookieName(_ name: String) -> Bool { - if ollamaSessionCookieNames.contains(name) { return true } + if ollamaSessionCookieNames.contains(name) { + return true + } // next-auth can split tokens into chunked cookies: `.0`, `.1`, ... return name.hasPrefix("__Secure-next-auth.session-token.") || name.hasPrefix("next-auth.session-token.") @@ -40,6 +42,9 @@ public enum OllamaUsageError: LocalizedError, Sendable { case parseFailed(String) case networkError(String) case noSessionCookie + case safariCookieAccessDenied + case browserCookieDecryptionDenied(String) + case browserCookieDecryptionDisabled(String) public var errorDescription: String? { switch self { @@ -57,6 +62,12 @@ public enum OllamaUsageError: LocalizedError, Sendable { "Ollama request failed: \(message)" case .noSessionCookie: "No Ollama session cookie found. Please sign in at \(Self.signInURL) in your browser." + case .safariCookieAccessDenied: + "Safari cookies need Full Disk Access for CodexBar (System Settings > Privacy & Security)." + case let .browserCookieDecryptionDenied(browserName): + "\(browserName) cookie decryption was declined in Keychain; retry with a manual refresh." + case let .browserCookieDecryptionDisabled(browserName): + "\(browserName) cookie decryption is disabled in CodexBar; enable Keychain access and refresh." } } } @@ -92,23 +103,41 @@ public enum OllamaCookieImporter { logger: ((String) -> Void)? = nil) throws -> [SessionInfo] { let log: (String) -> Void = { msg in logger?("[ollama-cookie] \(msg)") } - let preferredSources = preferredBrowsers.isEmpty - ? ollamaCookieImportOrder.cookieImportCandidates(using: browserDetection) - : preferredBrowsers.cookieImportCandidates(using: browserDetection) - let preferredCandidates = self.collectSessionInfo(from: preferredSources, logger: log) - return try self.selectSessionInfosWithFallback( - preferredCandidates: preferredCandidates, - allowFallbackBrowsers: allowFallbackBrowsers, - loadFallbackCandidates: { - guard !preferredBrowsers.isEmpty else { return [] } - let fallbackSources = self.fallbackBrowserSources( - browserDetection: browserDetection, - excluding: preferredSources) - guard !fallbackSources.isEmpty else { return [] } - log("No recognized Ollama session in preferred browsers; trying fallback import order") - return self.collectSessionInfo(from: fallbackSources, logger: log) - }, - logger: log) + var accessError: OllamaUsageError? + let preferredOrder = preferredBrowsers.isEmpty ? ollamaCookieImportOrder : preferredBrowsers + let preferredSources = self.cookieSources( + from: preferredOrder, + browserDetection: browserDetection, + accessError: &accessError) + let preferredCandidates = self.collectSessionInfo( + from: preferredSources, + logger: log, + accessError: &accessError) + do { + return try self.selectSessionInfos(from: preferredCandidates, logger: log) + } catch OllamaUsageError.noSessionCookie { + guard allowFallbackBrowsers, !preferredBrowsers.isEmpty else { + throw accessError ?? OllamaUsageError.noSessionCookie + } + } + + let fallbackOrder = ollamaCookieImportOrder.filter { !preferredBrowsers.contains($0) } + let fallbackSources = self.cookieSources( + from: fallbackOrder, + browserDetection: browserDetection, + accessError: &accessError) + if !fallbackSources.isEmpty { + log("No recognized Ollama session in preferred browsers; trying fallback import order") + } + let fallbackCandidates = self.collectSessionInfo( + from: fallbackSources, + logger: log, + accessError: &accessError) + do { + return try self.selectSessionInfos(from: fallbackCandidates, logger: log) + } catch OllamaUsageError.noSessionCookie { + throw accessError ?? OllamaUsageError.noSessionCookie + } } public static func importSession( @@ -193,18 +222,53 @@ public enum OllamaCookieImporter { return first } - private static func fallbackBrowserSources( + static func accessError(from error: Error) -> OllamaUsageError? { + guard case let BrowserCookieError.accessDenied(browser, _) = error else { return nil } + if browser == .safari { + return .safariCookieAccessDenied + } + guard browser.usesKeychainForCookieDecryption else { return nil } + return .browserCookieDecryptionDenied(browser.displayName) + } + + static func suppressedAccessError(for browser: Browser, now: Date = Date()) -> OllamaUsageError? { + guard browser.usesKeychainForCookieDecryption else { return nil } + if KeychainAccessGate.isDisabled { + return .browserCookieDecryptionDisabled(browser.displayName) + } + guard BrowserCookieAccessGate.hasActiveDenial(for: browser, now: now) else { return nil } + return .browserCookieDecryptionDenied(browser.displayName) + } + + private static func cookieSources( + from browserOrder: [Browser], browserDetection: BrowserDetection, - excluding triedSources: [Browser]) -> [Browser] + accessError: inout OllamaUsageError?) -> [Browser] + { + var sources: [Browser] = [] + for browser in browserOrder where browserDetection.isCookieSourceAvailable(browser) { + guard self.shouldAttemptCookieSource(browser, accessError: &accessError) else { continue } + sources.append(browser) + } + return sources + } + + static func shouldAttemptCookieSource( + _ browser: Browser, + now: Date = Date(), + accessError: inout OllamaUsageError?) -> Bool { - let tried = Set(triedSources) - return ollamaCookieImportOrder.cookieImportCandidates(using: browserDetection) - .filter { !tried.contains($0) } + guard BrowserCookieAccessGate.shouldAttempt(browser, now: now) else { + accessError = accessError ?? self.suppressedAccessError(for: browser, now: now) + return false + } + return true } private static func collectSessionInfo( from browserSources: [Browser], - logger: @escaping (String) -> Void) -> [SessionInfo] + logger: @escaping (String) -> Void, + accessError: inout OllamaUsageError?) -> [SessionInfo] { var candidates: [SessionInfo] = [] for browserSource in browserSources { @@ -221,6 +285,7 @@ public enum OllamaCookieImporter { } } catch { BrowserCookieAccessGate.recordIfNeeded(error) + accessError = accessError ?? self.accessError(from: error) logger("\(browserSource.displayName) cookie import failed: \(error.localizedDescription)") } } @@ -553,7 +618,9 @@ public struct OllamaUsageFetcher: Sendable { } @MainActor private static func recordDump(_ text: String) { - if self.recentDumps.count >= 5 { self.recentDumps.removeFirst() } + if self.recentDumps.count >= 5 { + self.recentDumps.removeFirst() + } self.recentDumps.append(text) } @@ -634,7 +701,9 @@ public struct OllamaUsageFetcher: Sendable { static func shouldAttachCookie(to url: URL?) -> Bool { guard url?.scheme?.lowercased() == "https" else { return false } guard let host = url?.host?.lowercased() else { return false } - if host == "ollama.com" || host == "www.ollama.com" { return true } + if host == "ollama.com" || host == "www.ollama.com" { + return true + } return host.hasSuffix(".ollama.com") } @@ -820,7 +889,9 @@ public enum OllamaAPIUsageFetcher { } private static func effectivePort(_ url: URL) -> Int? { - if let port = url.port { return port } + if let port = url.port { + return port + } switch url.scheme?.lowercased() { case "https": return 443 case "http": return 80 diff --git a/Tests/CodexBarTests/OllamaUIErrorMapperTests.swift b/Tests/CodexBarTests/OllamaUIErrorMapperTests.swift new file mode 100644 index 0000000000..bd02b52efc --- /dev/null +++ b/Tests/CodexBarTests/OllamaUIErrorMapperTests.swift @@ -0,0 +1,42 @@ +import CodexBarCore +import Testing +@testable import CodexBar + +struct OllamaUIErrorMapperTests { + @Test + func `maps Safari cookie access error to localized hint`() { + let message = OllamaUIErrorMapper.userFacingMessage( + OllamaUsageError.safariCookieAccessDenied.localizedDescription, + localize: { key in "localized:\(key)" }) + + #expect(message == "localized:ollama_safari_cookie_access_hint") + } + + @Test + func `maps Brave decryption denial with browser name`() { + let message = OllamaUIErrorMapper.userFacingMessage( + OllamaUsageError.browserCookieDecryptionDenied("Brave").localizedDescription, + localize: { key in + key == "ollama_browser_cookie_decryption_denied" ? "%@ localized denial" : key + }) + + #expect(message == "Brave localized denial") + } + + @Test + func `maps disabled Keychain access with browser name`() { + let message = OllamaUIErrorMapper.userFacingMessage( + OllamaUsageError.browserCookieDecryptionDisabled("Brave").localizedDescription, + localize: { key in + key == "ollama_browser_cookie_decryption_disabled" ? "%@ localized disabled" : key + }) + + #expect(message == "Brave localized disabled") + } + + @Test + func `preserves generic Ollama errors`() { + let raw = OllamaUsageError.noSessionCookie.localizedDescription + #expect(OllamaUIErrorMapper.userFacingMessage(raw, localize: { $0 }) == raw) + } +} diff --git a/Tests/CodexBarTests/OllamaUsageFetcherTests.swift b/Tests/CodexBarTests/OllamaUsageFetcherTests.swift index c2f141b39e..20de01569c 100644 --- a/Tests/CodexBarTests/OllamaUsageFetcherTests.swift +++ b/Tests/CodexBarTests/OllamaUsageFetcherTests.swift @@ -1,6 +1,9 @@ import Foundation import Testing @testable import CodexBarCore +#if os(macOS) +import SweetCookieKit +#endif struct OllamaUsageFetcherTests { @Test @@ -132,6 +135,84 @@ struct OllamaUsageFetcherTests { #expect(OllamaCookieImporter.defaultAllowFallbackBrowsers) } + @Test + func `cookie access errors map only unambiguous recovery paths`() { + let safari = OllamaCookieImporter.accessError(from: BrowserCookieError.accessDenied( + browser: .safari, + details: "Enable Full Disk Access.")) + guard case .safariCookieAccessDenied = safari else { + Issue.record("Expected Safari Full Disk Access error") + return + } + + let brave = OllamaCookieImporter.accessError(from: BrowserCookieError.accessDenied( + browser: .brave, + details: "macOS Keychain denied access.")) + guard case let .browserCookieDecryptionDenied(browserName) = brave else { + Issue.record("Expected Brave Keychain denial") + return + } + #expect(browserName == "Brave") + + let ambiguous = OllamaCookieImporter.accessError(from: BrowserCookieError.loadFailed( + browser: .brave, + details: "SQLite failed")) + #expect(ambiguous == nil) + } + + @Test + func `cookie cooldown maps only the browser that was denied`() { + BrowserCookieAccessGate.resetForTesting() + defer { BrowserCookieAccessGate.resetForTesting() } + let now = Date(timeIntervalSince1970: 1000) + + KeychainAccessGate.withTaskOverrideForTesting(false) { + BrowserCookieAccessGate.recordDenied(for: .brave, now: now) + + let brave = OllamaCookieImporter.suppressedAccessError( + for: .brave, + now: now.addingTimeInterval(1)) + guard case let .browserCookieDecryptionDenied(browserName) = brave else { + Issue.record("Expected stored Brave Keychain denial") + return + } + #expect(browserName == "Brave") + #expect(OllamaCookieImporter.suppressedAccessError( + for: .chrome, + now: now.addingTimeInterval(1)) == nil) + } + } + + @Test + func `disabled Keychain access maps to browser recovery hint`() { + KeychainAccessGate.withTaskOverrideForTesting(true) { + let error = OllamaCookieImporter.suppressedAccessError(for: .brave) + guard case let .browserCookieDecryptionDisabled(browserName) = error else { + Issue.record("Expected disabled Brave Keychain error") + return + } + #expect(browserName == "Brave") + } + } + + @Test + func `manual refresh bypasses browser denial cooldown`() async { + await BrowserCookieAccessGate.withDeniedBrowsersForTesting([.brave]) { + KeychainAccessGate.withTaskOverrideForTesting(false) { + BrowserCookieAccessGate.withExplicitRetry { + ProviderInteractionContext.$current.withValue(.userInitiated) { + var accessError: OllamaUsageError? + let shouldAttempt = OllamaCookieImporter.shouldAttemptCookieSource( + .brave, + accessError: &accessError) + #expect(shouldAttempt) + #expect(accessError == nil) + } + } + } + } + } + @Test func `cookie selector skips session like noise and finds recognized cookie`() throws { let first = OllamaCookieImporter.SessionInfo(