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
@@ -3,6 +3,7 @@
//
// Pure rules for accepting clipboard text and simple English/whitespace tokens.
import CryptoKit
import Foundation
public enum ClipboardHistoryPolicy: Sendable {
@@ -298,4 +299,20 @@ public enum ClipboardHistoryPolicy: Sendable {
) -> Bool {
now.timeIntervalSince(entry.createdAt) <= aiHintEligibilitySeconds
}
/// Stable content fingerprint for deduping auto triggers across pasteboard
/// generations. SHA-256 because `String.hashValue` is per-process randomized.
public static func contentFingerprint(for text: String) -> String {
SHA256.hash(data: Data(text.utf8))
.map { String(format: "%02x", $0) }
.joined()
}
/// Whether an action fired at `firedAt` still suppresses an identical repeat
/// at `now`. Universal Clipboard re-announces one copy under several
/// changeCounts within seconds; a deliberate re-copy after the window may
/// fire again.
public static func isRepeatSuppressed(firedAt: Date, now: Date = Date()) -> Bool {
now.timeIntervalSince(firedAt) <= aiHintEligibilitySeconds
}
}