diff --git a/src/components/CloudAccountList.tsx b/src/components/CloudAccountList.tsx index 454ed67..d4e5a16 100644 --- a/src/components/CloudAccountList.tsx +++ b/src/components/CloudAccountList.tsx @@ -41,6 +41,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import { getLocalizedErrorMessage } from '@/utils/errorMessages'; // ... (existing code: imports and comments) @@ -78,7 +79,7 @@ export function CloudAccountList() { onError: (err) => { toast({ title: t('cloud.toast.addFailed.title'), - description: err.message, + description: getLocalizedErrorMessage(err, t), variant: 'destructive', }); }, @@ -133,7 +134,7 @@ export function CloudAccountList() { onError: (err) => toast({ title: t('cloud.toast.switchFailed'), - description: err.message, + description: getLocalizedErrorMessage(err, t), variant: 'destructive', }), }, @@ -199,7 +200,7 @@ export function CloudAccountList() { onError: (err) => { toast({ title: t('cloud.toast.syncFailed.title'), - description: err.message, + description: getLocalizedErrorMessage(err, t), variant: 'destructive', }); }, diff --git a/src/ipc/process/handler.ts b/src/ipc/process/handler.ts index 2104624..1e6d369 100644 --- a/src/ipc/process/handler.ts +++ b/src/ipc/process/handler.ts @@ -1,6 +1,6 @@ import { exec, execSync } from 'child_process'; import { promisify } from 'util'; -import findProcess from 'find-process'; +import findProcess, { ProcessInfo } from 'find-process'; import { getAntigravityExecutablePath, isWsl } from '../../utils/paths'; import { logger } from '../../utils/logger'; @@ -51,6 +51,17 @@ function isHelperProcess(name: string, cmd: string): boolean { return false; } +function isPgrepNoMatchError(error: unknown): boolean { + if (!(error instanceof Error)) { + return false; + } + + const message = error.message; + const hasPgrep = message.includes('pgrep -xi Antigravity'); + const code = (error as { code?: number }).code; + return hasPgrep && code === 1; +} + /** * Checks if the Antigravity process is running. * Uses find-process package for robust cross-platform process detection. @@ -63,7 +74,16 @@ export async function isProcessRunning(): Promise { // Use find-process to search for Antigravity processes // 'name' search type matches process name - const processes = await findProcess('name', 'Antigravity', true); + let processes: ProcessInfo[] = []; + try { + processes = await findProcess('name', 'Antigravity', true); + } catch (error) { + if (isPgrepNoMatchError(error)) { + logger.debug('No Antigravity process found (pgrep returned 1)'); + return false; + } + throw error; + } logger.debug(`Found ${processes.length} processes matching 'Antigravity'`); diff --git a/src/localization/i18n.ts b/src/localization/i18n.ts index 3ad4af2..59bc0e2 100644 --- a/src/localization/i18n.ts +++ b/src/localization/i18n.ts @@ -34,6 +34,14 @@ i18n }, error: { generic: 'An unexpected error occurred.', + keychainUnavailable: 'Keychain is unavailable.', + keychainHint: { + translocation: + 'Detected macOS App Translocation. Move the app to /Applications and reopen.', + keychainDenied: + 'Keychain access denied. The app may be unsigned; see README for the self-signing workaround.', + signNotarize: 'Please use a signed and notarized build when available.', + }, }, nav: { accounts: 'Accounts', @@ -310,6 +318,12 @@ i18n }, error: { generic: '发生未知错误。', + keychainUnavailable: 'Keychain 不可用。', + keychainHint: { + translocation: '检测到 macOS App Translocation。请将应用移动到 /Applications 后重试。', + keychainDenied: 'Keychain 访问被拒绝。应用可能未签名,可参考 README 的“macOS 自签”说明。', + signNotarize: '请在可用时使用已签名并公证的版本。', + }, }, nav: { accounts: '账号', diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts new file mode 100644 index 0000000..55efff7 --- /dev/null +++ b/src/utils/errorMessages.ts @@ -0,0 +1,39 @@ +import type { TFunction } from 'i18next'; + +const KEYCHAIN_ERROR_CODE = 'ERR_KEYCHAIN_UNAVAILABLE'; +const KEYCHAIN_HINT_TRANSLOCATION = 'HINT_APP_TRANSLOCATION'; +const KEYCHAIN_HINT_KEYCHAIN_DENIED = 'HINT_KEYCHAIN_DENIED'; +const KEYCHAIN_HINT_SIGN_NOTARIZE = 'HINT_SIGN_NOTARIZE'; + +const KEYCHAIN_HINT_I18N_MAP: Record = { + [KEYCHAIN_HINT_TRANSLOCATION]: 'error.keychainHint.translocation', + [KEYCHAIN_HINT_KEYCHAIN_DENIED]: 'error.keychainHint.keychainDenied', + [KEYCHAIN_HINT_SIGN_NOTARIZE]: 'error.keychainHint.signNotarize', +}; + +function resolveKeychainMessage(hintCode: string | undefined, t: TFunction): string { + const base = t('error.keychainUnavailable'); + if (!hintCode) { + return base; + } + + const hintKey = KEYCHAIN_HINT_I18N_MAP[hintCode]; + if (!hintKey) { + return base; + } + + return `${base} ${t(hintKey)}`; +} + +export function getLocalizedErrorMessage(error: unknown, t: TFunction): string { + if (error instanceof Error) { + const rawMessage = error.message; + const [code, hint] = rawMessage.split('|'); + if (code === KEYCHAIN_ERROR_CODE) { + return resolveKeychainMessage(hint, t); + } + return rawMessage; + } + + return String(error); +} diff --git a/src/utils/security.ts b/src/utils/security.ts index 74a9409..bda7c3e 100644 --- a/src/utils/security.ts +++ b/src/utils/security.ts @@ -1,13 +1,43 @@ import keytar from 'keytar'; import crypto from 'crypto'; +import { app } from 'electron'; import { logger } from './logger'; const SERVICE_NAME = 'AntigravityManager'; const ACCOUNT_NAME = 'MasterKey'; +const KEYCHAIN_ERROR_CODE = 'ERR_KEYCHAIN_UNAVAILABLE'; +const KEYCHAIN_HINT_TRANSLOCATION = 'HINT_APP_TRANSLOCATION'; +const KEYCHAIN_HINT_KEYCHAIN_DENIED = 'HINT_KEYCHAIN_DENIED'; +const KEYCHAIN_HINT_SIGN_NOTARIZE = 'HINT_SIGN_NOTARIZE'; // Cache the key in memory to avoid frequent system calls let cachedMasterKey: Buffer | null = null; +function buildKeychainAccessHint(error: unknown): string | null { + if (process.platform !== 'darwin') { + return null; + } + + let appPath = ''; + try { + appPath = app.getAppPath(); + } catch { + appPath = ''; + } + + const isTranslocated = appPath.includes('/AppTranslocation/'); + if (isTranslocated) { + return KEYCHAIN_HINT_TRANSLOCATION; + } + + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.toLowerCase().includes('keychain')) { + return KEYCHAIN_HINT_KEYCHAIN_DENIED; + } + + return KEYCHAIN_HINT_SIGN_NOTARIZE; +} + async function getOrGenerateMasterKey(): Promise { if (cachedMasterKey) return cachedMasterKey; @@ -25,16 +55,17 @@ async function getOrGenerateMasterKey(): Promise { cachedMasterKey = Buffer.from(hexKey, 'hex'); return cachedMasterKey; } catch (error) { + const hint = buildKeychainAccessHint(error); logger.error('Security: Failed to access keychain/credential manager', error); // Fallback? If we can't store the key, we can't persistently encrypt. // For now, throw to prevent data loss (better not to write than to write something we can't decrypt later or write plain text when promised encrypted) - throw new Error('Key management system unavailable'); + const message = hint ? `${KEYCHAIN_ERROR_CODE}|${hint}` : KEYCHAIN_ERROR_CODE; + throw new Error(message); } } const ALGORITHM = 'aes-256-gcm'; const IV_LENGTH = 16; -const AUTH_TAG_LENGTH = 16; /** * Encrypts a string using AES-256-GCM.