2e2d8e33b3
- AppGroup.defaults: in DEBUG, missing App Group is a hard fatalError
with a precise remediation message (was a soft print + .standard
fallback, which desynced the keyboard extension from the main App).
Release keeps the fallback + NSLog so end-users still get a usable app.
- KeyboardViewController.loadPersistedLocale now prints a masked DEBUG
view of the live App Group config (provider, baseURL, masked key,
model, mode, locale) so the extension's view is visible in the
device console.
- KeyboardViewController.handleFinalTranscript now routes by typed error:
noAPIKey → red error '未配置 API Key · 请在主 App 设置中填写'
http 401 → red error 'API Key 无效 (401) · 请检查主 App 设置'
http 429 → red error 'API 限流 (429) · 请稍后再试'
other → insert raw transcript + generic error badge
- APISettingsCard gains a 'Test connection' button that runs a single
client.polish('ping') round-trip and surfaces the typed result inline.
- PolishingService.timeout raised 12s → 15s to match LLMClient.request
timeout (was racing and discarding successful responses in 12–15s).
- Tests: 4 new cases (HTTP 429, transport timeout, App Group cross-process,
AppGroupStore→LLMClient noAPIKey). All 8 tests pass on iPhone 16e sim.
xcodebuild iOS Simulator: SUCCEEDED
xcodebuild test: 8/8 passed
47 lines
1.4 KiB
Swift
47 lines
1.4 KiB
Swift
// PolishingService.swift
|
|
// OSGKeyboard · Keyboard Extension
|
|
//
|
|
// Takes raw ASR transcript and runs it through the user's configured LLM
|
|
// to produce polished, well-punctuated text. Falls back to the raw transcript
|
|
// if the LLM call fails or times out.
|
|
|
|
import Foundation
|
|
import OSGKeyboardShared
|
|
|
|
public actor PolishingService {
|
|
|
|
public enum PolishError: Error {
|
|
case noTranscript
|
|
case timeout
|
|
}
|
|
|
|
private let store: AppGroupStore
|
|
private let timeout: TimeInterval
|
|
|
|
public init(store: AppGroupStore = AppGroupStore(), timeout: TimeInterval = 15) {
|
|
self.store = store
|
|
self.timeout = timeout
|
|
}
|
|
|
|
public func polish(_ raw: String) async throws -> String {
|
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else { throw PolishError.noTranscript }
|
|
|
|
let client = store.makeClient()
|
|
let prompt = store.systemPrompt
|
|
|
|
return try await withThrowingTaskGroup(of: String.self) { group in
|
|
group.addTask {
|
|
try await client.polish(trimmed, systemPrompt: prompt)
|
|
}
|
|
group.addTask {
|
|
try await Task.sleep(nanoseconds: UInt64(self.timeout * 1_000_000_000))
|
|
throw PolishError.timeout
|
|
}
|
|
let result = try await group.next()!
|
|
group.cancelAll()
|
|
return result
|
|
}
|
|
}
|
|
}
|