feat(macos): add macOS menu-bar app and harden cross-device iCloud sync

Introduce a standalone macOS menu-bar app (OSGKeyboardMac) that reuses the
platform-agnostic OSGKeyboardShared core: record -> cloud/local ASR -> polish
-> insert. Local mode uses Qwen3-ASR via mlx-swift-asr (macOS 15+, Apple
Silicon); iOS targets stay zero-SPM.

Harden iCloud sync for multi-device correctness:
- Per-field settings merge (appSettings.v2) so concurrent edits no longer
  clobber each other's unrelated fields.
- Per-device usage statistics (G-Counter) that sum instead of max().
- Tombstoned dictionary/history merge so deletes propagate and entries can't
  resurrect.
- API keys replicate via iCloud Keychain, never iCloud KVS JSON; pulling a
  legacy blob without key fields no longer wipes local Keychain entries.
- Add a low-risk "Sync Now" action in Settings.

Fix Flow keyboard mic state: stay orange until the host publishes a real ready
contract, share a single MicVoiceAvailability gate, and self-heal stale
cross-process heartbeat jitter instead of getting stuck.

Extract shared storage (SpeechHistoryStore/UsageStatisticsStore,
ConfigurationStore) into OSGKeyboardShared and add tests for the new
sync/merge logic.
This commit is contained in:
Rocky
2026-07-08 18:13:56 +08:00
parent 128aab1b02
commit c2f07bd8d2
99 changed files with 6735 additions and 740 deletions
+102
View File
@@ -26,6 +26,14 @@ options:
# Carthage (matches the long-standing "zero dependencies" promise in the
# README). Optional post-ASR cloud polish routes through DeepSeek (or
# any OpenAI-compatible endpoint) using the existing `LLMClient`.
#
# macOS only: `mlx-swift-asr` (MLXASR) for Qwen3-ASR-1.7B on-device ASR.
# iOS targets remain zero-SPM.
packages:
MLXSwiftASR:
url: https://github.com/ontypehq/mlx-swift-asr
branch: main
settings:
base:
@@ -375,6 +383,91 @@ targets:
PRODUCT_BUNDLE_IDENTIFIER: com.osgkeyboard.ios.ext.tests
TARGETED_DEVICE_FAMILY: "1"
# =========================================================
# macOS 菜单栏 App (Phase 1 · 云端 MVP)
# =========================================================
# Standalone menu-bar utility: record → cloud ASR → polish → clipboard.
# Reuses the platform-agnostic core files from OSGKeyboardShared at the
# source level (no framework), excluding the iOS-only files that import
# SpeechAnalyzer / AVAudioSession / UIKit / SwiftUI views. Local mode uses
# Qwen3-ASR MLX via mlx-swift-asr (requires macOS 15+, Apple Silicon).
OSGKeyboardMac:
type: application
platform: macOS
deploymentTarget: "15.0"
sources:
# Same Icon Composer asset as the iOS app — do not ship a separate PNG
# appiconset; actool crashes if both coexist.
- path: OSGKeyboard/AppIcon.icon
buildPhase: resources
# Reuse the iOS asset catalog for provider logos + brand mark. Exclude
# the PNG appiconset so it doesn't clash with AppIcon.icon above.
- path: OSGKeyboard/Assets.xcassets
excludes:
- "AppIcon.appiconset"
- path: OSGKeyboardMac
- path: OSGKeyboardShared
excludes:
- "en.lproj"
- "zh-Hans.lproj"
- "Info.plist"
- "**/*.swift.example"
- "DesignSystem/WaveformView.swift"
- "DesignSystem/TranslationChip.swift"
- "DesignSystem/RecordButton.swift"
- "Services/ASRService.swift"
- "Services/CloudASR/CloudASRService.swift"
- "Services/ChunkedUtterancePipeline.swift"
- "Services/FlowContinuousCapture.swift"
- "Services/LiveDictationController.swift"
- "Services/KeyboardState.swift"
- "Services/CursorNavigation.swift"
- "Services/CustomLanguageModelManager.swift"
- "Models/MicVoiceAvailability+Keyboard.swift"
- "Utilities/ProgressiveDictationTranscriptAccumulator.swift"
- path: OSGKeyboardShared/en.lproj/Shared.strings
buildPhase: resources
- path: OSGKeyboardShared/zh-Hans.lproj/Shared.strings
buildPhase: resources
entitlements:
path: OSGKeyboardMac/OSGKeyboardMac.entitlements
properties:
com.apple.security.app-sandbox: true
com.apple.security.device.audio-input: true
com.apple.security.network.client: true
com.apple.developer.ubiquity-kvstore-identifier: $(TeamIdentifierPrefix)com.osgkeyboard.ios
keychain-access-groups:
- $(AppIdentifierPrefix)com.osgkeyboard.shared
info:
path: OSGKeyboardMac/Info.plist
properties:
CFBundleDisplayName: OSGKeyboard
CFBundleShortVersionString: "$(MARKETING_VERSION)"
CFBundleVersion: "$(CURRENT_PROJECT_VERSION)"
CFBundleDevelopmentRegion: en
CFBundleLocalizations:
- en
- zh-Hans
LSApplicationCategoryType: public.app-category.utilities
NSMicrophoneUsageDescription: "OSGKeyboard uses the microphone to transcribe your voice via your configured cloud speech provider."
NSSpeechRecognitionUsageDescription: "OSGKeyboard uses on-device speech recognition for local dictation mode."
NSHumanReadableCopyright: "OSGKeyboard"
ITSAppUsesNonExemptEncryption: false
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.osgkeyboard.mac
MACOSX_DEPLOYMENT_TARGET: "15.0"
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
# macOS app is not an app extension; override the framework-wide flag.
APPLICATION_EXTENSION_API_ONLY: NO
CODE_SIGN_STYLE: Automatic
DEVELOPMENT_TEAM: X329MZU23S
dependencies:
- sdk: Speech.framework
- sdk: AVFoundation.framework
- package: MLXSwiftASR
product: MLXASR
schemes:
OSGKeyboard:
build:
@@ -391,3 +484,12 @@ schemes:
- OSGKeyboardExtTests
archive:
config: Release
OSGKeyboardMac:
build:
targets:
OSGKeyboardMac: all
run:
config: Debug
archive:
config: Release