feat: default right modifier hotkey
11 files modified
3 files added
| | |
| | | cfg, err := config.Load() |
| | | if err != nil { |
| | | logger.Error("Failed to load config: %v", err) |
| | | cfg = config.Default() |
| | | cfg = config.ExistingConfigFallback() |
| | | } |
| | | app.cfg = cfg |
| | | |
| | |
| | | logger.Error("Failed to load config for engine init: %v", err) |
| | | cfg = a.cfg |
| | | if cfg == nil { |
| | | cfg = config.Default() |
| | | cfg = config.ExistingConfigFallback() |
| | | } |
| | | } |
| | | |
| | |
| | | }, |
| | | "inputMonitoring": { |
| | | "title": "Input Monitoring", |
| | | "desc": "Listens for the global right Alt hotkey while other apps are in front." |
| | | "desc": "Listens for the configured global hotkey while other apps are in front." |
| | | }, |
| | | "backgroundActivity": { |
| | | "title": "Background Activity / Login Item", |
| | |
| | | "hintHoldDesc": "Hold the hotkey to record, release to recognize and type. Best for quick phrases.", |
| | | "hintFree": "Free Talk", |
| | | "hintFreeDesc": "Tap the hotkey once to start, speak freely, tap again to stop and type. Best for long dictation.", |
| | | "hintCombo": "Keyboard shortcuts (e.g. Ctrl+C) won't trigger recording." |
| | | "hintCombo": "Keyboard shortcuts (e.g. Ctrl+C) won't trigger recording. If the right Command / Windows key is unavailable, choose another hotkey with Change." |
| | | }, |
| | | "about": { |
| | | "version": "Version", |
| | |
| | | }, |
| | | "inputMonitoring": { |
| | | "title": "输入监控", |
| | | "desc": "用于在私语输入法窗口不可见、其他 App 正在前台时监听右 Alt 全局热键。" |
| | | "desc": "用于在私语输入法窗口不可见、其他 App 正在前台时监听当前全局热键。" |
| | | }, |
| | | "backgroundActivity": { |
| | | "title": "后台活动 / 登录项", |
| | |
| | | "hintHoldDesc": "长按热键开始录音,松开即停止识别并输入文字,适合快速短语。", |
| | | "hintFree": "自由说话", |
| | | "hintFreeDesc": "短按一下热键开始,连续说话,再短按一下停止并输入文字,适合长段口述。", |
| | | "hintCombo": "使用组合键(如 Ctrl+C)不会触发录音。" |
| | | "hintCombo": "使用组合键(如 Ctrl+C)不会触发录音。若右侧 Command / Windows 键不可用,请点击修改并选择其他热键。" |
| | | }, |
| | | "about": { |
| | | "version": "版本", |
| | |
| | | export const soundFeedback = writable<boolean>(true); |
| | | export const copyToClipboard = writable<boolean>(true); |
| | | export const startAtLogin = writable<boolean>(false); |
| | | export const hotkeyVK = writable<number>(0xa5); |
| | | export const hotkeyVK = writable<number>(0x5c); |
| | | export const hotkeyMode = writable<"tap" | "hold">("hold"); |
| | | export const languageMode = writable<"auto" | "manual">("auto"); |
| | | export const languageID = writable<LanguageID>("zh-CN"); |
| | |
| | | export const indicatorVisible = writable<boolean>(false); |
| | | export const indicatorStatus = writable<IndicatorStatus>("hidden"); |
| | | export const indicatorVolume = writable<number>(0); |
| | | export const hotkeyName = writable<string>("Ctrl"); |
| | | const defaultHotkeyName = |
| | | typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform) |
| | | ? "R-⌘" |
| | | : "R-Win"; |
| | | |
| | | export const hotkeyName = writable<string>(defaultHotkeyName); |
| | |
| | | import ( |
| | | "encoding/json" |
| | | "os" |
| | | "runtime" |
| | | "voicesnap/internal/logger" |
| | | "voicesnap/internal/model" |
| | | "voicesnap/internal/paths" |
| | | ) |
| | | |
| | | const ( |
| | | LegacyDefaultHotkeyVK = 0xA5 |
| | | RightModifierDefaultVK = 0x5C |
| | | HotkeyModeTap = "tap" |
| | | HotkeyModeHold = "hold" |
| | | LanguageModeAuto = "auto" |
| | |
| | | |
| | | // Default returns a default configuration. |
| | | func Default() *Config { |
| | | return defaultWithHotkey(defaultHotkeyVKForNewConfig(runtime.GOOS)) |
| | | } |
| | | |
| | | func legacyDefaultForExistingConfig() *Config { |
| | | return defaultWithHotkey(LegacyDefaultHotkeyVK) |
| | | } |
| | | |
| | | // ExistingConfigFallback returns a legacy-safe fallback for cases where a |
| | | // config file exists but cannot be read or parsed. |
| | | func ExistingConfigFallback() *Config { |
| | | return legacyDefaultForExistingConfig() |
| | | } |
| | | |
| | | func defaultWithHotkey(hotkeyVK int) *Config { |
| | | return &Config{ |
| | | HotkeyVK: 0xA5, // Right Alt |
| | | HotkeyVK: hotkeyVK, |
| | | HotkeyMode: HotkeyModeHold, |
| | | AutoHide: true, |
| | | SoundFeedback: true, |
| | |
| | | LanguageID: model.DefaultLanguageID, |
| | | ModelDownloadUrl: "http://www.maikami.com/voicesnap/sensevoice.zip", |
| | | FallbackModelDownloadUrl: "https://modelscope.cn/models/sherpa-onnx/sherpa-onnx-sense-voice-zh-en-ja-ko-yue/resolve/master/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2024-07-17.tar.bz2", |
| | | } |
| | | } |
| | | |
| | | func defaultHotkeyVKForNewConfig(goos string) int { |
| | | switch goos { |
| | | case "darwin", "windows": |
| | | return RightModifierDefaultVK |
| | | default: |
| | | return LegacyDefaultHotkeyVK |
| | | } |
| | | } |
| | | |
| | |
| | | return nil, err |
| | | } |
| | | |
| | | cfg := Default() |
| | | cfg := legacyDefaultForExistingConfig() |
| | | if err := json.Unmarshal(data, cfg); err != nil { |
| | | return nil, err |
| | | } |
| | |
| | | package config |
| | | |
| | | import ( |
| | | "encoding/json" |
| | | "os" |
| | | "path/filepath" |
| | | "runtime" |
| | | "testing" |
| | | |
| | | "voicesnap/internal/model" |
| | | "voicesnap/internal/paths" |
| | | ) |
| | | |
| | | func TestDefaultLanguageConfig(t *testing.T) { |
| | |
| | | } |
| | | if cfg.ModelSelectionMode != ModelSelectionModeAuto { |
| | | t.Fatalf("model selection mode = %q, want %q", cfg.ModelSelectionMode, ModelSelectionModeAuto) |
| | | } |
| | | } |
| | | |
| | | func TestDefaultHotkeyForNewConfig(t *testing.T) { |
| | | tests := []struct { |
| | | goos string |
| | | want int |
| | | }{ |
| | | {goos: "darwin", want: RightModifierDefaultVK}, |
| | | {goos: "windows", want: RightModifierDefaultVK}, |
| | | {goos: "linux", want: LegacyDefaultHotkeyVK}, |
| | | } |
| | | |
| | | for _, tt := range tests { |
| | | if got := defaultHotkeyVKForNewConfig(tt.goos); got != tt.want { |
| | | t.Fatalf("defaultHotkeyVKForNewConfig(%q) = 0x%X, want 0x%X", tt.goos, got, tt.want) |
| | | } |
| | | } |
| | | } |
| | | |
| | | func TestLoadCreatesFirstConfigWithNewPlatformDefault(t *testing.T) { |
| | | withIsolatedConfigHome(t) |
| | | |
| | | cfg, err := Load() |
| | | if err != nil { |
| | | t.Fatalf("Load() error = %v", err) |
| | | } |
| | | if got, want := cfg.HotkeyVK, defaultHotkeyVKForNewConfig(runtime.GOOS); got != want { |
| | | t.Fatalf("HotkeyVK = 0x%X, want 0x%X", got, want) |
| | | } |
| | | if cfg.HotkeyMode != HotkeyModeHold { |
| | | t.Fatalf("HotkeyMode = %q, want %q", cfg.HotkeyMode, HotkeyModeHold) |
| | | } |
| | | |
| | | raw := readConfigJSON(t) |
| | | if got, ok := raw["HotkeyVK"].(float64); !ok || int(got) != cfg.HotkeyVK { |
| | | t.Fatalf("saved HotkeyVK = %#v, want %d", raw["HotkeyVK"], cfg.HotkeyVK) |
| | | } |
| | | if got := raw["HotkeyMode"]; got != HotkeyModeHold { |
| | | t.Fatalf("saved HotkeyMode = %#v, want %q", got, HotkeyModeHold) |
| | | } |
| | | } |
| | | |
| | | func TestLoadExistingConfigPreservesHotkeyAndMode(t *testing.T) { |
| | | withIsolatedConfigHome(t) |
| | | writeConfigJSON(t, map[string]interface{}{ |
| | | "HotkeyVK": 0x41, |
| | | "HotkeyMode": HotkeyModeTap, |
| | | }) |
| | | |
| | | cfg, err := Load() |
| | | if err != nil { |
| | | t.Fatalf("Load() error = %v", err) |
| | | } |
| | | if cfg.HotkeyVK != 0x41 { |
| | | t.Fatalf("HotkeyVK = 0x%X, want 0x41", cfg.HotkeyVK) |
| | | } |
| | | if cfg.HotkeyMode != HotkeyModeTap { |
| | | t.Fatalf("HotkeyMode = %q, want %q", cfg.HotkeyMode, HotkeyModeTap) |
| | | } |
| | | } |
| | | |
| | | func TestLoadExistingConfigMissingHotkeyUsesLegacyFallback(t *testing.T) { |
| | | withIsolatedConfigHome(t) |
| | | writeConfigJSON(t, map[string]interface{}{ |
| | | "HotkeyMode": HotkeyModeTap, |
| | | }) |
| | | |
| | | cfg, err := Load() |
| | | if err != nil { |
| | | t.Fatalf("Load() error = %v", err) |
| | | } |
| | | if cfg.HotkeyVK != LegacyDefaultHotkeyVK { |
| | | t.Fatalf("HotkeyVK = 0x%X, want legacy 0x%X", cfg.HotkeyVK, LegacyDefaultHotkeyVK) |
| | | } |
| | | if cfg.HotkeyVK == RightModifierDefaultVK { |
| | | t.Fatalf("existing config missing HotkeyVK must not inherit new default 0x%X", RightModifierDefaultVK) |
| | | } |
| | | if cfg.HotkeyMode != HotkeyModeTap { |
| | | t.Fatalf("HotkeyMode = %q, want %q", cfg.HotkeyMode, HotkeyModeTap) |
| | | } |
| | | } |
| | | |
| | | func TestLoadExistingConfigMissingModeKeepsLegacyHoldFallback(t *testing.T) { |
| | | withIsolatedConfigHome(t) |
| | | writeConfigJSON(t, map[string]interface{}{ |
| | | "HotkeyVK": 0x41, |
| | | }) |
| | | |
| | | cfg, err := Load() |
| | | if err != nil { |
| | | t.Fatalf("Load() error = %v", err) |
| | | } |
| | | if cfg.HotkeyVK != 0x41 { |
| | | t.Fatalf("HotkeyVK = 0x%X, want 0x41", cfg.HotkeyVK) |
| | | } |
| | | if cfg.HotkeyMode != HotkeyModeHold { |
| | | t.Fatalf("HotkeyMode = %q, want %q", cfg.HotkeyMode, HotkeyModeHold) |
| | | } |
| | | } |
| | | |
| | | func TestExistingConfigFallbackUsesLegacyHotkey(t *testing.T) { |
| | | cfg := ExistingConfigFallback() |
| | | if cfg.HotkeyVK != LegacyDefaultHotkeyVK { |
| | | t.Fatalf("HotkeyVK = 0x%X, want legacy 0x%X", cfg.HotkeyVK, LegacyDefaultHotkeyVK) |
| | | } |
| | | if cfg.HotkeyVK == RightModifierDefaultVK { |
| | | t.Fatalf("existing config fallback must not inherit new default 0x%X", RightModifierDefaultVK) |
| | | } |
| | | if cfg.HotkeyMode != HotkeyModeHold { |
| | | t.Fatalf("HotkeyMode = %q, want %q", cfg.HotkeyMode, HotkeyModeHold) |
| | | } |
| | | } |
| | | |
| | |
| | | t.Fatalf("bad mode normalized to %q, want %q", got, ModelSelectionModeAuto) |
| | | } |
| | | } |
| | | |
| | | func withIsolatedConfigHome(t *testing.T) { |
| | | t.Helper() |
| | | home := t.TempDir() |
| | | t.Setenv("HOME", home) |
| | | t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config")) |
| | | } |
| | | |
| | | func writeConfigJSON(t *testing.T, value map[string]interface{}) { |
| | | t.Helper() |
| | | if err := paths.Ensure(); err != nil { |
| | | t.Fatalf("paths.Ensure() error = %v", err) |
| | | } |
| | | data, err := json.Marshal(value) |
| | | if err != nil { |
| | | t.Fatalf("json.Marshal() error = %v", err) |
| | | } |
| | | if err := os.WriteFile(paths.File("config.json"), data, 0644); err != nil { |
| | | t.Fatalf("write config error = %v", err) |
| | | } |
| | | } |
| | | |
| | | func readConfigJSON(t *testing.T) map[string]interface{} { |
| | | t.Helper() |
| | | data, err := os.ReadFile(paths.File("config.json")) |
| | | if err != nil { |
| | | t.Fatalf("read config error = %v", err) |
| | | } |
| | | var raw map[string]interface{} |
| | | if err := json.Unmarshal(data, &raw); err != nil { |
| | | t.Fatalf("json.Unmarshal() error = %v", err) |
| | | } |
| | | return raw |
| | | } |
| | |
| | | cfg, err := config.Load() |
| | | if err != nil { |
| | | logger.Error("Failed to load config for model selection: %v", err) |
| | | cfg = config.Default() |
| | | cfg = config.ExistingConfigFallback() |
| | | } |
| | | current := modelselection.Resolve(cfg, language.NewSystemDetector()) |
| | | return NewWithModelID(current.ModelID) |
| | |
| | | func resolveCurrentModel() (model.ResolvedModel, error) { |
| | | cfg, err := config.Load() |
| | | if err != nil { |
| | | cfg = config.Default() |
| | | cfg = config.ExistingConfigFallback() |
| | | } |
| | | current := modelselection.Resolve(cfg, language.NewSystemDetector()) |
| | | return model.ResolveModel(current.ModelID) |
| | |
| | | |
| | | import ( |
| | | "fmt" |
| | | "runtime" |
| | | "time" |
| | | ) |
| | | |
| | |
| | | |
| | | // GetKeyName returns the display name for a virtual key code. |
| | | func GetKeyName(vk int) string { |
| | | return GetKeyNameForPlatform(vk, runtime.GOOS) |
| | | } |
| | | |
| | | func GetKeyNameForPlatform(vk int, goos string) string { |
| | | switch vk { |
| | | case 0x11: |
| | | return "Ctrl" |
| | |
| | | case 0x0D: |
| | | return "Enter" |
| | | case 0x5B: |
| | | if goos == "darwin" { |
| | | return "L-⌘" |
| | | } |
| | | return "L-Win" |
| | | case 0x5C: |
| | | if goos == "darwin" { |
| | | return "R-⌘" |
| | | } |
| | | return "R-Win" |
| | | case 0x1B: |
| | | return "Esc" |
| New file |
| | |
| | | package hotkey |
| | | |
| | | import "testing" |
| | | |
| | | func TestGetKeyNameForPlatformRightModifier(t *testing.T) { |
| | | tests := []struct { |
| | | name string |
| | | goos string |
| | | vk int |
| | | want string |
| | | }{ |
| | | {name: "darwin left command", goos: "darwin", vk: 0x5B, want: "L-⌘"}, |
| | | {name: "darwin right command", goos: "darwin", vk: 0x5C, want: "R-⌘"}, |
| | | {name: "windows left windows", goos: "windows", vk: 0x5B, want: "L-Win"}, |
| | | {name: "windows right windows", goos: "windows", vk: 0x5C, want: "R-Win"}, |
| | | {name: "linux right windows fallback", goos: "linux", vk: 0x5C, want: "R-Win"}, |
| | | } |
| | | |
| | | for _, tt := range tests { |
| | | t.Run(tt.name, func(t *testing.T) { |
| | | if got := GetKeyNameForPlatform(tt.vk, tt.goos); got != tt.want { |
| | | t.Fatalf("GetKeyNameForPlatform(0x%X, %q) = %q, want %q", tt.vk, tt.goos, got, tt.want) |
| | | } |
| | | }) |
| | | } |
| | | } |
| | |
| | | if s.cfg == nil { |
| | | cfg, err := config.Load() |
| | | if err != nil { |
| | | cfg = config.Default() |
| | | cfg = config.ExistingConfigFallback() |
| | | } |
| | | s.cfg = cfg |
| | | } |
| | |
| | | - Hotkey: |
| | | - macOS: `C1.source/privatevoice.src/internal/hotkey/hotkey_darwin.go` |
| | | - Windows: `C1.source/privatevoice.src/internal/hotkey/hotkey_windows.go` |
| | | - Shared display: `C1.source/privatevoice.src/internal/hotkey/hotkey.go`; R222 maps `0x5C` to macOS `R-⌘` and Windows `R-Win`. |
| | | - Config defaults: |
| | | - `C1.source/privatevoice.src/internal/config/config.go`: first generated config uses platform default hotkey (`0x5C` on macOS / Windows) and `hold`; existing config files load through a legacy-safe fallback so missing `HotkeyVK` does not inherit the R222 new default. |
| | | - Text input: |
| | | - macOS: `C1.source/privatevoice.src/internal/input/paste_darwin.go` |
| | | - Windows: `C1.source/privatevoice.src/internal/input/paste_windows.go` |
| New file |
| | |
| | | # BR-HOTKEY-DEFAULTS 分支创建记录 |
| | | |
| | | 任务 ID:`BR-HOTKEY-DEFAULTS-R222-5-CODER-20260706.0151` |
| | | 父任务:`BR-HOTKEY-DEFAULTS` / `R222-5` |
| | | 角色:Coder |
| | | 时间:2026-07-06 01:51 +0800 |
| | | |
| | | ## 基线 |
| | | |
| | | - Git 根:`/Users/ar/Projects/PrivateVoice/03-O` |
| | | - 源码根:`/Users/ar/Projects/PrivateVoice/03-O/C1.source/privatevoice.src` |
| | | - 基线分支:`main` |
| | | - 基线 commit:`1f725abec29ad465f3c73c28c360818139761212` |
| | | - 目标分支:`feature/default-right-modifier-hotkey` |
| | | - 分支用途:R222 主线默认右侧修饰键热键和默认长按输入方式的短生命周期 feature 分支。 |
| | | |
| | | ## 执行前核验 |
| | | |
| | | ```bash |
| | | git status --short --branch |
| | | git rev-parse HEAD |
| | | git branch --list feature/default-right-modifier-hotkey |
| | | ``` |
| | | |
| | | 结果: |
| | | |
| | | - `main` 工作区 clean。 |
| | | - 当前 HEAD 为 `1f725abec29ad465f3c73c28c360818139761212`。 |
| | | - 目标分支不存在。 |
| | | |
| | | ## 创建命令 |
| | | |
| | | ```bash |
| | | git switch -c feature/default-right-modifier-hotkey 1f725abec29ad465f3c73c28c360818139761212 |
| | | git status --short --branch |
| | | git rev-parse HEAD |
| | | ``` |
| | | |
| | | 结果: |
| | | |
| | | - 已切换到 `feature/default-right-modifier-hotkey`。 |
| | | - 分支 HEAD 为 `1f725abec29ad465f3c73c28c360818139761212`。 |
| | | |
| | | ## 范围与边界 |
| | | |
| | | - 只服务 R222。 |
| | | - 只允许修改 `/Users/ar/Projects/PrivateVoice/03-O/C1.source/privatevoice.src` 内与默认热键 / 默认输入方式直接相关的源码,以及必要项目管理证据。 |
| | | - 不触碰 `/Users/ar/Projects/PrivateVoice/P1-inputmethodkit-spike`。 |
| | | - 不触碰 `/Users/ar/Projects/PrivateVoice/P2-mas-accessibility-spike`。 |
| | | - 不 push、不 commit、不 tag、不发布、不生成 manifest、不刷新 codebase-memory、不生成正式发布包。 |
| | | |
| | | ## 生命周期 |
| | | |
| | | - R222-5 Coder 完成后进入 R222-6 正式代码审核。 |
| | | - R222-6 / R222-7 完成后由 PMO / Owner 决定是否合并、保留或删除该短生命周期分支。 |
| | | - 未经 PMO / Owner 明确指令,不得从该分支进入发布或冻结。 |
| New file |
| | |
| | | # R222-5 开发自查 |
| | | |
| | | 任务 ID:`BR-HOTKEY-DEFAULTS-R222-5-CODER-20260706.0151` |
| | | 父任务:`R222-5` |
| | | 角色:Coder |
| | | 时间:2026-07-06 01:57 +0800 |
| | | 结论:`PM_NOTIFY_READY R222-5` |
| | | 建议 Review Gate ID:`R222-6-GATE-20260706.0151` |
| | | |
| | | ## 目标与边界 |
| | | |
| | | - 目标:在当前主线基础上实现 R222 默认值需求:首次安装 / 首次生成配置时,macOS 默认热键为右 Command,Windows 默认热键为右 Windows,默认输入方式为长按。 |
| | | - 分支:`feature/default-right-modifier-hotkey` |
| | | - 基线:`main` / `1f725abec29ad465f3c73c28c360818139761212` |
| | | - 源码根:`/Users/ar/Projects/PrivateVoice/03-O/C1.source/privatevoice.src` |
| | | - 未做:未 push、未 commit、未 tag、未发布、未生成 manifest、未刷新 codebase-memory、未生成正式发布包、未触碰 P1 / P2 spike worktree。 |
| | | |
| | | ## 分支记录 |
| | | |
| | | - 分支创建记录:`/Users/ar/Projects/PrivateVoice/03-O/K2.项目管理/BR-HOTKEY-DEFAULTS-分支创建记录.md` |
| | | - 创建命令: |
| | | |
| | | ```bash |
| | | git switch -c feature/default-right-modifier-hotkey 1f725abec29ad465f3c73c28c360818139761212 |
| | | ``` |
| | | |
| | | ## 变更范围 |
| | | |
| | | - `C1.source/privatevoice.src/internal/config/config.go` |
| | | - 新增首次生成配置默认热键选择:macOS / Windows 使用 `0x5C`,其他平台保留 legacy `0xA5`。 |
| | | - 新增 existing-config legacy-safe fallback,避免已有配置缺字段或加载失败时继承 R222 新默认。 |
| | | - `C1.source/privatevoice.src/internal/config/config_test.go` |
| | | - 覆盖首次生成配置、已有配置保护、缺字段保护、自定义热键 / 输入方式保护、加载失败 fallback。 |
| | | - `C1.source/privatevoice.src/internal/hotkey/hotkey.go` |
| | | - 新增平台化显示映射:macOS `0x5C` 显示 `R-⌘`,Windows / 其他平台显示 `R-Win`。 |
| | | - `C1.source/privatevoice.src/internal/hotkey/hotkey_test.go` |
| | | - 覆盖 macOS / Windows 右侧修饰键显示映射。 |
| | | - `C1.source/privatevoice.src/app.go` |
| | | - `C1.source/privatevoice.src/internal/engine/engine.go` |
| | | - `C1.source/privatevoice.src/services/engine_service.go` |
| | | - 将 `config.Load()` 失败后的兜底改为 existing-config legacy-safe fallback,避免异常配置在运行态拿到 R222 新默认。 |
| | | - `C1.source/privatevoice.src/frontend/src/lib/stores/config.ts` |
| | | - 前端热键初始 store 从 `0xA5` 更新为 `0x5C`,降低首次加载闪烁。 |
| | | - `C1.source/privatevoice.src/frontend/src/lib/stores/indicator.ts` |
| | | - 初始热键名按浏览器平台显示为 `R-⌘` 或 `R-Win`,后端加载后仍以真实配置覆盖。 |
| | | - `C1.source/privatevoice.src/frontend/src/lib/i18n/zh.json` |
| | | - `C1.source/privatevoice.src/frontend/src/lib/i18n/en.json` |
| | | - 去除权限页“右 Alt”旧默认文案,补充右侧 Command / Windows 不可用时使用修改入口的提示。 |
| | | - `CODEGRAPH.md` |
| | | - 记录 R222 配置默认和 hotkey 显示映射边界。 |
| | | |
| | | ## 实现策略 |
| | | |
| | | - `config.Default()` 仍代表“新生成默认配置”,但默认热键由 `defaultHotkeyVKForNewConfig(runtime.GOOS)` 决定。 |
| | | - `Load()` 遇到 `config.json` 不存在时,调用 `Default()` 并保存,因此新装 / 首次生成配置得到: |
| | | - macOS:`HotkeyVK=0x5C` |
| | | - Windows:`HotkeyVK=0x5C` |
| | | - `HotkeyMode=hold` |
| | | - `Load()` 遇到已有配置时,不再用 `Default()+Unmarshal`,而是用 `ExistingConfigFallback()+Unmarshal`: |
| | | - 已有配置缺 `HotkeyVK`:运行态 fallback 为旧默认 `0xA5`,不会静默拿到 R222 新默认。 |
| | | - 已有配置缺 `HotkeyMode`:保持既有 `hold` fallback,因为 R222 前旧默认也已经是 `hold`。 |
| | | - `config.Load()` 失败后的调用方兜底改为 `ExistingConfigFallback()`,避免异常配置由调用方绕过保护。 |
| | | |
| | | ## 平台差异 |
| | | |
| | | - macOS: |
| | | - 默认内部值使用 `0x5C`。 |
| | | - 既有 `hotkey_darwin.go` 已将 `0x5C` 映射到物理 keyCode `54`,即右 Command。 |
| | | - 展示名返回 `R-⌘`;正常文案用“右 Command / Windows 键不可用时可修改”提示兜底。 |
| | | - Windows: |
| | | - 默认内部值使用 `0x5C`,即 `VK_RWIN`。 |
| | | - 展示名返回 `R-Win`。 |
| | | - 本机未做 Windows 真机运行时验证;右 Windows 是否触发开始菜单 / 系统保留行为仍需 R222-7 QA 真机覆盖。 |
| | | - Linux / 非目标平台: |
| | | - 保留 legacy 默认 `0xA5`,避免 R222 主线需求扩大到未定义平台。 |
| | | |
| | | ## 配置保护策略 |
| | | |
| | | - 新默认只写入“配置文件不存在 / 首次生成配置”的路径。 |
| | | - 只要 `config.json` 存在: |
| | | - 已有旧默认 `0xA5` 不会变成 `0x5C`。 |
| | | - 用户自定义热键不会被覆盖。 |
| | | - 用户自定义 `tap` / `hold` 不会被覆盖。 |
| | | - 缺 `HotkeyVK` 的旧 / 异常配置不会借 R222 静默变成右 Command / 右 Windows。 |
| | | - 本轮不新增配置版本迁移,不做已有配置批量升级。 |
| | | |
| | | ## 验证命令与结果 |
| | | |
| | | ```bash |
| | | gofmt -w app.go internal/config/config.go internal/config/config_test.go internal/engine/engine.go internal/hotkey/hotkey.go internal/hotkey/hotkey_test.go services/engine_service.go |
| | | ``` |
| | | |
| | | 结果:PASS。 |
| | | |
| | | ```bash |
| | | go test ./internal/config ./internal/hotkey |
| | | ``` |
| | | |
| | | 结果:PASS。 |
| | | |
| | | ```bash |
| | | npm --prefix /Users/ar/Projects/PrivateVoice/03-O/C1.source/privatevoice.src/frontend run build |
| | | ``` |
| | | |
| | | 结果:PASS。存在既有 Svelte a11y warnings: |
| | | |
| | | - `src/components/update/UpdateDialog.svelte` |
| | | - `src/components/shared/ToggleSwitch.svelte` |
| | | |
| | | 上述文件本轮未修改。 |
| | | |
| | | ```bash |
| | | go test ./... |
| | | ``` |
| | | |
| | | 结果:PASS。存在既有 macOS SDK / onnxruntime link warnings,不影响退出码。 |
| | | |
| | | ```bash |
| | | git diff --check |
| | | ``` |
| | | |
| | | 结果:PASS。 |
| | | |
| | | ```bash |
| | | GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ go test -exec=/usr/bin/true ./... |
| | | ``` |
| | | |
| | | 结果:PASS。 |
| | | |
| | | 说明:曾并行运行 `go test ./...` 与前端 build,出现一次 embed 哈希文件瞬时不一致失败;随后按顺序先跑前端 build,再重跑 `go test ./...`,结果 PASS。有效门禁以顺序重跑结果为准。 |
| | | |
| | | ## 待审 diff 范围 |
| | | |
| | | ```bash |
| | | git diff --name-only |
| | | ``` |
| | | |
| | | 范围: |
| | | |
| | | - `C1.source/privatevoice.src/app.go` |
| | | - `C1.source/privatevoice.src/frontend/src/lib/i18n/en.json` |
| | | - `C1.source/privatevoice.src/frontend/src/lib/i18n/zh.json` |
| | | - `C1.source/privatevoice.src/frontend/src/lib/stores/config.ts` |
| | | - `C1.source/privatevoice.src/frontend/src/lib/stores/indicator.ts` |
| | | - `C1.source/privatevoice.src/internal/config/config.go` |
| | | - `C1.source/privatevoice.src/internal/config/config_test.go` |
| | | - `C1.source/privatevoice.src/internal/engine/engine.go` |
| | | - `C1.source/privatevoice.src/internal/hotkey/hotkey.go` |
| | | - `C1.source/privatevoice.src/internal/hotkey/hotkey_test.go` |
| | | - `C1.source/privatevoice.src/services/engine_service.go` |
| | | - `CODEGRAPH.md` |
| | | - `K2.项目管理/BR-HOTKEY-DEFAULTS-分支创建记录.md` |
| | | - `K2.项目管理/R222-5-开发自查.md` |
| | | |
| | | ## 未覆盖风险 |
| | | |
| | | - 未做 Windows 真机右 Windows 运行时验证;只能证明 Windows 交叉编译 / 测试通过和 `0x5C` 映射正确。R222-7 必须覆盖真实 Windows 行为、系统保留键和 Start 菜单风险。 |
| | | - 未做 macOS GUI 手动长按录音 smoke;本轮通过单元测试和现有热键物理映射证明实现面,真实右 Command 长按仍需 R222-7 QA。 |
| | | - 无右侧修饰键、远程桌面、外接键盘映射异常和快捷键冲突仍需 QA 记录;本轮保留修改入口和提示文案,不静默 fallback 到左侧键。 |
| | | |
| | | ## 结论 |
| | | |
| | | `PM_NOTIFY_READY R222-5` |
| | | |
| | | 建议 PMO 派发 R222-6 正式代码审核,Review Gate ID:`R222-6-GATE-20260706.0151`。本结论不代表 R222-6 代码审核 PASS、R222-7 QA PASS、发布完成或主线合并许可。 |