fix: keyboard stability, host-only analytics, and auto-reply dedupe

- record analytics/usage only in the host app; the extension no longer opens the
  SQLite queues, so hiding the keyboard cannot hold WAL locks (0xdead10cc)
- sync clipboard history writes across processes so deletes made on one side
  are not reverted by the other
- guard memory-relief reentrancy on controller and telemetry sides to break
  the recursive crash loop
- resolve credentials off the main thread and migrate legacy keys once per
  install, so Keychain reads cannot be caught in the suspend window
- gate Volcengine ASR ping completion to once (SIGTRAP on double resume)
- treat same-text captures as the same logical copy and dedupe auto actions
  by content fingerprint within 30s, stopping Universal Clipboard storms
- realign version line to 2.1.0 (build 110)
This commit is contained in:
2026-09-25 12:14:29 +08:00
parent c0848a00de
commit a2abfc5e8c
31 changed files with 727 additions and 314 deletions
@@ -47,7 +47,11 @@ public struct AppGroupPersistor {
state.clipboardAutoEmailReplyEnabled = store.clipboardAutoEmailReplyEnabled
applySkillSnapshot(store: store, into: state)
state.keyboardHapticIntensity = store.keyboardHapticIntensity
applyAPIKeyAvailability(store: store, into: state)
// Credential availability is intentionally NOT resolved here: it needs
// synchronous Keychain reads, which are a RunningBoard 0xdead10cc kill
// vector on the extension's main thread inside the appear/suspend
// window. KeyboardState's optimistic defaults stand until
// `KeyboardConfigSync.refreshCredentialsFromKeychain()` lands.
#if DEBUG
// Log only credential availability and the base URL origin. Never put
@@ -106,7 +110,7 @@ public struct AppGroupPersistor {
state.clipboardAutoEmailReplyEnabled = store.clipboardAutoEmailReplyEnabled
applySkillSnapshot(store: store, into: state)
state.keyboardHapticIntensity = store.keyboardHapticIntensity
applyAPIKeyAvailability(store: store, into: state)
// Credential availability is refreshed asynchronously — see load(into:).
}
/// Resolve once outside SwiftUI body evaluation. Assigning the complete
@@ -154,17 +158,22 @@ public struct AppGroupPersistor {
)
}
/// Applies a credential snapshot that was resolved OFF the main actor
/// (see `KeyboardConfigSync.refreshCredentialsFromKeychain`). This method
/// itself performs no Keychain access.
///
/// Cloud without ASR/LLM keys blocks the mic. Local ASR still works when
/// the polish key is missing — show a soft tip above the mic instead.
private func applyAPIKeyAvailability(
store: AppGroupStore,
public func applyCredentialAvailability(
isPolishKeyMissing: Bool,
isCloudAPIKeyMissingForVoiceInput: Bool,
into state: KeyboardViewController.State
) {
state.aiServiceAvailable = !store.isPolishKeyMissing
if store.isCloudAPIKeyMissingForVoiceInput {
state.aiServiceAvailable = !isPolishKeyMissing
if isCloudAPIKeyMissingForVoiceInput {
state.micDisabled = true
state.micDisabledHint = ExtL10n.string("keyboard.mic.disabled.missingApiKey")
} else if store.isPolishKeyMissing {
} else if isPolishKeyMissing {
state.micDisabled = false
state.micDisabledHint = ExtL10n.string("keyboard.mic.hint.missingPolishApiKey")
} else {