Files
2026-07-13 12:22:33 +08:00

342 lines
13 KiB
Swift

import CodexBarCore
import Commander
import Foundation
extension CodexBarCLI {
static func runDiagnose(_ values: ParsedValues) async {
let output = CLIOutputPreferences.from(values: values)
let config = Self.loadConfig(output: output)
let format = Self.decodeFormat(from: values)
guard format == .json else {
Self.exit(
code: .failure,
message: "Error: only JSON format is supported for diagnose",
output: output,
kind: .args)
}
let providerSelection: ProviderSelection
if let rawProvider = values.options["provider"]?.last {
guard let parsed = ProviderSelection(argument: rawProvider) else {
Self.exit(
code: .failure,
message: "Error: unknown provider '\(rawProvider)'",
output: output,
kind: .args)
}
providerSelection = parsed
} else {
providerSelection = Self.providerSelection(rawOverride: nil, enabled: config.enabledProviders())
}
let providers = providerSelection.asList
let pretty = values.flags.contains("pretty")
let verbose = values.flags.contains("verbose")
let outputPath = values.options["output"]?.last
let browserDetection = BrowserDetection()
let baseFetcher = UsageFetcher()
let tokenSelection = TokenAccountCLISelection(label: nil, index: nil, allAccounts: false)
let tokenContext: TokenAccountCLIContext
do {
tokenContext = try TokenAccountCLIContext(
selection: tokenSelection,
config: config,
verbose: verbose)
} catch {
Self.exit(code: .failure, message: "Error: \(error.localizedDescription)", output: output, kind: .config)
}
var diagnostics: [ProviderDiagnosticExport] = []
diagnostics.reserveCapacity(providers.count)
for provider in providers {
await diagnostics.append(Self.makeDiagnosticExport(
provider: provider,
tokenContext: tokenContext,
baseFetcher: baseFetcher,
browserDetection: browserDetection,
verbose: verbose))
}
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = pretty ? [.prettyPrinted, .sortedKeys] : .sortedKeys
do {
let data: Data = if diagnostics.count == 1, let diagnostic = diagnostics.first {
try encoder.encode(diagnostic)
} else {
try encoder.encode(ProviderDiagnosticBatchExport(
timestamp: Date(),
diagnostics: diagnostics))
}
var jsonString = String(data: data, encoding: .utf8) ?? "{}"
jsonString = LogRedactor.redact(jsonString)
if let outputPath, !outputPath.isEmpty {
try Self.writeDiagnosticExport(jsonString, to: outputPath)
} else {
print(jsonString)
}
} catch {
Self.exit(
code: .failure,
message: "Error encoding diagnostic: \(error.localizedDescription)",
output: output,
kind: .runtime)
}
Self.exit(code: .success, output: output, kind: .runtime)
}
static func writeDiagnosticExport(_ jsonString: String, to path: String) throws {
let url = URL(fileURLWithPath: path)
let parent = url.deletingLastPathComponent()
if !parent.path.isEmpty {
try FileManager.default.createDirectory(
at: parent,
withIntermediateDirectories: true)
}
try jsonString.write(to: url, atomically: true, encoding: .utf8)
}
}
extension CodexBarCLI {
private static func makeDiagnosticExport(
provider: UsageProvider,
tokenContext: TokenAccountCLIContext,
baseFetcher: UsageFetcher,
browserDetection: BrowserDetection,
verbose: Bool) async -> ProviderDiagnosticExport
{
let account = ((try? tokenContext.resolvedAccounts(for: provider)) ?? []).first
let env = tokenContext.environment(
base: ProcessInfo.processInfo.environment,
provider: provider,
account: account,
codexActiveSourceOverride: nil)
let settings = tokenContext.settingsSnapshot(
for: provider,
account: account,
codexActiveSourceOverride: nil)
let preferredSourceMode = tokenContext.preferredSourceMode(for: provider)
let sourceMode = tokenContext.effectiveSourceMode(
base: preferredSourceMode,
provider: provider,
account: account)
let fetcher = tokenContext.fetcher(base: baseFetcher, provider: provider, env: env)
let fetchContext = ProviderFetchContext(
runtime: .cli,
sourceMode: sourceMode,
includeCredits: true,
includeOptionalUsage: true,
webTimeout: 60,
webDebugDumpHTML: false,
verbose: verbose,
env: env,
settings: settings,
fetcher: fetcher,
claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),
browserDetection: browserDetection,
selectedTokenAccountID: account?.id,
tokenAccountTokenUpdater: tokenContext.tokenUpdater(for: account),
providerManualTokenUpdater: tokenContext.manualTokenUpdater())
let descriptor = ProviderDescriptorRegistry.descriptor(for: provider)
let outcome = await Self.fetchProviderUsage(provider: provider, context: fetchContext)
return ProviderDiagnosticExportBuilder.build(.init(
provider: provider,
descriptor: descriptor,
outcome: outcome,
sourceMode: sourceMode,
settings: settings,
auth: Self.diagnosticAuthSummary(
provider: provider,
account: account,
config: tokenContext.config.providerConfig(for: provider),
environment: env,
settings: settings),
appVersion: Self.currentVersion()))
}
static func diagnosticAuthSummary(
provider: UsageProvider,
account: ProviderTokenAccount?,
config: ProviderConfig?,
environment: [String: String],
settings: ProviderSettingsSnapshot?) -> ProviderDiagnosticAuthSummary
{
if provider == .minimax {
let authMode = self.resolveMiniMaxAuthMode(environment: environment, settings: settings)
return ProviderDiagnosticAuthSummary(
configured: authMode.usesAPIToken || authMode.usesCookie,
modes: authMode == .none ? [] : [authMode.description])
}
var modes: [String] = []
if account != nil {
modes.append("tokenAccount")
}
let hasConfigAPIAuth = if provider == .bedrock {
config?.sanitizedAPIKey != nil && config?.sanitizedSecretKey != nil
} else {
config?.sanitizedAPIKey != nil || config?.sanitizedSecretKey != nil
}
if hasConfigAPIAuth {
modes.append("api")
}
if Self.environmentAPIAuthConfigured(provider: provider, environment: environment), !modes.contains("api") {
modes.append("api")
}
if config?.sanitizedCookieHeader != nil {
modes.append("web")
}
if Self.environmentWebAuthConfigured(provider: provider, environment: environment), !modes.contains("web") {
modes.append("web")
}
return ProviderDiagnosticAuthSummary(
configured: !modes.isEmpty,
modes: modes)
}
private static func environmentAPIAuthConfigured(
provider: UsageProvider,
environment: [String: String]) -> Bool
{
self.environmentCoreAPIAuthConfigured(provider: provider, environment: environment) ||
self.environmentExtendedAPIAuthConfigured(provider: provider, environment: environment)
}
private static func environmentCoreAPIAuthConfigured(
provider: UsageProvider,
environment: [String: String]) -> Bool
{
switch provider {
case .alibaba:
AlibabaCodingPlanSettingsReader.apiToken(environment: environment) != nil
case .azureopenai:
AzureOpenAISettingsReader.apiKey(environment: environment) != nil
case .bedrock:
BedrockSettingsReader.hasCredentials(environment: environment)
case .claude:
ClaudeAdminAPISettingsReader.apiKey(environment: environment) != nil
case .codebuff:
CodebuffSettingsReader.apiKey(environment: environment) != nil
case .chutes:
ChutesSettingsReader.apiKey(environment: environment) != nil
case .crof:
CrofSettingsReader.apiKey(environment: environment) != nil
case .crossmodel:
CrossModelSettingsReader.apiToken(environment: environment) != nil
case .deepgram:
DeepgramSettingsReader.apiKey(environment: environment) != nil
case .deepseek:
DeepSeekSettingsReader.apiKey(environment: environment) != nil
case .doubao:
DoubaoSettingsReader.apiKey(environment: environment) != nil
case .elevenlabs:
ElevenLabsSettingsReader.apiKey(environment: environment) != nil
case .groq:
GroqSettingsReader.apiKey(environment: environment) != nil
case .kilo:
KiloSettingsReader.apiKey(environment: environment) != nil
case .factory:
FactorySettingsReader.apiKey(environment: environment) != nil
default:
false
}
}
private static func environmentExtendedAPIAuthConfigured(
provider: UsageProvider,
environment: [String: String]) -> Bool
{
switch provider {
case .kimi:
KimiSettingsReader.apiKey(environment: environment) != nil
case .kimik2:
KimiK2SettingsReader.apiKey(environment: environment) != nil
case .llmproxy:
LLMProxySettingsReader.apiKey(environment: environment) != nil
case .clawrouter:
ClawRouterSettingsReader.apiKey(environment: environment) != nil
case .sub2api:
Sub2APISettingsReader.apiKey(environment: environment) != nil
case .moonshot:
MoonshotSettingsReader.apiKey(environment: environment) != nil
case .ollama:
OllamaAPISettingsReader.apiKey(environment: environment) != nil
case .openai:
OpenAIAPISettingsReader.apiKey(environment: environment) != nil
case .openrouter:
OpenRouterSettingsReader.apiToken(environment: environment) != nil
case .stepfun:
StepFunSettingsReader.token(environment: environment) != nil
case .synthetic:
SyntheticSettingsReader.apiKey(environment: environment) != nil
case .venice:
VeniceSettingsReader.apiKey(environment: environment) != nil
case .warp:
WarpSettingsReader.apiKey(environment: environment) != nil
case .zai:
ZaiSettingsReader.apiToken(environment: environment) != nil
default:
false
}
}
private static func environmentWebAuthConfigured(
provider: UsageProvider,
environment: [String: String]) -> Bool
{
switch provider {
case .alibabatokenplan:
AlibabaTokenPlanSettingsReader.cookieHeader(environment: environment) != nil
case .kimi:
KimiSettingsReader.authToken(environment: environment) != nil
case .manus:
ManusSettingsReader.sessionToken(environment: environment) != nil
case .perplexity:
PerplexitySettingsReader.sessionToken(environment: environment) != nil
default:
false
}
}
static func resolveMiniMaxAuthMode(
environment: [String: String],
settings: ProviderSettingsSnapshot?) -> MiniMaxAuthMode
{
let apiToken = ProviderTokenResolver.minimaxToken(environment: environment)
let envCookieHeader = ProviderTokenResolver.minimaxCookie(environment: environment)
let settingsCookieHeader = CookieHeaderNormalizer.normalize(settings?.minimax?.manualCookieHeader)
let cookieHeader = envCookieHeader ?? settingsCookieHeader
return MiniMaxAuthMode.resolve(apiToken: apiToken, cookieHeader: cookieHeader)
}
}
#if DEBUG
extension CodexBarCLI {
static func _diagnosticAuthSummaryForTesting(
provider: UsageProvider,
account: ProviderTokenAccount?,
config: ProviderConfig?,
environment: [String: String],
settings: ProviderSettingsSnapshot?) -> ProviderDiagnosticAuthSummary
{
self.diagnosticAuthSummary(
provider: provider,
account: account,
config: config,
environment: environment,
settings: settings)
}
static func _resolveMiniMaxAuthModeForTesting(
environment: [String: String],
settings: ProviderSettingsSnapshot?) -> MiniMaxAuthMode
{
self.resolveMiniMaxAuthMode(environment: environment, settings: settings)
}
}
#endif