Files
OSGKeyboard/OSGKeyboardShared/Services/ProviderToolRunnerState.swift
T
Rocky cc8dd1070a feat: macOS architecture, cloud ASR/LLM providers, and 6-step iOS onboarding
- Add macOS menu-bar dictation app with local ASR models (SenseVoice/Qwen3),
  global Option hotkey, and bottom overlay
- Add cloud ASR/LLM providers (Anthropic, Volcengine, Bailian, and more) with
  provider logos, model listing, and connection checks
- Add shared 7-day usage stats UI (UsageStatsCluster / SevenDayUsageChart)
- Add iOS onboarding step 6 for polish LLM setup; hide custom-language-model
  diagnostic toggle behind DEBUG
- Unify iOS onboarding tagline with the macOS brand line ("开口即文字。")
- Rewrite README (Chinese-first, product-oriented) and refresh GitHub Pages
2026-07-11 19:10:20 +08:00

131 lines
4.2 KiB
Swift

// ProviderToolRunnerState.swift
// OSGKeyboard · Shared
//
// Pure state machine for Settings provider tool rows (validate / fetch models).
import Foundation
public struct ProviderToolRunnerState: Equatable, Sendable {
public var isRunning: Bool
public var message: String?
public var failed: Bool
public var models: [String]
public init(
isRunning: Bool = false,
message: String? = nil,
failed: Bool = false,
models: [String] = []
) {
self.isRunning = isRunning
self.message = message
self.failed = failed
self.models = models
}
}
@MainActor
public enum ProviderToolRunner {
public static func runValidate(
runningMessage: String,
successMessage: String,
validate: () async throws -> Void
) async -> ProviderToolRunnerState {
var state = ProviderToolRunnerState(isRunning: true, message: runningMessage, failed: false)
do {
try await validate()
state.isRunning = false
state.message = successMessage
state.failed = false
} catch {
state.isRunning = false
state.failed = true
state.message = (error as? LocalizedError)?.errorDescription ?? "\(error)"
}
return state
}
public static func runFetchModels(
runningMessage: String,
loadedMessage: (Int) -> String,
emptyMessage: String,
currentModel: String,
fetchModels: () async throws -> [String]
) async -> (state: ProviderToolRunnerState, selectedModel: String?) {
var state = ProviderToolRunnerState(isRunning: true, message: runningMessage, failed: false)
do {
let fetched = try await fetchModels()
guard !fetched.isEmpty else {
state.isRunning = false
state.failed = true
state.message = emptyMessage
state.models = []
return (state, nil)
}
var resolved = fetched
let trimmed = currentModel.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty, !resolved.contains(trimmed) {
resolved.insert(trimmed, at: 0)
}
state.models = resolved
state.isRunning = false
state.failed = false
state.message = loadedMessage(resolved.count)
let selected: String?
if trimmed.isEmpty, let first = resolved.first {
selected = first
} else {
selected = nil
}
return (state, selected)
} catch {
state.isRunning = false
state.failed = true
state.message = (error as? LocalizedError)?.errorDescription ?? "\(error)"
state.models = []
return (state, nil)
}
}
}
public enum HardTimeout {
/// Returns the first completed result; the losing task is cancelled.
public static func run<T: Sendable>(
seconds: TimeInterval,
operation: @escaping @Sendable () async throws -> T
) async throws -> T {
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask { try await operation() }
group.addTask {
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
throw CancellationError()
}
guard let result = try await group.next() else {
throw CancellationError()
}
group.cancelAll()
return result
}
}
/// Non-throwing variant for tasks that should fall back when time elapses.
public static func value<T: Sendable>(
seconds: TimeInterval,
operation: @escaping @Sendable () async -> T,
onTimeout: @escaping @Sendable () -> T
) async -> T {
await withTaskGroup(of: T.self) { group in
group.addTask { await operation() }
group.addTask {
try? await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
return onTimeout()
}
let result = await group.next() ?? onTimeout()
group.cancelAll()
return result
}
}
}