1568 lines
69 KiB
Swift
1568 lines
69 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
|
|
struct OpenAIWebRefreshGateContext {
|
|
let force: Bool
|
|
let accountDidChange: Bool
|
|
let lastError: String?
|
|
let lastSnapshotAt: Date?
|
|
let lastAttemptAt: Date?
|
|
let now: Date
|
|
let refreshInterval: TimeInterval
|
|
}
|
|
|
|
struct OpenAIWebRefreshPolicyContext {
|
|
let accessEnabled: Bool
|
|
let batterySaverEnabled: Bool
|
|
let force: Bool
|
|
let refreshPhase: ProviderRefreshPhase
|
|
}
|
|
|
|
// MARK: - OpenAI web lifecycle
|
|
|
|
extension UsageStore {
|
|
private struct OpenAIDashboardRefreshContext {
|
|
let targetEmail: String?
|
|
let allowCurrentSnapshotFallback: Bool
|
|
let expectedGuard: CodexAccountScopedRefreshGuard?
|
|
let refreshTaskToken: UUID
|
|
let allowCodexUsageBackfill: Bool
|
|
let force: Bool
|
|
}
|
|
|
|
private struct OpenAIDashboardCookieImportRequest {
|
|
let normalizedTarget: String?
|
|
let allowAnyAccount: Bool
|
|
let cookieSource: ProviderCookieSource
|
|
let cacheScope: CookieHeaderCache.Scope?
|
|
let preferCachedCookieHeader: Bool?
|
|
let force: Bool
|
|
}
|
|
|
|
private static let openAIWebRefreshMultiplier: TimeInterval = 5
|
|
private static let openAIWebPrimaryFetchTimeout: TimeInterval = 25
|
|
private static let openAIWebRetryFetchTimeout: TimeInterval = 8
|
|
private static let openAIWebPostImportFetchTimeout: TimeInterval = 25
|
|
|
|
static func openAIWebDashboardFetchTimeout(didImportCookies: Bool) -> TimeInterval {
|
|
didImportCookies ? self.openAIWebPostImportFetchTimeout : self.openAIWebPrimaryFetchTimeout
|
|
}
|
|
|
|
static func openAIWebRetryDashboardFetchTimeout(afterCookieImport: Bool) -> TimeInterval {
|
|
afterCookieImport ? self.openAIWebPostImportFetchTimeout : self.openAIWebRetryFetchTimeout
|
|
}
|
|
|
|
nonisolated static func refreshPhase(
|
|
hasCompletedInitialRefresh: Bool) -> ProviderRefreshPhase
|
|
{
|
|
hasCompletedInitialRefresh ? .regular : .startup
|
|
}
|
|
|
|
nonisolated static func openAIWebRefreshPhase(
|
|
providerRefreshPhase: ProviderRefreshPhase,
|
|
startupConnectivityRetryAttempt: Int?) -> ProviderRefreshPhase
|
|
{
|
|
startupConnectivityRetryAttempt == nil ? providerRefreshPhase : .startup
|
|
}
|
|
|
|
func openAIWebRefreshIntervalSeconds() -> TimeInterval {
|
|
let base = max(self.normalRefreshIntervalForHeuristics() ?? 0, 120)
|
|
return base * Self.openAIWebRefreshMultiplier
|
|
}
|
|
|
|
func requestOpenAIDashboardRefreshIfStale(reason: String) {
|
|
guard self.isEnabled(.codex),
|
|
self.settings.openAIWebAccessEnabled,
|
|
self.settings.codexCookieSource.isEnabled
|
|
else { return }
|
|
let now = Date()
|
|
let refreshInterval = self.openAIWebRefreshIntervalSeconds()
|
|
let dashboard = self.openAIDashboard ?? self.lastOpenAIDashboardSnapshot
|
|
let lastUpdatedAt = dashboard?.updatedAt
|
|
let needsMenuHistoryRefresh = dashboard?.dailyBreakdown.isEmpty == true &&
|
|
dashboard?.usageBreakdown.isEmpty == true
|
|
if needsMenuHistoryRefresh,
|
|
Self.shouldSkipOpenAIWebEmptyHistoryRetry(.init(
|
|
force: false,
|
|
accountDidChange: self.openAIWebAccountDidChange,
|
|
lastError: self.lastOpenAIDashboardError,
|
|
lastSnapshotAt: lastUpdatedAt,
|
|
lastAttemptAt: self.lastOpenAIDashboardAttemptAt,
|
|
now: now,
|
|
refreshInterval: refreshInterval))
|
|
{
|
|
return
|
|
}
|
|
if let lastUpdatedAt, now.timeIntervalSince(lastUpdatedAt) < refreshInterval, !needsMenuHistoryRefresh {
|
|
return
|
|
}
|
|
let stamp = now.formatted(date: .abbreviated, time: .shortened)
|
|
self.logOpenAIWeb("[\(stamp)] OpenAI web refresh request: \(reason)")
|
|
let forceRefresh = Self.forceOpenAIWebRefreshForStaleRequest(
|
|
batterySaverEnabled: self.settings.openAIWebBatterySaverEnabled) || needsMenuHistoryRefresh
|
|
self.openAIWebLogger.info(
|
|
"OpenAI web stale refresh gate",
|
|
metadata: [
|
|
"reason": reason,
|
|
"force": forceRefresh ? "1" : "0",
|
|
"batterySaverEnabled": self.settings.openAIWebBatterySaverEnabled ? "1" : "0",
|
|
"interaction": ProviderInteractionContext.current == .userInitiated ? "user" : "background",
|
|
])
|
|
let expectedGuard = self.freshCodexOpenAIWebRefreshGuard()
|
|
Task { await self.refreshOpenAIDashboardIfNeeded(force: forceRefresh, expectedGuard: expectedGuard) }
|
|
}
|
|
|
|
func applyOpenAIDashboard(
|
|
_ dash: OpenAIDashboardSnapshot,
|
|
targetEmail: String?,
|
|
expectedGuard: CodexAccountScopedRefreshGuard? = nil,
|
|
refreshTaskToken: UUID? = nil,
|
|
allowCodexUsageBackfill: Bool = true) async
|
|
{
|
|
guard self.shouldApplyOpenAIDashboardRefreshTask(token: refreshTaskToken) else { return }
|
|
self.settings.invalidateCodexAccountReconciliationSnapshotCache()
|
|
let authority = self.evaluateCodexDashboardAuthority(
|
|
dashboard: dash,
|
|
sourceKind: .liveWeb,
|
|
routingTargetEmail: targetEmail)
|
|
if let expectedGuard {
|
|
let shouldApply = switch authority.decision.disposition {
|
|
case .attach:
|
|
self.shouldApplyOpenAIDashboardRefreshGuard(
|
|
expectedGuard: expectedGuard,
|
|
routingTargetEmail: targetEmail)
|
|
case .displayOnly, .failClosed:
|
|
self.shouldApplyOpenAIDashboardPolicyResult(
|
|
expectedGuard: expectedGuard,
|
|
routingTargetEmail: targetEmail)
|
|
}
|
|
guard shouldApply else { return }
|
|
}
|
|
|
|
let attachedAccountEmail = self.codexDashboardAttachmentEmail(from: authority.input)
|
|
self.reconcileCodexPublishedUsageOwner(with: self.freshCodexAccountScopedRefreshGuard(
|
|
preferCurrentSnapshot: true,
|
|
allowLastKnownLiveFallback: false))
|
|
|
|
await self.applyOpenAIDashboardAuthorityDecision(
|
|
authority.decision,
|
|
dashboard: dash,
|
|
authorityInput: authority.input,
|
|
attachedAccountEmail: attachedAccountEmail,
|
|
allowCodexUsageBackfill: allowCodexUsageBackfill)
|
|
}
|
|
|
|
func applyOpenAIDashboardFailure(
|
|
message: String,
|
|
expectedGuard: CodexAccountScopedRefreshGuard? = nil,
|
|
refreshTaskToken: UUID? = nil,
|
|
routingTargetEmail: String? = nil) async
|
|
{
|
|
guard self.shouldApplyOpenAIDashboardRefreshTask(token: refreshTaskToken) else { return }
|
|
if let expectedGuard,
|
|
!self.shouldApplyOpenAIWebNonSuccessResult(
|
|
expectedGuard: expectedGuard,
|
|
routingTargetEmail: routingTargetEmail)
|
|
{
|
|
return
|
|
}
|
|
if self.openAIWebManagedTargetStoreIsUnreadable() {
|
|
await self.failClosedRefreshForUnreadableManagedCodexStore()
|
|
return
|
|
}
|
|
if self.openAIWebManagedTargetIsMissing() {
|
|
await self.failClosedRefreshForMissingManagedCodexTarget()
|
|
return
|
|
}
|
|
if self.openAIWebProfileTargetEmailIsMissing() {
|
|
await self.failClosedRefreshForMissingProfileCodexTarget()
|
|
return
|
|
}
|
|
|
|
OpenAIDashboardFetcher.evictAllCachedWebViews()
|
|
await MainActor.run {
|
|
if let cached = self.lastOpenAIDashboardSnapshot {
|
|
self.openAIDashboard = cached
|
|
self.openAIDashboardAttachmentAuthorized = self.lastOpenAIDashboardAttachmentAuthorized
|
|
let stamp = cached.updatedAt.formatted(date: .abbreviated, time: .shortened)
|
|
self.lastOpenAIDashboardError =
|
|
"Last OpenAI dashboard refresh failed: \(message). Cached values from \(stamp)."
|
|
} else {
|
|
self.lastOpenAIDashboardError = message
|
|
self.openAIDashboard = nil
|
|
self.openAIDashboardAttachmentAuthorized = false
|
|
}
|
|
}
|
|
}
|
|
|
|
func applyOpenAIDashboardLoginRequiredFailure(
|
|
expectedGuard: CodexAccountScopedRefreshGuard? = nil,
|
|
refreshTaskToken: UUID? = nil,
|
|
routingTargetEmail: String? = nil) async
|
|
{
|
|
guard self.shouldApplyOpenAIDashboardRefreshTask(token: refreshTaskToken) else { return }
|
|
if let expectedGuard,
|
|
!self.shouldApplyOpenAIWebNonSuccessResult(
|
|
expectedGuard: expectedGuard,
|
|
routingTargetEmail: routingTargetEmail)
|
|
{
|
|
return
|
|
}
|
|
if self.openAIWebManagedTargetStoreIsUnreadable() {
|
|
await self.failClosedRefreshForUnreadableManagedCodexStore()
|
|
return
|
|
}
|
|
if self.openAIWebManagedTargetIsMissing() {
|
|
await self.failClosedRefreshForMissingManagedCodexTarget()
|
|
return
|
|
}
|
|
if self.openAIWebProfileTargetEmailIsMissing() {
|
|
await self.failClosedRefreshForMissingProfileCodexTarget()
|
|
return
|
|
}
|
|
|
|
OpenAIDashboardFetcher.evictAllCachedWebViews()
|
|
await MainActor.run {
|
|
self.lastOpenAIDashboardError = [
|
|
"OpenAI web access requires a signed-in chatgpt.com session.",
|
|
"Sign in using \(self.codexBrowserCookieOrder.loginHint), " +
|
|
"then update OpenAI cookies in Providers → Codex.",
|
|
].joined(separator: " ")
|
|
self.openAIDashboard = self.lastOpenAIDashboardSnapshot
|
|
self.openAIDashboardAttachmentAuthorized = self.lastOpenAIDashboardAttachmentAuthorized
|
|
self.openAIDashboardRequiresLogin = true
|
|
}
|
|
}
|
|
|
|
private func failClosedOpenAIDashboardSnapshot() {
|
|
self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
|
|
self.openAIDashboardRequiresLogin = true
|
|
}
|
|
|
|
private func applyOpenAIDashboardAuthorityDecision(
|
|
_ decision: CodexDashboardAuthorityDecision,
|
|
dashboard: OpenAIDashboardSnapshot,
|
|
authorityInput: CodexDashboardAuthorityInput,
|
|
attachedAccountEmail: String?,
|
|
allowCodexUsageBackfill: Bool) async
|
|
{
|
|
switch decision.disposition {
|
|
case .attach:
|
|
self.openAIDashboard = dashboard
|
|
self.openAIDashboardAttachmentAuthorized = true
|
|
self.lastOpenAIDashboardSnapshot = dashboard
|
|
self.lastOpenAIDashboardAttachmentAuthorized = true
|
|
self.lastOpenAIDashboardError = nil
|
|
self.openAIDashboardRequiresLogin = false
|
|
|
|
if decision.allowedEffects.contains(.usageBackfill),
|
|
allowCodexUsageBackfill,
|
|
self.snapshots[.codex] == nil,
|
|
let usage = dashboard.toUsageSnapshot(provider: .codex, accountEmail: attachedAccountEmail),
|
|
CodexWeeklyResetConfirmation.initialDecision(previous: nil, initial: usage) == .publishInitial
|
|
{
|
|
self.snapshots[.codex] = usage
|
|
self.errors[.codex] = nil
|
|
self.failureGates[.codex]?.recordSuccess()
|
|
self.lastSourceLabels[.codex] = "openai-web"
|
|
self.lastCodexUsagePublicationGuard = self.currentCodexAccountScopedRefreshGuard(
|
|
preferCurrentSnapshot: true,
|
|
allowLastKnownLiveFallback: false)
|
|
}
|
|
|
|
if decision.allowedEffects.contains(.creditsAttachment),
|
|
self.credits == nil,
|
|
let credits = dashboard.toCreditsSnapshot()
|
|
{
|
|
self.credits = credits
|
|
self.lastCreditsSnapshot = credits
|
|
self.lastCreditsSnapshotAccountKey = Self.normalizeCodexAccountScopedKey(attachedAccountEmail)
|
|
self.lastCreditsSource = .dashboardWeb
|
|
self.lastCreditsError = nil
|
|
self.creditsFailureStreak = 0
|
|
}
|
|
|
|
if decision.allowedEffects.contains(.refreshGuardSeed) {
|
|
self.seedCodexAccountScopedRefreshGuard(accountEmail: attachedAccountEmail)
|
|
}
|
|
|
|
if let attachedAccountEmail, !attachedAccountEmail.isEmpty {
|
|
OpenAIDashboardCacheStore.save(OpenAIDashboardCache(
|
|
accountEmail: attachedAccountEmail,
|
|
snapshot: dashboard))
|
|
}
|
|
|
|
if decision.allowedEffects.contains(.historicalBackfill) {
|
|
self.backfillCodexHistoricalFromDashboardIfNeeded(
|
|
dashboard,
|
|
authorityDecision: decision,
|
|
attachedAccountEmail: attachedAccountEmail)
|
|
}
|
|
|
|
case .displayOnly:
|
|
self.applyOpenAIDashboardCleanup(decision.cleanup, preserveVisibleDashboard: true)
|
|
self.openAIDashboard = dashboard
|
|
self.openAIDashboardAttachmentAuthorized = false
|
|
self.lastOpenAIDashboardSnapshot = dashboard
|
|
self.lastOpenAIDashboardAttachmentAuthorized = false
|
|
self.lastOpenAIDashboardError = nil
|
|
self.openAIDashboardRequiresLogin = false
|
|
|
|
case .failClosed:
|
|
self.applyOpenAIDashboardCleanup(decision.cleanup, preserveVisibleDashboard: false)
|
|
self.lastOpenAIDashboardError = self.openAIDashboardPolicyFailureMessage(
|
|
for: decision,
|
|
authorityInput: authorityInput)
|
|
self.openAIDashboardRequiresLogin = true
|
|
}
|
|
}
|
|
|
|
private func applyOpenAIDashboardCleanup(
|
|
_ cleanup: Set<CodexDashboardCleanup>,
|
|
preserveVisibleDashboard: Bool)
|
|
{
|
|
if cleanup.contains(.dashboardDerivedUsage) {
|
|
self.clearDashboardDerivedCodexUsageIfNeeded()
|
|
}
|
|
if cleanup.contains(.dashboardDerivedCredits) {
|
|
self.clearDashboardDerivedCreditsIfNeeded()
|
|
}
|
|
if cleanup.contains(.dashboardRefreshGuardSeed) {
|
|
self.clearDashboardRefreshGuardSeedIfNeeded()
|
|
}
|
|
if cleanup.contains(.dashboardCache) {
|
|
OpenAIDashboardCacheStore.clear()
|
|
}
|
|
if cleanup.contains(.dashboardSnapshot), !preserveVisibleDashboard {
|
|
self.openAIDashboard = nil
|
|
self.openAIDashboardAttachmentAuthorized = false
|
|
self.lastOpenAIDashboardSnapshot = nil
|
|
self.lastOpenAIDashboardAttachmentAuthorized = false
|
|
}
|
|
}
|
|
|
|
private func clearDashboardDerivedCodexUsageIfNeeded() {
|
|
guard self.lastSourceLabels[.codex] == "openai-web" else { return }
|
|
self.clearCodexPublishedUsageState()
|
|
}
|
|
|
|
private func clearDashboardDerivedCreditsIfNeeded() {
|
|
guard self.lastCreditsSource == .dashboardWeb else { return }
|
|
self.credits = nil
|
|
self.lastCreditsError = nil
|
|
self.lastCreditsSnapshot = nil
|
|
self.lastCreditsSnapshotAccountKey = nil
|
|
self.lastCreditsSource = .none
|
|
self.creditsFailureStreak = 0
|
|
}
|
|
|
|
private func clearDashboardRefreshGuardSeedIfNeeded() {
|
|
let currentGuard = self.currentCodexAccountScopedRefreshGuard(
|
|
preferCurrentSnapshot: false,
|
|
allowLastKnownLiveFallback: false)
|
|
if self.snapshots[.codex] != nil,
|
|
self.lastCodexUsagePublicationGuard.map({
|
|
!Self.codexScopedRefreshGuardsMatchAccount($0, currentGuard)
|
|
}) ?? true
|
|
{
|
|
self.clearCodexPublishedUsageState()
|
|
}
|
|
self.lastCodexAccountScopedRefreshGuard = currentGuard
|
|
}
|
|
|
|
private func openAIDashboardPolicyFailureMessage(
|
|
for decision: CodexDashboardAuthorityDecision,
|
|
authorityInput: CodexDashboardAuthorityInput) -> String
|
|
{
|
|
switch decision.reason {
|
|
case let .wrongEmail(expected, actual):
|
|
[
|
|
"OpenAI dashboard signed in as \(actual ?? "unknown"), but Codex uses \(expected ?? "unknown").",
|
|
"Switch accounts in your browser and update OpenAI cookies in Providers → Codex.",
|
|
].joined(separator: " ")
|
|
case let .sameEmailAmbiguity(email):
|
|
"OpenAI dashboard ownership is ambiguous for \(email); Codex will not attach dashboard data."
|
|
case .missingDashboardSignedInEmail:
|
|
"OpenAI dashboard did not report a signed-in account. Refresh OpenAI cookies and try again."
|
|
case .unresolvedWithoutTrustedEvidence:
|
|
"OpenAI dashboard ownership could not be verified for the active Codex account."
|
|
case .providerAccountMissingScopedEmail:
|
|
"Codex account ownership could not be verified because the scoped email is unavailable."
|
|
case .providerAccountLacksExactOwnershipProof:
|
|
[
|
|
"OpenAI dashboard ownership could not be matched to the active Codex account.",
|
|
"Refresh Codex account data, then retry OpenAI web access.",
|
|
].joined(separator: " ")
|
|
case .exactProviderAccountMatch,
|
|
.trustedEmailMatchNoCompetingOwner,
|
|
.trustedContinuityNoCompetingOwner:
|
|
"OpenAI dashboard ownership policy blocked this dashboard."
|
|
}
|
|
}
|
|
|
|
func refreshOpenAIDashboardIfNeeded(
|
|
force: Bool = false,
|
|
expectedGuard: CodexAccountScopedRefreshGuard? = nil,
|
|
bypassCoalescing: Bool = false,
|
|
allowCodexUsageBackfill: Bool = true) async
|
|
{
|
|
self.syncOpenAIWebState()
|
|
guard self.isEnabled(.codex),
|
|
self.settings.openAIWebAccessEnabled,
|
|
self.settings.codexCookieSource.isEnabled
|
|
else { return }
|
|
if self.openAIWebManagedTargetStoreIsUnreadable() {
|
|
await self.failClosedRefreshForUnreadableManagedCodexStore()
|
|
return
|
|
}
|
|
if self.openAIWebManagedTargetIsMissing() {
|
|
await self.failClosedRefreshForMissingManagedCodexTarget()
|
|
return
|
|
}
|
|
if self.openAIWebProfileTargetEmailIsMissing() {
|
|
await self.failClosedRefreshForMissingProfileCodexTarget()
|
|
return
|
|
}
|
|
|
|
let allowCurrentSnapshotFallback = expectedGuard?.source == .liveSystem && expectedGuard?
|
|
.identity == .unresolved
|
|
let targetEmail = self.currentCodexOpenAIWebTargetEmail(
|
|
allowCurrentSnapshotFallback: allowCurrentSnapshotFallback,
|
|
allowLastKnownLiveFallback: expectedGuard?.identity != .unresolved)
|
|
let refreshKey = self.openAIDashboardRefreshKey(targetEmail: targetEmail, expectedGuard: expectedGuard)
|
|
if !bypassCoalescing,
|
|
let task = self.openAIDashboardRefreshTask,
|
|
self.openAIDashboardRefreshTaskKey == refreshKey
|
|
{
|
|
await task.value
|
|
return
|
|
}
|
|
if bypassCoalescing {
|
|
self.openAIDashboardBackgroundRefreshTask?.cancel()
|
|
self.openAIDashboardRefreshTask?.cancel()
|
|
}
|
|
self.handleOpenAIWebTargetEmailChangeIfNeeded(
|
|
targetEmail: targetEmail,
|
|
targetScope: self.codexCookieCacheScopeForOpenAIWeb())
|
|
|
|
let now = Date()
|
|
let minInterval = self.openAIWebRefreshIntervalSeconds()
|
|
let refreshGate = OpenAIWebRefreshGateContext(
|
|
force: force,
|
|
accountDidChange: self.openAIWebAccountDidChange,
|
|
lastError: self.lastOpenAIDashboardError,
|
|
lastSnapshotAt: self.lastOpenAIDashboardSnapshot?.updatedAt,
|
|
lastAttemptAt: self.lastOpenAIDashboardAttemptAt,
|
|
now: now,
|
|
refreshInterval: minInterval)
|
|
if Self.shouldSkipOpenAIWebRefresh(refreshGate) {
|
|
return
|
|
}
|
|
self.lastOpenAIDashboardAttemptAt = now
|
|
|
|
let taskToken = UUID()
|
|
let context = OpenAIDashboardRefreshContext(
|
|
targetEmail: targetEmail,
|
|
allowCurrentSnapshotFallback: allowCurrentSnapshotFallback,
|
|
expectedGuard: expectedGuard,
|
|
refreshTaskToken: taskToken,
|
|
allowCodexUsageBackfill: allowCodexUsageBackfill,
|
|
force: force)
|
|
let task = Task { [weak self] in
|
|
guard let self else { return }
|
|
await self.performOpenAIDashboardRefreshIfNeeded(context)
|
|
}
|
|
self.openAIDashboardRefreshTask = task
|
|
self.openAIDashboardRefreshTaskKey = refreshKey
|
|
self.openAIDashboardRefreshTaskToken = taskToken
|
|
await task.value
|
|
if self.openAIDashboardRefreshTaskToken == taskToken {
|
|
self.openAIDashboardRefreshTask = nil
|
|
self.openAIDashboardRefreshTaskKey = nil
|
|
self.openAIDashboardRefreshTaskToken = nil
|
|
}
|
|
}
|
|
|
|
func scheduleOpenAIDashboardRefreshIfNeeded(expectedGuard: CodexAccountScopedRefreshGuard? = nil) {
|
|
self.syncOpenAIWebState()
|
|
let allowCurrentSnapshotFallback = expectedGuard?.source == .liveSystem && expectedGuard?
|
|
.identity == .unresolved
|
|
let targetEmail = self.currentCodexOpenAIWebTargetEmail(
|
|
allowCurrentSnapshotFallback: allowCurrentSnapshotFallback,
|
|
allowLastKnownLiveFallback: expectedGuard?.identity != .unresolved)
|
|
let refreshKey = self.openAIDashboardRefreshKey(targetEmail: targetEmail, expectedGuard: expectedGuard)
|
|
if let task = self.openAIDashboardBackgroundRefreshTask,
|
|
!task.isCancelled,
|
|
self.openAIDashboardBackgroundRefreshTaskKey == refreshKey
|
|
{
|
|
return
|
|
}
|
|
|
|
if self.openAIDashboardBackgroundRefreshTaskKey != nil,
|
|
self.openAIDashboardBackgroundRefreshTaskKey != refreshKey
|
|
{
|
|
self.invalidateOpenAIDashboardRefreshTask()
|
|
}
|
|
self.openAIDashboardBackgroundRefreshTask?.cancel()
|
|
self.openAIDashboardBackgroundRefreshTaskKey = refreshKey
|
|
self.openAIDashboardBackgroundRefreshTask = Task(priority: .utility) { @MainActor [weak self] in
|
|
guard let self else { return }
|
|
defer {
|
|
if self.openAIDashboardBackgroundRefreshTaskKey == refreshKey {
|
|
self.openAIDashboardBackgroundRefreshTask = nil
|
|
self.openAIDashboardBackgroundRefreshTaskKey = nil
|
|
}
|
|
}
|
|
|
|
guard !Task.isCancelled else { return }
|
|
await self.refreshOpenAIDashboardIfNeeded(force: false, expectedGuard: expectedGuard)
|
|
guard !Task.isCancelled else { return }
|
|
self.persistWidgetSnapshot(reason: "dashboard")
|
|
}
|
|
}
|
|
|
|
private func performOpenAIDashboardRefreshIfNeeded(_ context: OpenAIDashboardRefreshContext) async {
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
self.openAIDashboardCookieImportStatus = nil
|
|
var latestCookieImportStatus: String?
|
|
if self.openAIWebDebugLines.isEmpty {
|
|
self.resetOpenAIWebDebugLog(context: "refresh")
|
|
} else {
|
|
let stamp = Date().formatted(date: .abbreviated, time: .shortened)
|
|
self.logOpenAIWeb("[\(stamp)] OpenAI web refresh start")
|
|
}
|
|
let log: (String) -> Void = { [weak self] line in
|
|
guard let self else { return }
|
|
self.logOpenAIWeb(line)
|
|
}
|
|
|
|
do {
|
|
let normalized = context.targetEmail?
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
.lowercased()
|
|
var effectiveEmail = context.targetEmail
|
|
|
|
// Use a per-email persistent `WKWebsiteDataStore` so multiple dashboard sessions can coexist.
|
|
// Strategy:
|
|
// - Try the existing per-email WebKit cookie store first (fast; avoids Keychain prompts).
|
|
// - On login-required or account mismatch, import cookies from the configured browser order and retry once.
|
|
var didImportCookiesForRefresh = false
|
|
if self.openAIWebAccountDidChange, let targetEmail = context.targetEmail, !targetEmail.isEmpty {
|
|
// On account switches, proactively re-import cookies so we don't show stale data from the previous
|
|
// user.
|
|
let imported = await self.importOpenAIDashboardCookiesIfNeeded(
|
|
targetEmail: targetEmail,
|
|
force: true)
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
didImportCookiesForRefresh = true
|
|
latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus()
|
|
if await self.abortOpenAIDashboardRetryAfterImportFailure(
|
|
importedEmail: imported,
|
|
targetEmail: targetEmail,
|
|
expectedGuard: context.expectedGuard,
|
|
cookieImportStatus: latestCookieImportStatus,
|
|
refreshTaskToken: context.refreshTaskToken)
|
|
{
|
|
self.openAIWebAccountDidChange = false
|
|
return
|
|
}
|
|
if let imported {
|
|
effectiveEmail = imported
|
|
}
|
|
self.openAIWebAccountDidChange = false
|
|
}
|
|
|
|
var dash = try await self.loadLatestOpenAIDashboard(
|
|
accountEmail: effectiveEmail,
|
|
logger: log,
|
|
allowNavigationTimeoutRetry: context.force,
|
|
timeout: Self.openAIWebDashboardFetchTimeout(didImportCookies: didImportCookiesForRefresh))
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
|
|
if self.dashboardEmailMismatch(expected: normalized, actual: dash.signedInEmail) {
|
|
if let imported = await self.importOpenAIDashboardCookiesIfNeeded(
|
|
targetEmail: context.targetEmail,
|
|
force: true)
|
|
{
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
effectiveEmail = imported
|
|
}
|
|
latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus()
|
|
dash = try await self.loadLatestOpenAIDashboard(
|
|
accountEmail: effectiveEmail,
|
|
logger: log,
|
|
allowNavigationTimeoutRetry: context.force,
|
|
timeout: Self.openAIWebRetryDashboardFetchTimeout(afterCookieImport: true))
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
}
|
|
|
|
await self.applyOpenAIDashboard(
|
|
dash,
|
|
targetEmail: effectiveEmail,
|
|
expectedGuard: context.expectedGuard,
|
|
refreshTaskToken: context.refreshTaskToken,
|
|
allowCodexUsageBackfill: context.allowCodexUsageBackfill)
|
|
} catch let OpenAIDashboardFetcher.FetchError.noDashboardData(body) {
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
await self.retryOpenAIDashboardAfterNoData(
|
|
body: body,
|
|
context: context,
|
|
latestCookieImportStatus: &latestCookieImportStatus,
|
|
logger: log)
|
|
} catch OpenAIDashboardFetcher.FetchError.loginRequired {
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
await self.retryOpenAIDashboardAfterLoginRequired(
|
|
context: context,
|
|
latestCookieImportStatus: &latestCookieImportStatus,
|
|
logger: log)
|
|
} catch {
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
if Self.isOpenAIDashboardTimeout(error) {
|
|
await self.retryOpenAIDashboardAfterTimeout(
|
|
context: context,
|
|
latestCookieImportStatus: &latestCookieImportStatus,
|
|
logger: log)
|
|
return
|
|
}
|
|
let message = self.preferredOpenAIDashboardFailureMessage(
|
|
error: error,
|
|
targetEmail: context.targetEmail,
|
|
cookieImportStatus: latestCookieImportStatus)
|
|
await self.applyOpenAIDashboardFailure(
|
|
message: message,
|
|
expectedGuard: context.expectedGuard,
|
|
refreshTaskToken: context.refreshTaskToken,
|
|
routingTargetEmail: context.targetEmail)
|
|
}
|
|
}
|
|
|
|
private func retryOpenAIDashboardAfterTimeout(
|
|
context: OpenAIDashboardRefreshContext,
|
|
latestCookieImportStatus: inout String?,
|
|
logger: @escaping (String) -> Void) async
|
|
{
|
|
if !context.force {
|
|
OpenAIDashboardFetcher.evictAllCachedWebViews()
|
|
logger("OpenAI web refresh timed out; skipping immediate background retry.")
|
|
await self.applyOpenAIDashboardFailure(
|
|
message: L(
|
|
"OpenAI web dashboard refresh timed out. CodexBar will retry after the refresh cooldown."),
|
|
expectedGuard: context.expectedGuard,
|
|
refreshTaskToken: context.refreshTaskToken,
|
|
routingTargetEmail: context.targetEmail)
|
|
return
|
|
}
|
|
|
|
let targetEmail = self.currentCodexOpenAIWebTargetEmail(
|
|
allowCurrentSnapshotFallback: context.allowCurrentSnapshotFallback,
|
|
allowLastKnownLiveFallback: context.expectedGuard?.identity != .unresolved)
|
|
var effectiveEmail = targetEmail
|
|
let imported = await self.importOpenAIDashboardCookiesIfNeeded(
|
|
targetEmail: targetEmail,
|
|
force: true,
|
|
preferCachedCookieHeader: true)
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus()
|
|
if await self.abortOpenAIDashboardRetryAfterImportFailure(
|
|
importedEmail: imported,
|
|
targetEmail: targetEmail,
|
|
expectedGuard: context.expectedGuard,
|
|
cookieImportStatus: latestCookieImportStatus,
|
|
refreshTaskToken: context.refreshTaskToken)
|
|
{
|
|
return
|
|
}
|
|
if let imported {
|
|
effectiveEmail = imported
|
|
}
|
|
do {
|
|
let dash = try await self.loadLatestOpenAIDashboard(
|
|
accountEmail: effectiveEmail,
|
|
logger: logger,
|
|
allowNavigationTimeoutRetry: context.force,
|
|
timeout: Self.openAIWebRetryDashboardFetchTimeout(afterCookieImport: true))
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
await self.applyOpenAIDashboard(
|
|
dash,
|
|
targetEmail: effectiveEmail,
|
|
expectedGuard: context.expectedGuard,
|
|
refreshTaskToken: context.refreshTaskToken,
|
|
allowCodexUsageBackfill: context.allowCodexUsageBackfill)
|
|
} catch {
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
let message = self.preferredOpenAIDashboardFailureMessage(
|
|
error: error,
|
|
targetEmail: targetEmail,
|
|
cookieImportStatus: latestCookieImportStatus)
|
|
await self.applyOpenAIDashboardFailure(
|
|
message: message,
|
|
expectedGuard: context.expectedGuard,
|
|
refreshTaskToken: context.refreshTaskToken,
|
|
routingTargetEmail: targetEmail)
|
|
}
|
|
}
|
|
|
|
private func retryOpenAIDashboardAfterNoData(
|
|
body: String,
|
|
context: OpenAIDashboardRefreshContext,
|
|
latestCookieImportStatus: inout String?,
|
|
logger: @escaping (String) -> Void) async
|
|
{
|
|
let targetEmail = self.currentCodexOpenAIWebTargetEmail(
|
|
allowCurrentSnapshotFallback: context.allowCurrentSnapshotFallback,
|
|
allowLastKnownLiveFallback: context.expectedGuard?.identity != .unresolved)
|
|
var effectiveEmail = targetEmail
|
|
let imported = await self.importOpenAIDashboardCookiesIfNeeded(targetEmail: targetEmail, force: true)
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus()
|
|
if await self.abortOpenAIDashboardRetryAfterImportFailure(
|
|
importedEmail: imported,
|
|
targetEmail: targetEmail,
|
|
expectedGuard: context.expectedGuard,
|
|
cookieImportStatus: latestCookieImportStatus,
|
|
refreshTaskToken: context.refreshTaskToken)
|
|
{
|
|
return
|
|
}
|
|
if let imported {
|
|
effectiveEmail = imported
|
|
}
|
|
do {
|
|
let dash = try await self.loadLatestOpenAIDashboard(
|
|
accountEmail: effectiveEmail,
|
|
logger: logger,
|
|
allowNavigationTimeoutRetry: context.force,
|
|
timeout: Self.openAIWebRetryDashboardFetchTimeout(afterCookieImport: true))
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
await self.applyOpenAIDashboard(
|
|
dash,
|
|
targetEmail: effectiveEmail,
|
|
expectedGuard: context.expectedGuard,
|
|
refreshTaskToken: context.refreshTaskToken,
|
|
allowCodexUsageBackfill: context.allowCodexUsageBackfill)
|
|
} catch let OpenAIDashboardFetcher.FetchError.noDashboardData(retryBody) {
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
let finalBody = retryBody.isEmpty ? body : retryBody
|
|
let message = self.openAIDashboardFriendlyError(
|
|
body: finalBody,
|
|
targetEmail: targetEmail,
|
|
cookieImportStatus: latestCookieImportStatus)
|
|
?? OpenAIDashboardFetcher.FetchError.noDashboardData(body: finalBody).localizedDescription
|
|
await self.applyOpenAIDashboardFailure(
|
|
message: message,
|
|
expectedGuard: context.expectedGuard,
|
|
refreshTaskToken: context.refreshTaskToken,
|
|
routingTargetEmail: targetEmail)
|
|
} catch {
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
let message = self.preferredOpenAIDashboardFailureMessage(
|
|
error: error,
|
|
targetEmail: targetEmail,
|
|
cookieImportStatus: latestCookieImportStatus)
|
|
await self.applyOpenAIDashboardFailure(
|
|
message: message,
|
|
expectedGuard: context.expectedGuard,
|
|
refreshTaskToken: context.refreshTaskToken,
|
|
routingTargetEmail: targetEmail)
|
|
}
|
|
}
|
|
|
|
private func retryOpenAIDashboardAfterLoginRequired(
|
|
context: OpenAIDashboardRefreshContext,
|
|
latestCookieImportStatus: inout String?,
|
|
logger: @escaping (String) -> Void) async
|
|
{
|
|
let targetEmail = self.currentCodexOpenAIWebTargetEmail(
|
|
allowCurrentSnapshotFallback: context.allowCurrentSnapshotFallback,
|
|
allowLastKnownLiveFallback: context.expectedGuard?.identity != .unresolved)
|
|
var effectiveEmail = targetEmail
|
|
let imported = await self.importOpenAIDashboardCookiesIfNeeded(targetEmail: targetEmail, force: true)
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus()
|
|
if await self.abortOpenAIDashboardRetryAfterImportFailure(
|
|
importedEmail: imported,
|
|
targetEmail: targetEmail,
|
|
expectedGuard: context.expectedGuard,
|
|
cookieImportStatus: latestCookieImportStatus,
|
|
refreshTaskToken: context.refreshTaskToken)
|
|
{
|
|
return
|
|
}
|
|
if let imported {
|
|
effectiveEmail = imported
|
|
}
|
|
do {
|
|
let dash = try await self.loadLatestOpenAIDashboard(
|
|
accountEmail: effectiveEmail,
|
|
logger: logger,
|
|
allowNavigationTimeoutRetry: context.force,
|
|
timeout: Self.openAIWebRetryDashboardFetchTimeout(afterCookieImport: true))
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
await self.applyOpenAIDashboard(
|
|
dash,
|
|
targetEmail: effectiveEmail,
|
|
expectedGuard: context.expectedGuard,
|
|
refreshTaskToken: context.refreshTaskToken,
|
|
allowCodexUsageBackfill: context.allowCodexUsageBackfill)
|
|
} catch OpenAIDashboardFetcher.FetchError.loginRequired {
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
await self.applyOpenAIDashboardLoginRequiredFailure(
|
|
expectedGuard: context.expectedGuard,
|
|
refreshTaskToken: context.refreshTaskToken,
|
|
routingTargetEmail: targetEmail)
|
|
} catch {
|
|
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
|
|
let message = self.preferredOpenAIDashboardFailureMessage(
|
|
error: error,
|
|
targetEmail: targetEmail,
|
|
cookieImportStatus: latestCookieImportStatus)
|
|
await self.applyOpenAIDashboardFailure(
|
|
message: message,
|
|
expectedGuard: context.expectedGuard,
|
|
refreshTaskToken: context.refreshTaskToken,
|
|
routingTargetEmail: targetEmail)
|
|
}
|
|
}
|
|
|
|
// MARK: - OpenAI web account switching
|
|
|
|
/// Detect Codex account-source changes and clear stale OpenAI web state so the UI can't show the wrong user.
|
|
/// This does not delete other isolated WebKit cookie stores (we keep multiple accounts around).
|
|
func handleOpenAIWebTargetEmailChangeIfNeeded(
|
|
targetEmail: String?,
|
|
targetScope: CookieHeaderCache.Scope? = nil)
|
|
{
|
|
let normalized = targetEmail?
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
.lowercased()
|
|
|
|
guard let normalized, !normalized.isEmpty else { return }
|
|
|
|
let isolationKey = Self.openAIWebTargetIsolationKey(email: normalized, scope: targetScope)
|
|
let previousIsolationKey = self.lastOpenAIDashboardTargetIsolationKey
|
|
self.lastOpenAIDashboardTargetEmail = normalized
|
|
self.lastOpenAIDashboardTargetIsolationKey = isolationKey
|
|
|
|
if let previousIsolationKey,
|
|
previousIsolationKey != isolationKey
|
|
{
|
|
let stamp = Date().formatted(date: .abbreviated, time: .shortened)
|
|
self.logOpenAIWeb(
|
|
"[\(stamp)] Codex account source changed; clearing OpenAI web snapshot")
|
|
self.openAIWebAccountDidChange = true
|
|
self.openAIDashboard = nil
|
|
self.openAIDashboardAttachmentAuthorized = false
|
|
self.lastOpenAIDashboardSnapshot = nil
|
|
self.lastOpenAIDashboardAttachmentAuthorized = false
|
|
self.lastOpenAIDashboardError = nil
|
|
self.lastOpenAIDashboardAttemptAt = nil
|
|
self.openAIDashboardRequiresLogin = true
|
|
self.openAIDashboardCookieImportStatus = L("Codex account changed; importing browser cookies…")
|
|
self.lastOpenAIDashboardCookieImportAttemptAt = nil
|
|
self.lastOpenAIDashboardCookieImportEmail = nil
|
|
}
|
|
}
|
|
|
|
nonisolated static func openAIWebTargetIsolationKey(
|
|
email: String,
|
|
scope: CookieHeaderCache.Scope?) -> String
|
|
{
|
|
"\(email.lowercased())|\(scope?.isolationIdentifier ?? "live")"
|
|
}
|
|
|
|
func importOpenAIDashboardBrowserCookiesNow() async {
|
|
self.resetOpenAIWebDebugLog(context: "manual import")
|
|
let targetEmail = self.currentCodexOpenAIWebTargetEmail(
|
|
allowCurrentSnapshotFallback: true,
|
|
allowLastKnownLiveFallback: false)
|
|
_ = await self.importOpenAIDashboardCookiesIfNeeded(targetEmail: targetEmail, force: true)
|
|
let expectedGuard = self.freshCodexOpenAIWebRefreshGuard()
|
|
await self.refreshOpenAIDashboardIfNeeded(
|
|
force: true,
|
|
expectedGuard: expectedGuard,
|
|
bypassCoalescing: true)
|
|
}
|
|
|
|
func currentCodexOpenAIWebTargetEmail(
|
|
allowCurrentSnapshotFallback: Bool,
|
|
allowLastKnownLiveFallback: Bool) -> String?
|
|
{
|
|
switch self.settings.codexResolvedActiveSource {
|
|
case .liveSystem:
|
|
let liveSystem = self.settings.codexAccountReconciliationSnapshot.liveSystemAccount?.email
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if let liveSystem, !liveSystem.isEmpty {
|
|
self.lastKnownLiveSystemCodexEmail = liveSystem
|
|
return liveSystem
|
|
}
|
|
|
|
if allowCurrentSnapshotFallback,
|
|
let snapshotEmail = self.snapshots[.codex]?.accountEmail(for: .codex)?
|
|
.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
!snapshotEmail.isEmpty
|
|
{
|
|
self.lastKnownLiveSystemCodexEmail = snapshotEmail
|
|
return snapshotEmail
|
|
}
|
|
|
|
if allowLastKnownLiveFallback {
|
|
let lastKnown = self.lastKnownLiveSystemCodexEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if let lastKnown, !lastKnown.isEmpty {
|
|
return lastKnown
|
|
}
|
|
}
|
|
return nil
|
|
case .managedAccount:
|
|
return self.codexAccountEmailForOpenAIDashboard()
|
|
case let .profileHome(path):
|
|
return self.currentProfileCodexRuntimeEmail(path: path)
|
|
}
|
|
}
|
|
|
|
private func openAIDashboardRefreshKey(
|
|
targetEmail: String?,
|
|
expectedGuard: CodexAccountScopedRefreshGuard?) -> String
|
|
{
|
|
let source = String(describing: expectedGuard?.source ?? self.settings.codexResolvedActiveSource)
|
|
let identityKey = Self.codexIdentityGuardKey(expectedGuard?.identity ?? .unresolved) ?? "unresolved"
|
|
let accountKey = Self.normalizeCodexAccountScopedKey(targetEmail) ?? "unknown"
|
|
let authFingerprint = CodexAuthFingerprint.normalize(expectedGuard?.authFingerprint) ?? "nil"
|
|
return "\(source)|\(identityKey)|\(accountKey)|auth:\(authFingerprint)"
|
|
}
|
|
|
|
private func actionableOpenAIDashboardImportFailure(targetEmail: String?) -> String? {
|
|
self.actionableOpenAIDashboardImportFailure(
|
|
targetEmail: targetEmail,
|
|
cookieImportStatus: self.openAIDashboardCookieImportStatus)
|
|
}
|
|
|
|
private func actionableOpenAIDashboardImportFailure(
|
|
targetEmail: String?,
|
|
cookieImportStatus: String?) -> String?
|
|
{
|
|
let status = cookieImportStatus?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard let status, !status.isEmpty else { return nil }
|
|
|
|
if status.localizedCaseInsensitiveContains("openai cookies are for") {
|
|
return "\(status) Switch chatgpt.com account, then refresh OpenAI cookies."
|
|
}
|
|
if status.localizedCaseInsensitiveContains("no signed-in openai web session found")
|
|
|| status.localizedCaseInsensitiveContains("no matching openai web session found")
|
|
{
|
|
let targetLabel = targetEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let accountLabel = (targetLabel?.isEmpty == false) ? targetLabel! : "your OpenAI account"
|
|
return "\(status) Sign in to chatgpt.com as \(accountLabel), then refresh OpenAI cookies."
|
|
}
|
|
if status.localizedCaseInsensitiveContains("openai cookie import failed")
|
|
|| status.localizedCaseInsensitiveContains("browser cookie import failed")
|
|
{
|
|
return status
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func preferredOpenAIDashboardFailureMessage(
|
|
error: Error,
|
|
targetEmail: String?,
|
|
cookieImportStatus: String?) -> String
|
|
{
|
|
if let actionable = self.actionableOpenAIDashboardImportFailure(
|
|
targetEmail: targetEmail,
|
|
cookieImportStatus: cookieImportStatus)
|
|
{
|
|
return actionable
|
|
}
|
|
return error.localizedDescription
|
|
}
|
|
|
|
private static func isOpenAIDashboardTimeout(_ error: Error) -> Bool {
|
|
let nsError = error as NSError
|
|
return nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorTimedOut
|
|
}
|
|
|
|
private func abortOpenAIDashboardRetryAfterImportFailure(
|
|
importedEmail: String?,
|
|
targetEmail: String?,
|
|
expectedGuard: CodexAccountScopedRefreshGuard?,
|
|
cookieImportStatus: String?,
|
|
refreshTaskToken: UUID) async -> Bool
|
|
{
|
|
guard importedEmail == nil,
|
|
let message = self.actionableOpenAIDashboardImportFailure(
|
|
targetEmail: targetEmail,
|
|
cookieImportStatus: cookieImportStatus)
|
|
else {
|
|
return false
|
|
}
|
|
await self.applyOpenAIDashboardFailure(
|
|
message: message,
|
|
expectedGuard: expectedGuard,
|
|
refreshTaskToken: refreshTaskToken,
|
|
routingTargetEmail: targetEmail)
|
|
return true
|
|
}
|
|
|
|
private func shouldApplyOpenAIDashboardRefreshTask(token: UUID?) -> Bool {
|
|
guard let token else { return true }
|
|
return self.openAIDashboardRefreshTaskToken == token
|
|
}
|
|
|
|
private func shouldContinueOpenAIDashboardRefresh(token: UUID?) -> Bool {
|
|
!Task.isCancelled && self.shouldApplyOpenAIDashboardRefreshTask(token: token)
|
|
}
|
|
|
|
func invalidateOpenAIDashboardRefreshTask() {
|
|
self.openAIDashboardBackgroundRefreshTask?.cancel()
|
|
self.openAIDashboardBackgroundRefreshTask = nil
|
|
self.openAIDashboardBackgroundRefreshTaskKey = nil
|
|
self.openAIDashboardRefreshTask?.cancel()
|
|
self.openAIDashboardRefreshTask = nil
|
|
self.openAIDashboardRefreshTaskKey = nil
|
|
self.openAIDashboardRefreshTaskToken = nil
|
|
}
|
|
|
|
private func currentOpenAIDashboardCookieImportStatus() -> String? {
|
|
self.openAIDashboardCookieImportStatus
|
|
}
|
|
|
|
private func loadLatestOpenAIDashboard(
|
|
accountEmail: String?,
|
|
logger: @escaping (String) -> Void,
|
|
allowNavigationTimeoutRetry: Bool,
|
|
timeout: TimeInterval) async throws -> OpenAIDashboardSnapshot
|
|
{
|
|
if let override = self._test_openAIDashboardLoaderOverride {
|
|
return try await override(accountEmail, logger, allowNavigationTimeoutRetry, timeout)
|
|
}
|
|
return try await OpenAIDashboardFetcher().loadLatestDashboard(
|
|
accountEmail: accountEmail,
|
|
cacheScope: self.codexCookieCacheScopeForOpenAIWeb(),
|
|
logger: logger,
|
|
debugDumpHTML: timeout != Self.openAIWebPrimaryFetchTimeout,
|
|
allowNavigationTimeoutRetry: allowNavigationTimeoutRetry,
|
|
timeout: timeout)
|
|
}
|
|
|
|
private func failClosedForUnreadableManagedCodexStore() async -> String? {
|
|
self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
|
|
self.openAIDashboardRequiresLogin = true
|
|
self.openAIDashboardCookieImportStatus = [
|
|
L("Managed Codex account data is unavailable."),
|
|
L("Fix the managed account store before importing OpenAI cookies."),
|
|
].joined(separator: " ")
|
|
return nil
|
|
}
|
|
|
|
private func failClosedRefreshForUnreadableManagedCodexStore() async {
|
|
self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
|
|
self.openAIDashboardRequiresLogin = true
|
|
self.lastOpenAIDashboardError = [
|
|
L("Managed Codex account data is unavailable."),
|
|
L("Fix the managed account store before refreshing OpenAI web data."),
|
|
].joined(separator: " ")
|
|
}
|
|
|
|
private func failClosedForMissingManagedCodexTarget() async -> String? {
|
|
self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
|
|
self.openAIDashboardRequiresLogin = true
|
|
self.openAIDashboardCookieImportStatus = [
|
|
L("The selected managed Codex account is unavailable."),
|
|
L("Pick another Codex account before importing OpenAI cookies."),
|
|
].joined(separator: " ")
|
|
return nil
|
|
}
|
|
|
|
private func failClosedRefreshForMissingManagedCodexTarget() async {
|
|
self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
|
|
self.openAIDashboardRequiresLogin = true
|
|
self.lastOpenAIDashboardError = [
|
|
L("The selected managed Codex account is unavailable."),
|
|
L("Pick another Codex account before refreshing OpenAI web data."),
|
|
].joined(separator: " ")
|
|
}
|
|
|
|
private func failClosedForMissingProfileCodexTarget() async -> String? {
|
|
self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
|
|
self.openAIDashboardRequiresLogin = true
|
|
self.openAIDashboardCookieImportStatus = [
|
|
L("The selected Codex profile has no verified account email."),
|
|
L("Refresh the profile before importing OpenAI cookies."),
|
|
].joined(separator: " ")
|
|
return nil
|
|
}
|
|
|
|
private func failClosedRefreshForMissingProfileCodexTarget() async {
|
|
self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
|
|
self.openAIDashboardRequiresLogin = true
|
|
self.lastOpenAIDashboardError = [
|
|
L("The selected Codex profile has no verified account email."),
|
|
L("Refresh the profile before refreshing OpenAI web data."),
|
|
].joined(separator: " ")
|
|
}
|
|
|
|
private func openAIWebCookieImportShouldFailClosed() async -> Bool {
|
|
if self.openAIWebManagedTargetStoreIsUnreadable() {
|
|
_ = await self.failClosedForUnreadableManagedCodexStore()
|
|
return true
|
|
}
|
|
if self.openAIWebManagedTargetIsMissing() {
|
|
_ = await self.failClosedForMissingManagedCodexTarget()
|
|
return true
|
|
}
|
|
if self.openAIWebProfileTargetEmailIsMissing() {
|
|
_ = await self.failClosedForMissingProfileCodexTarget()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
private func openAIDashboardCookieImportResult(
|
|
request: OpenAIDashboardCookieImportRequest,
|
|
logger: @escaping (String) -> Void) async throws -> OpenAIDashboardBrowserCookieImporter.ImportResult
|
|
{
|
|
if let override = self._test_openAIDashboardCookieImportOverride {
|
|
return try await override(
|
|
request.normalizedTarget,
|
|
request.allowAnyAccount,
|
|
request.cookieSource,
|
|
request.cacheScope,
|
|
logger)
|
|
}
|
|
|
|
let importer = OpenAIDashboardBrowserCookieImporter(browserDetection: self.browserDetection)
|
|
switch request.cookieSource {
|
|
case .manual:
|
|
self.settings.ensureCodexCookieLoaded()
|
|
// Manual OpenAI cookies still come from one provider-level setting. Auto-imported cookies are
|
|
// isolated per managed account, but a manual header is an explicit override owned by settings,
|
|
// so switching managed accounts does not currently swap it underneath the user.
|
|
let manualHeader = self.settings.codexCookieHeader
|
|
guard CookieHeaderNormalizer.normalize(manualHeader) != nil else {
|
|
throw OpenAIDashboardBrowserCookieImporter.ImportError.manualCookieHeaderInvalid
|
|
}
|
|
return try await importer.importManualCookies(
|
|
cookieHeader: manualHeader,
|
|
intoAccountEmail: request.normalizedTarget,
|
|
allowAnyAccount: request.allowAnyAccount,
|
|
cacheScope: request.cacheScope,
|
|
logger: logger)
|
|
case .auto:
|
|
return try await importer.importBestCookies(
|
|
intoAccountEmail: request.normalizedTarget,
|
|
allowAnyAccount: request.allowAnyAccount,
|
|
preferCachedCookieHeader: request.preferCachedCookieHeader ?? !request.force,
|
|
cacheScope: request.cacheScope,
|
|
logger: logger)
|
|
case .off:
|
|
return OpenAIDashboardBrowserCookieImporter.ImportResult(
|
|
sourceLabel: "Off",
|
|
cookieCount: 0,
|
|
signedInEmail: request.normalizedTarget,
|
|
matchesCodexEmail: true)
|
|
}
|
|
}
|
|
|
|
func importOpenAIDashboardCookiesIfNeeded(
|
|
targetEmail: String?,
|
|
force: Bool,
|
|
preferCachedCookieHeader: Bool? = nil) async -> String?
|
|
{
|
|
guard !Task.isCancelled else { return nil }
|
|
if await self.openAIWebCookieImportShouldFailClosed() {
|
|
return nil
|
|
}
|
|
guard !Task.isCancelled else { return nil }
|
|
|
|
let normalizedTarget = targetEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let allowAnyAccount = normalizedTarget == nil || normalizedTarget?.isEmpty == true
|
|
let cookieSource = self.settings.codexCookieSource
|
|
let cacheScope = self.codexCookieCacheScopeForOpenAIWeb()
|
|
|
|
let now = Date()
|
|
let lastEmail = self.lastOpenAIDashboardCookieImportEmail
|
|
let lastAttempt = self.lastOpenAIDashboardCookieImportAttemptAt ?? .distantPast
|
|
|
|
let shouldAttempt: Bool = if force {
|
|
true
|
|
} else {
|
|
if allowAnyAccount {
|
|
now.timeIntervalSince(lastAttempt) > 300
|
|
} else {
|
|
self.openAIDashboardRequiresLogin &&
|
|
(
|
|
lastEmail?.lowercased() != normalizedTarget?.lowercased() || now
|
|
.timeIntervalSince(lastAttempt) > 300)
|
|
}
|
|
}
|
|
|
|
guard shouldAttempt else { return normalizedTarget }
|
|
self.lastOpenAIDashboardCookieImportEmail = normalizedTarget
|
|
self.lastOpenAIDashboardCookieImportAttemptAt = now
|
|
|
|
let stamp = now.formatted(date: .abbreviated, time: .shortened)
|
|
let targetLabel = normalizedTarget ?? "unknown"
|
|
self.logOpenAIWeb("[\(stamp)] import start (target=\(targetLabel))")
|
|
|
|
do {
|
|
let log: (String) -> Void = { [weak self] message in
|
|
guard let self else { return }
|
|
self.logOpenAIWeb(message)
|
|
}
|
|
|
|
let request = OpenAIDashboardCookieImportRequest(
|
|
normalizedTarget: normalizedTarget,
|
|
allowAnyAccount: allowAnyAccount,
|
|
cookieSource: cookieSource,
|
|
cacheScope: cacheScope,
|
|
preferCachedCookieHeader: preferCachedCookieHeader,
|
|
force: force)
|
|
let result = try await self.openAIDashboardCookieImportResult(
|
|
request: request,
|
|
logger: log)
|
|
guard !Task.isCancelled else { return nil }
|
|
let effectiveEmail = result.signedInEmail?
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
.isEmpty == false
|
|
? result.signedInEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
: normalizedTarget
|
|
self.lastOpenAIDashboardCookieImportEmail = effectiveEmail ?? normalizedTarget
|
|
await MainActor.run {
|
|
let signed = result.signedInEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let matchText = result.matchesCodexEmail ? "matches Codex" : "does not match Codex"
|
|
let sourceLabel = switch cookieSource {
|
|
case .manual:
|
|
"Manual cookie header"
|
|
case .auto:
|
|
"\(result.sourceLabel) cookies"
|
|
case .off:
|
|
"OpenAI cookies disabled"
|
|
}
|
|
if let signed, !signed.isEmpty {
|
|
self.openAIDashboardCookieImportStatus =
|
|
allowAnyAccount
|
|
? [
|
|
"Using \(sourceLabel) (\(result.cookieCount)).",
|
|
"Signed in as \(signed).",
|
|
].joined(separator: " ")
|
|
: [
|
|
"Using \(sourceLabel) (\(result.cookieCount)).",
|
|
"Signed in as \(signed) (\(matchText)).",
|
|
].joined(separator: " ")
|
|
} else {
|
|
self.openAIDashboardCookieImportStatus =
|
|
"Using \(sourceLabel) (\(result.cookieCount))."
|
|
}
|
|
}
|
|
return effectiveEmail
|
|
} catch let err as OpenAIDashboardBrowserCookieImporter.ImportError {
|
|
guard !Task.isCancelled else { return nil }
|
|
switch err {
|
|
case let .noMatchingAccount(found):
|
|
let foundText: String = if found.isEmpty {
|
|
"no signed-in session detected in \(self.codexBrowserCookieOrder.loginHint)"
|
|
} else {
|
|
found
|
|
.sorted { lhs, rhs in
|
|
if lhs.sourceLabel == rhs.sourceLabel {
|
|
return lhs.email < rhs.email
|
|
}
|
|
return lhs.sourceLabel < rhs.sourceLabel
|
|
}
|
|
.map { "\($0.sourceLabel): \($0.email)" }
|
|
.joined(separator: " • ")
|
|
}
|
|
self.logOpenAIWeb("[\(stamp)] import mismatch: \(foundText)")
|
|
await MainActor.run {
|
|
self.openAIDashboardCookieImportStatus = allowAnyAccount
|
|
? [
|
|
"No signed-in OpenAI web session found.",
|
|
"Found \(foundText).",
|
|
].joined(separator: " ")
|
|
: Self.conciseOpenAICookieMismatchStatus(
|
|
found: found.map(\.email),
|
|
targetEmail: normalizedTarget)
|
|
self.failClosedOpenAIDashboardSnapshot()
|
|
}
|
|
case .noCookiesFound,
|
|
.browserAccessDenied,
|
|
.browserCookieLoadTimedOut,
|
|
.dashboardStillRequiresLogin,
|
|
.manualCookieHeaderInvalid:
|
|
self.logOpenAIWeb("[\(stamp)] import failed: \(err.localizedDescription)")
|
|
await MainActor.run {
|
|
self.openAIDashboardCookieImportStatus =
|
|
"OpenAI cookie import failed: \(err.localizedDescription)"
|
|
self.openAIDashboardRequiresLogin = true
|
|
}
|
|
}
|
|
} catch {
|
|
guard !Task.isCancelled else { return nil }
|
|
self.logOpenAIWeb("[\(stamp)] import failed: \(error.localizedDescription)")
|
|
await MainActor.run {
|
|
self.openAIDashboardCookieImportStatus =
|
|
"Browser cookie import failed: \(error.localizedDescription)"
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func resetOpenAIWebDebugLog(context: String) {
|
|
let stamp = Date().formatted(date: .abbreviated, time: .shortened)
|
|
self.openAIWebDebugLines.removeAll(keepingCapacity: true)
|
|
self.openAIDashboardCookieImportDebugLog = nil
|
|
self.logOpenAIWeb("[\(stamp)] OpenAI web \(context) start")
|
|
}
|
|
|
|
private func logOpenAIWeb(_ message: String) {
|
|
let safeMessage = LogRedactor.redact(message)
|
|
self.openAIWebLogger.debug(safeMessage)
|
|
self.openAIWebDebugLines.append(safeMessage)
|
|
if self.openAIWebDebugLines.count > 240 {
|
|
self.openAIWebDebugLines.removeFirst(self.openAIWebDebugLines.count - 240)
|
|
}
|
|
self.openAIDashboardCookieImportDebugLog = self.openAIWebDebugLines.joined(separator: "\n")
|
|
}
|
|
|
|
func resetOpenAIWebState() {
|
|
self.invalidateOpenAIDashboardRefreshTask()
|
|
OpenAIDashboardFetcher.evictAllCachedWebViews()
|
|
self.openAIDashboard = nil
|
|
self.openAIDashboardAttachmentAuthorized = false
|
|
self.lastOpenAIDashboardError = nil
|
|
self.lastOpenAIDashboardSnapshot = nil
|
|
self.lastOpenAIDashboardAttachmentAuthorized = false
|
|
self.lastOpenAIDashboardTargetEmail = nil
|
|
self.lastOpenAIDashboardTargetIsolationKey = nil
|
|
self.lastOpenAIDashboardAttemptAt = nil
|
|
self.openAIDashboardRequiresLogin = false
|
|
self.openAIDashboardCookieImportStatus = nil
|
|
self.openAIDashboardCookieImportDebugLog = nil
|
|
self.lastOpenAIDashboardCookieImportAttemptAt = nil
|
|
self.lastOpenAIDashboardCookieImportEmail = nil
|
|
self.lastKnownLiveSystemCodexEmail = nil
|
|
}
|
|
|
|
/// Routing-only optimization: this detects whether the fetched browser session appears to be for a
|
|
/// different account than the route target, so we can retry after cookie import. Ownership proof
|
|
/// happens exclusively through CodexDashboardAuthority.
|
|
private func dashboardEmailMismatch(expected: String?, actual: String?) -> Bool {
|
|
guard let expected, !expected.isEmpty else { return false }
|
|
guard let raw = actual?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { return false }
|
|
return raw.lowercased() != expected.lowercased()
|
|
}
|
|
|
|
private func openAIWebManagedTargetStoreIsUnreadable() -> Bool {
|
|
guard case .managedAccount = self.settings.codexResolvedActiveSource else {
|
|
return false
|
|
}
|
|
return self.settings.codexSettingsSnapshot(tokenOverride: nil).managedAccountStoreUnreadable
|
|
}
|
|
|
|
private func openAIWebManagedTargetIsMissing() -> Bool {
|
|
guard case .managedAccount = self.settings.codexResolvedActiveSource else {
|
|
return false
|
|
}
|
|
return self.selectedManagedCodexAccountForOpenAIWeb() == nil
|
|
}
|
|
|
|
private func openAIWebProfileTargetEmailIsMissing() -> Bool {
|
|
guard case let .profileHome(path) = self.settings.codexResolvedActiveSource else {
|
|
return false
|
|
}
|
|
return self.currentProfileCodexRuntimeEmail(path: path) == nil
|
|
}
|
|
|
|
private func selectedManagedCodexAccountForOpenAIWeb() -> ManagedCodexAccount? {
|
|
guard case let .managedAccount(id) = self.settings.codexResolvedActiveSource else {
|
|
return nil
|
|
}
|
|
|
|
let snapshot = self.settings.codexAccountReconciliationSnapshot
|
|
return snapshot.storedAccounts.first { $0.id == id }
|
|
}
|
|
|
|
func codexAccountEmailForOpenAIDashboard(allowLastKnownLiveFallback: Bool = true) -> String? {
|
|
switch self.settings.codexResolvedActiveSource {
|
|
case .liveSystem:
|
|
let liveSystem = self.settings.codexAccountReconciliationSnapshot.liveSystemAccount?.email
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if let liveSystem, !liveSystem.isEmpty {
|
|
self.lastKnownLiveSystemCodexEmail = liveSystem
|
|
return liveSystem
|
|
}
|
|
|
|
guard allowLastKnownLiveFallback else { return nil }
|
|
let lastKnown = self.lastKnownLiveSystemCodexEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if let lastKnown, !lastKnown.isEmpty {
|
|
return lastKnown
|
|
}
|
|
return nil
|
|
case .managedAccount:
|
|
if self.openAIWebManagedTargetStoreIsUnreadable() {
|
|
return nil
|
|
}
|
|
|
|
let managed = self.currentManagedCodexRuntimeEmail()?
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if let managed, !managed.isEmpty {
|
|
return managed
|
|
}
|
|
return nil
|
|
case let .profileHome(path):
|
|
let profile = self.currentProfileCodexRuntimeEmail(path: path)?
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if let profile, !profile.isEmpty {
|
|
return profile
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func codexCookieCacheScopeForOpenAIWeb() -> CookieHeaderCache.Scope? {
|
|
switch self.settings.codexResolvedActiveSource {
|
|
case .liveSystem:
|
|
nil
|
|
case let .managedAccount(id):
|
|
self.openAIWebManagedTargetStoreIsUnreadable() ? .managedStoreUnreadable : .managedAccount(id)
|
|
case let .profileHome(path):
|
|
.profileHome(path)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - OpenAI web error messaging
|
|
|
|
extension UsageStore {
|
|
nonisolated static func shouldRunOpenAIWebRefresh(_ context: OpenAIWebRefreshPolicyContext) -> Bool {
|
|
guard context.accessEnabled else { return false }
|
|
guard context.force || context.refreshPhase != .startup else { return false }
|
|
return context.force || !context.batterySaverEnabled
|
|
}
|
|
|
|
nonisolated static func forceOpenAIWebRefreshForStaleRequest(batterySaverEnabled: Bool) -> Bool {
|
|
!batterySaverEnabled
|
|
}
|
|
|
|
nonisolated static func shouldSkipOpenAIWebRefresh(_ context: OpenAIWebRefreshGateContext) -> Bool {
|
|
if context.force || context.accountDidChange {
|
|
return false
|
|
}
|
|
if let lastAttemptAt = context.lastAttemptAt,
|
|
context.now.timeIntervalSince(lastAttemptAt) < context.refreshInterval
|
|
{
|
|
return true
|
|
}
|
|
if context.lastError == nil,
|
|
let lastSnapshotAt = context.lastSnapshotAt,
|
|
context.now.timeIntervalSince(lastSnapshotAt) < context.refreshInterval
|
|
{
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
nonisolated static func shouldSkipOpenAIWebEmptyHistoryRetry(_ context: OpenAIWebRefreshGateContext) -> Bool {
|
|
if context.force || context.accountDidChange {
|
|
return false
|
|
}
|
|
guard let lastAttemptAt = context.lastAttemptAt,
|
|
context.now.timeIntervalSince(lastAttemptAt) < context.refreshInterval
|
|
else { return false }
|
|
guard let lastSnapshotAt = context.lastSnapshotAt else { return true }
|
|
return lastAttemptAt >= lastSnapshotAt
|
|
}
|
|
|
|
func syncOpenAIWebState() {
|
|
guard self.isEnabled(.codex),
|
|
self.settings.openAIWebAccessEnabled,
|
|
self.settings.codexCookieSource.isEnabled
|
|
else {
|
|
self.resetOpenAIWebState()
|
|
return
|
|
}
|
|
|
|
let targetEmail = self.currentCodexOpenAIWebTargetEmail(
|
|
allowCurrentSnapshotFallback: true,
|
|
allowLastKnownLiveFallback: true)
|
|
self.handleOpenAIWebTargetEmailChangeIfNeeded(
|
|
targetEmail: targetEmail,
|
|
targetScope: self.codexCookieCacheScopeForOpenAIWeb())
|
|
}
|
|
|
|
func openAIDashboardFriendlyError(
|
|
body: String,
|
|
targetEmail: String?,
|
|
cookieImportStatus: String?) -> String?
|
|
{
|
|
let trimmed = body.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let status = cookieImportStatus?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if trimmed.isEmpty {
|
|
return [
|
|
"OpenAI web dashboard returned an empty page.",
|
|
"Sign in to chatgpt.com and update OpenAI cookies in Providers → Codex.",
|
|
].joined(separator: " ")
|
|
}
|
|
|
|
let lower = trimmed.lowercased()
|
|
let looksLikePublicLanding = lower.contains("skip to content")
|
|
&& (lower.contains("about") || lower.contains("openai") || lower.contains("chatgpt"))
|
|
let looksLoggedOut = lower.contains("sign in")
|
|
|| lower.contains("log in")
|
|
|| lower.contains("create account")
|
|
|| lower.contains("continue with google")
|
|
|| lower.contains("continue with apple")
|
|
|| lower.contains("continue with microsoft")
|
|
|
|
guard looksLikePublicLanding || looksLoggedOut else { return nil }
|
|
let emailLabel = targetEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let targetLabel = (emailLabel?.isEmpty == false) ? emailLabel! : "your OpenAI account"
|
|
if let status, !status.isEmpty {
|
|
if status.contains("cookies do not match Codex account")
|
|
|| status.localizedCaseInsensitiveContains("openai cookies are for")
|
|
|| status.localizedCaseInsensitiveContains("cookie import failed")
|
|
{
|
|
return "\(status) Switch chatgpt.com account, then refresh OpenAI cookies."
|
|
}
|
|
}
|
|
return [
|
|
"OpenAI web dashboard returned a public page (not signed in).",
|
|
"Sign in to chatgpt.com as \(targetLabel), then update OpenAI cookies in Providers → Codex.",
|
|
].joined(separator: " ")
|
|
}
|
|
|
|
private static func conciseOpenAICookieMismatchStatus(
|
|
found: [String],
|
|
targetEmail: String?)
|
|
-> String
|
|
{
|
|
let normalizedFound = Array(Set(
|
|
found
|
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() }
|
|
.filter { !$0.isEmpty }))
|
|
.sorted()
|
|
|
|
let foundLabel: String = switch normalizedFound.count {
|
|
case 0:
|
|
""
|
|
case 1:
|
|
normalizedFound[0]
|
|
case 2:
|
|
"\(normalizedFound[0]) or \(normalizedFound[1])"
|
|
default:
|
|
"\(normalizedFound[0]) or \(normalizedFound.count - 1) other accounts"
|
|
}
|
|
|
|
let targetLabel = targetEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if normalizedFound.isEmpty {
|
|
guard let targetLabel, !targetLabel.isEmpty else {
|
|
return L("No matching OpenAI web session found.")
|
|
}
|
|
return L("No matching OpenAI web session found for %@.", targetLabel)
|
|
}
|
|
guard let targetLabel, !targetLabel.isEmpty else {
|
|
return L("OpenAI cookies are for %@.", foundLabel)
|
|
}
|
|
return L("OpenAI cookies are for %1$@, not %2$@.", foundLabel, targetLabel)
|
|
}
|
|
}
|