feat: cursor navigation, key sounds, dictionary tooling, key security

Batch of in-progress app work from the working tree.

- feat(keyboard): CursorNavigation + CursorDragPad for caret movement;
  KeyboardSoundFeedback for system key click sounds
- feat(dictionary): DictionaryAliasGenerator + PersonalDictionaryEntrySheet;
  TranscriptPostProcessor quality gate; retire DictionaryLearner
- feat(ui): TabBarVisibility handling; drop PageHeaderRow /
  PageHeaderConfirmButton; refresh views and localizable strings
- fix(security): move the hardcoded DeepSeek key out of
  PreconfiguredKeys.swift into a gitignored PreconfiguredKeys.local.swift
  (seeded from .example by generate-xcodeproj.sh)
- docs(agents): add Conventional Commits versioning + bilingual changelog rules
- chore(gitignore): ignore PreconfiguredKeys.local.swift, .cache/, pycache

Custom language model / lexicon work stays on
feature/custom-language-model-asr. Changelog bullets added under
[Unreleased]; no version bump.
This commit is contained in:
Rocky
2026-07-05 18:27:23 +08:00
parent 074e24d87f
commit 05e005e9ce
60 changed files with 3247 additions and 1388 deletions
+19 -7
View File
@@ -36,15 +36,25 @@ public enum LLMError: Error, LocalizedError, Sendable, Equatable {
}
public protocol LLMClient: Sendable {
func polish(_ text: String, systemPrompt: String) async throws -> String
/// Polish `text` with `systemPrompt`. `timeout` overrides the
/// per-request HTTP timeout for this call; when `nil` the client's
/// `requestTimeout` baseline is used. Long transcripts must pass a
/// larger, length-scaled timeout so the HTTP request is not cut off
/// mid-generation (see `PolishingService.effectiveTimeout`).
func polish(_ text: String, systemPrompt: String, timeout: TimeInterval?) async throws -> String
/// Single source of truth for the upper bound on a single LLM HTTP
/// round-trip. Both the `URLRequest` we send and any wrapping
/// timeout-style race (e.g. `PolishingService`'s `withThrowingTaskGroup`)
/// must read from this property so the two never disagree.
/// Baseline upper bound for a single LLM HTTP round-trip when no
/// per-request `timeout` is supplied.
var requestTimeout: TimeInterval { get }
}
public extension LLMClient {
/// Convenience overload that uses the baseline `requestTimeout`.
func polish(_ text: String, systemPrompt: String) async throws -> String {
try await polish(text, systemPrompt: systemPrompt, timeout: nil)
}
}
// MARK: - OpenAI-compatible implementation
public struct OpenAICompatibleClient: LLMClient {
@@ -71,7 +81,7 @@ public struct OpenAICompatibleClient: LLMClient {
self.session = session
}
public func polish(_ text: String, systemPrompt: String) async throws -> String {
public func polish(_ text: String, systemPrompt: String, timeout: TimeInterval?) async throws -> String {
guard !apiKey.isEmpty else { throw LLMError.noAPIKey }
let urlString = baseURL.hasSuffix("/")
@@ -93,7 +103,9 @@ public struct OpenAICompatibleClient: LLMClient {
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
req.timeoutInterval = requestTimeout
// Per-request timeout scales with transcript length; fall back to
// the baseline when the caller does not supply one.
req.timeoutInterval = timeout ?? requestTimeout
let encoder = JSONEncoder()
req.httpBody = try encoder.encode(request)