Files
steipete--codexbar/Sources/CodexBar/UsageStore+ClaudeDebug.swift
T
2026-07-13 12:22:33 +08:00

175 lines
8.0 KiB
Swift

import CodexBarCore
import Foundation
import SweetCookieKit
@MainActor
extension UsageStore {
func debugClaudeDump() async -> String {
await ClaudeStatusProbe.latestDumps()
}
}
extension UsageStore {
struct ClaudeDebugLogConfiguration {
let runtime: CodexBarCore.ProviderRuntime
let sourceMode: ProviderSourceMode
let environment: [String: String]
let webExtrasEnabled: Bool
let usageDataSource: ClaudeUsageDataSource
let cookieSource: ProviderCookieSource
let cookieHeader: String
let keepCLISessionsAlive: Bool
}
static func debugClaudeLog(
browserDetection: BrowserDetection,
configuration: ClaudeDebugLogConfiguration) async -> String
{
struct OAuthDebugProbe: Sendable {
let hasCredentials: Bool
let ownerRawValue: String
let sourceRawValue: String
let isExpired: Bool
}
return await runWithTimeout(seconds: 15) {
var lines: [String] = []
let manualHeader = configuration.cookieSource == .manual
? CookieHeaderNormalizer.normalize(configuration.cookieHeader)
: nil
let hasKey = if configuration.cookieSource == .off {
false
} else if let manualHeader {
ClaudeWebAPIFetcher.hasSessionKey(cookieHeader: manualHeader)
} else {
ClaudeWebAPIFetcher.hasSessionKey(browserDetection: browserDetection) { msg in lines.append(msg) }
}
let oauthProbe = await withTaskGroup(of: OAuthDebugProbe.self) { group in
// Preserve task-local test overrides while keeping the keychain read off the calling task.
group.addTask(priority: .utility) {
let oauthRecord = try? ClaudeOAuthCredentialsStore.loadRecord(
environment: configuration.environment,
allowKeychainPrompt: false,
respectKeychainPromptCooldown: true,
allowClaudeKeychainRepairWithoutPrompt: false)
return OAuthDebugProbe(
hasCredentials: oauthRecord?.credentials.scopes.contains("user:profile") == true,
ownerRawValue: oauthRecord?.owner.rawValue ?? "none",
sourceRawValue: oauthRecord?.source.rawValue ?? "none",
isExpired: oauthRecord?.credentials.isExpired ?? false)
}
return await group.next() ?? OAuthDebugProbe(
hasCredentials: false,
ownerRawValue: "none",
sourceRawValue: "none",
isExpired: false)
}
let hasOAuthCredentials = ClaudeOAuthPlanningAvailability.isAvailable(
runtime: configuration.runtime,
sourceMode: configuration.sourceMode,
environment: configuration.environment)
let hasClaudeBinary = ClaudeCLIResolver.isAvailable(environment: configuration.environment)
let delegatedCooldownSeconds = ClaudeOAuthDelegatedRefreshCoordinator.cooldownRemainingSeconds()
let planningInput = ClaudeSourcePlanningInput(
runtime: configuration.runtime,
selectedDataSource: configuration.usageDataSource,
webExtrasEnabled: configuration.webExtrasEnabled,
hasWebSession: hasKey,
hasCLI: hasClaudeBinary,
hasOAuthCredentials: hasOAuthCredentials)
let plan = ClaudeSourcePlanner.resolve(input: planningInput)
let strategy = plan.compatibilityStrategy
lines.append(contentsOf: plan.debugLines())
lines.append("hasSessionKey=\(hasKey)")
lines.append("hasOAuthCredentials=\(hasOAuthCredentials)")
lines.append("oauthCredentialOwner=\(oauthProbe.ownerRawValue)")
lines.append("oauthCredentialSource=\(oauthProbe.sourceRawValue)")
lines.append("oauthCredentialExpired=\(oauthProbe.isExpired)")
lines.append("delegatedRefreshCLIAvailable=\(hasClaudeBinary)")
lines.append("delegatedRefreshCooldownActive=\(delegatedCooldownSeconds != nil)")
if let delegatedCooldownSeconds {
lines.append("delegatedRefreshCooldownSeconds=\(delegatedCooldownSeconds)")
}
lines.append("hasClaudeBinary=\(hasClaudeBinary)")
if strategy?.useWebExtras == true {
lines.append("web_extras=enabled")
}
lines.append("")
guard let strategy else {
lines.append("No planner-selected Claude source.")
return lines.joined(separator: "\n")
}
switch strategy.dataSource {
case .auto:
lines.append("Auto source selected.")
return lines.joined(separator: "\n")
case .api:
let hasAdminKey = ProviderTokenResolver.claudeAdminAPIToken(
environment: configuration.environment) != nil
lines.append("Admin API source selected.")
lines.append("hasAdminAPIKey=\(hasAdminKey)")
return lines.joined(separator: "\n")
case .web:
do {
let web: ClaudeWebAPIFetcher.WebUsageData =
if let manualHeader {
try await ClaudeWebAPIFetcher.fetchUsage(cookieHeader: manualHeader) { msg in
lines.append(msg)
}
} else {
try await ClaudeWebAPIFetcher.fetchUsage(browserDetection: browserDetection) { msg in
lines.append(msg)
}
}
lines.append("")
lines.append("Web API summary:")
let sessionReset = web.sessionResetsAt?.description ?? "nil"
lines.append("session_used=\(web.sessionPercentUsed)% resetsAt=\(sessionReset)")
if let weekly = web.weeklyPercentUsed {
let weeklyReset = web.weeklyResetsAt?.description ?? "nil"
lines.append("weekly_used=\(weekly)% resetsAt=\(weeklyReset)")
} else {
lines.append("weekly_used=nil")
}
lines.append("opus_used=\(web.opusPercentUsed?.description ?? "nil")")
if let extra = web.extraUsageCost {
let resetsAt = extra.resetsAt?.description ?? "nil"
let period = extra.period ?? "nil"
let line =
"extra_usage used=\(extra.used) limit=\(extra.limit) " +
"currency=\(extra.currencyCode) period=\(period) resetsAt=\(resetsAt)"
lines.append(line)
} else {
lines.append("extra_usage=nil")
}
return lines.joined(separator: "\n")
} catch {
lines.append("Web API failed: \(error.localizedDescription)")
return lines.joined(separator: "\n")
}
case .cli:
let fetcher = ClaudeUsageFetcher(
browserDetection: browserDetection,
environment: configuration.environment,
runtime: configuration.runtime,
dataSource: configuration.usageDataSource,
keepCLISessionsAlive: configuration.keepCLISessionsAlive)
let cli = await fetcher.debugRawProbe(model: "sonnet")
lines.append(cli)
return lines.joined(separator: "\n")
case .oauth:
lines.append("OAuth source selected.")
return lines.joined(separator: "\n")
}
}
}
}