Ariver
2026-05-18 6a74e8e739c3d2db2db737b1ecb0117eb88a4129
feat: show background activity status
9 files modified
77 ■■■■■ changed files
TODO.md 9 ●●●●● patch | view | raw | blame | history
VoiceSnapGo/app.go 2 ●●● patch | view | raw | blame | history
VoiceSnapGo/build/darwin/Info.plist 2 ●●● patch | view | raw | blame | history
VoiceSnapGo/frontend/src/components/settings/PermissionsPage.svelte 25 ●●●● patch | view | raw | blame | history
VoiceSnapGo/frontend/src/lib/i18n/en.json 7 ●●●●● patch | view | raw | blame | history
VoiceSnapGo/frontend/src/lib/i18n/zh.json 7 ●●●●● patch | view | raw | blame | history
VoiceSnapGo/internal/permissions/permissions.go 1 ●●●● patch | view | raw | blame | history
VoiceSnapGo/internal/permissions/permissions_darwin.go 23 ●●●●● patch | view | raw | blame | history
VoiceSnapGo/internal/permissions/permissions_other.go 1 ●●●● patch | view | raw | blame | history
TODO.md
@@ -17,6 +17,15 @@
## Done
- [2026-05-18] 将 macOS 后台活动/登录项纳入权限检查面板。
  - 说明: 系统设置里的 “VoiceSnap Local” 是本地稳定签名证书的开发者名,下面挂着 VoiceSnap 的登录项/后台活动。
  - 它不是右 Alt、录音、自动上屏的必需隐私权限,但会影响登录后是否自动在后台运行。
  - 面板新增“后台活动 / 登录项”,标为“可选”,不影响核心权限总绿灯。
  - build: `20260518.1604`
  - 输出: `VoiceSnapGo/build/local/arm64/VoiceSnap.app`
  - 输出: `VoiceSnapGo/build/local/arm64/VoiceSnap-2.1.1-build20260518.1604-arm64-local.dmg`
  - 兼容输出: `VoiceSnapGo/build/local/arm64/VoiceSnap-2.1.1-arm64-local.dmg` 已替换为同一份新包。
  - 已安装到 `/Applications/VoiceSnap.app`;启动日志验证 build `20260518.1604`。
- [2026-05-18] 修复右 Alt 事件进入但被 macOS 物理状态查询误判为未按下的问题。
  - 日志显示 R-Alt `flagsChanged` 事件正常进入,`flags=0x80140`,但 `physicalDown=false` 导致 `pollDown=false`。
  - 修复: 按事件 keyCode 区分左右 Alt,并用该事件的 modifier flags 判定按下/释放。
VoiceSnapGo/app.go
@@ -27,7 +27,7 @@
const (
    appVersion        = "2.1.1"
    appBuild          = "20260518.1554"
    appBuild          = "20260518.1604"
    appDisplayVersion = appVersion + " (build " + appBuild + ")"
    appName           = "VoiceSnap"
VoiceSnapGo/build/darwin/Info.plist
@@ -17,7 +17,7 @@
    <key>CFBundleShortVersionString</key>
    <string>2.1.1</string>
    <key>CFBundleVersion</key>
    <string>20260518.1554</string>
    <string>20260518.1604</string>
    <key>LSMinimumSystemVersion</key>
    <string>11.0</string>
    <key>NSMicrophoneUsageDescription</key>
VoiceSnapGo/frontend/src/components/settings/PermissionsPage.svelte
@@ -10,7 +10,7 @@
    required: boolean
  }
  const permissionOrder = ['microphone', 'accessibility', 'inputMonitoring']
  const permissionOrder = ['microphone', 'accessibility', 'inputMonitoring', 'backgroundActivity']
  let statuses = $state<PermissionStatus[]>([])
  let loading = $state(true)
@@ -56,13 +56,15 @@
  }
  function stateText(item: PermissionStatus) {
    if (!item.required) return item.granted ? t('permissions.enabled') : t('permissions.disabled')
    if (item.granted) return t('permissions.granted')
    if (loading) return t('permissions.checking')
    return t('permissions.missing')
  }
  function allGranted() {
    return statuses.length > 0 && statuses.every(item => item.granted)
    const required = statuses.filter(item => item.required)
    return required.length > 0 && required.every(item => item.granted)
  }
</script>
@@ -95,7 +97,12 @@
        </div>
        <div class="permission-info">
          <div class="permission-heading">
            <span class="permission-title">{titleFor(item.id)}</span>
            <span class="permission-title">
              {titleFor(item.id)}
              {#if !item.required}
                <span class="optional-badge">{t('permissions.optional')}</span>
              {/if}
            </span>
            <span class="permission-state" class:granted={item.granted}>{stateText(item)}</span>
          </div>
          <p class="permission-desc">{descFor(item.id)}</p>
@@ -250,10 +257,22 @@
  }
  .permission-title {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-size: var(--font-size-lg);
    font-weight: 650;
  }
  .optional-badge {
    border-radius: 999px;
    background: var(--color-bg-secondary);
    color: var(--color-secondary-label);
    font-size: var(--font-size-xs);
    font-weight: 600;
    padding: 2px 7px;
  }
  .permission-state {
    color: var(--color-red);
    font-size: var(--font-size-sm);
VoiceSnapGo/frontend/src/lib/i18n/en.json
@@ -54,6 +54,9 @@
    "grant": "Grant",
    "granted": "Granted",
    "missing": "Missing",
    "enabled": "Enabled",
    "disabled": "Disabled",
    "optional": "Optional",
    "checking": "Checking",
    "allGranted": "All permissions are ready",
    "needsAttention": "Some permissions need attention",
@@ -69,6 +72,10 @@
    "inputMonitoring": {
      "title": "Input Monitoring",
      "desc": "Listens for the global right Alt hotkey while other apps are in front."
    },
    "backgroundActivity": {
      "title": "Background Activity / Login Item",
      "desc": "Starts VoiceSnap after login and allows it to run as a menu bar app in the background; it does not affect the currently running voice input."
    }
  },
  "hotkeys": {
VoiceSnapGo/frontend/src/lib/i18n/zh.json
@@ -54,6 +54,9 @@
    "grant": "去授权",
    "granted": "已授权",
    "missing": "未授权",
    "enabled": "已启用",
    "disabled": "未启用",
    "optional": "可选",
    "checking": "检查中",
    "allGranted": "所有权限已就绪",
    "needsAttention": "有权限需要处理",
@@ -69,6 +72,10 @@
    "inputMonitoring": {
      "title": "输入监控",
      "desc": "用于在 VoiceSnap 窗口不可见、其他 App 正在前台时监听右 Alt 全局热键。"
    },
    "backgroundActivity": {
      "title": "后台活动 / 登录项",
      "desc": "用于登录系统后自动启动 VoiceSnap,并允许它作为菜单栏应用在后台运行;不影响当前已经打开的语音输入。"
    }
  },
  "hotkeys": {
VoiceSnapGo/internal/permissions/permissions.go
@@ -12,4 +12,5 @@
    IDMicrophone      = "microphone"
    IDAccessibility   = "accessibility"
    IDInputMonitoring = "inputMonitoring"
    IDBackground      = "backgroundActivity"
)
VoiceSnapGo/internal/permissions/permissions_darwin.go
@@ -42,7 +42,10 @@
*/
import "C"
import "os/exec"
import (
    "os/exec"
    "voicesnap/internal/startup"
)
// Statuses returns the current macOS privacy permission state.
func Statuses() []Status {
@@ -65,6 +68,12 @@
            State:    grantedState(C.inputMonitoringGranted() != 0),
            Required: true,
        },
        {
            ID:       IDBackground,
            Granted:  startup.IsEnabled(),
            State:    enabledState(startup.IsEnabled()),
            Required: false,
        },
    }
}
@@ -80,6 +89,9 @@
        openSettings(id)
    case IDInputMonitoring:
        C.requestInputMonitoring()
        openSettings(id)
    case IDBackground:
        _ = startup.SetEnabled(true)
        openSettings(id)
    }
}
@@ -110,6 +122,13 @@
    return "denied"
}
func enabledState(enabled bool) string {
    if enabled {
        return "enabled"
    }
    return "disabled"
}
func openSettings(id string) {
    url := "x-apple.systempreferences:com.apple.preference.security"
    switch id {
@@ -119,6 +138,8 @@
        url = "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
    case IDInputMonitoring:
        url = "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent"
    case IDBackground:
        url = "x-apple.systempreferences:com.apple.LoginItems-Settings.extension"
    }
    _ = exec.Command("open", url).Start()
}
VoiceSnapGo/internal/permissions/permissions_other.go
@@ -7,6 +7,7 @@
        {ID: IDMicrophone, Granted: true, State: "granted", Required: true},
        {ID: IDAccessibility, Granted: true, State: "granted", Required: false},
        {ID: IDInputMonitoring, Granted: true, State: "granted", Required: false},
        {ID: IDBackground, Granted: true, State: "enabled", Required: false},
    }
}