Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions scripts/platform-guards.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';

test('only Mac platforms expose AppleScript actions', () => {
const source = fs.readFileSync('src/platform.ts', 'utf8').replace('export const isMacOS', 'globalThis.isMacOS');
for (const [platform, expected] of [['Win32', false], ['Linux x86_64', false], ['MacIntel', true], ['MacARM', true]]) {
const context = { navigator: { platform } };
vm.runInNewContext(source, context);
assert.equal(context.isMacOS, expected);
}
const context = {};
vm.runInNewContext(source, context);
assert.equal(context.isMacOS, false);
});

test('both app reload paths and the toolbar gate saved settings by platform', () => {
const app = fs.readFileSync('src/App.tsx', 'utf8');
assert.equal(app.match(/if \(isMacOS && settings.auto_reload_ide\)/g)?.length, 2);
const list = fs.readFileSync('src/components/AccountList.tsx', 'utf8');
assert.ok(list.includes('disabled={!isMacOS}'));
assert.ok(list.includes('aria-pressed={isMacOS && autoReload}'));
});
19 changes: 19 additions & 0 deletions src-tauri/src/ide_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const IDE_CONFIGS: &[(&str, &str)] = &[

/// 检测运行中的 IDE
pub fn detect_running_ides() -> Vec<String> {
if !cfg!(target_os = "macos") {
return Vec::new();
}
let mut running = Vec::new();

for &(name, bundle_id) in IDE_CONFIGS {
Expand Down Expand Up @@ -38,6 +41,9 @@ pub fn detect_running_ides() -> Vec<String> {

/// 重载指定 IDE
pub fn reload_ide(name: &str, use_window_reload: bool) -> Result<(), String> {
if !cfg!(target_os = "macos") {
return Err("IDE 自动重载仅支持 macOS,请手动重载 IDE".into());
}
// 杀死所有 codex 进程(排除 Codex Switcher 自身)
let script = r#"
for pid in $(pgrep -f codex 2>/dev/null); do
Expand Down Expand Up @@ -108,6 +114,9 @@ pub fn reload_ide(name: &str, use_window_reload: bool) -> Result<(), String> {

/// 移除 Codex App 的隔离属性 (修复闪退)
pub fn remove_quarantine() -> Result<(), String> {
if !cfg!(target_os = "macos") {
return Err("隔离属性修复仅适用于 macOS".into());
}
let script = r#"
do shell script "xattr -dr com.apple.quarantine /Applications/Codex.app" with administrator privileges
"#;
Expand All @@ -130,3 +139,13 @@ fn run_applescript(script: &str) -> Result<String, String> {

Ok(String::from_utf8_lossy(&output.stdout).to_string())
}

#[cfg(all(test, not(target_os = "macos")))]
mod tests {
#[test]
fn unsupported_actions_return_before_spawning_platform_tools() {
assert!(super::detect_running_ides().is_empty());
assert!(super::reload_ide("Visual Studio Code", false).unwrap_err().contains("macOS"));
assert!(super::remove_quarantine().unwrap_err().contains("macOS"));
}
}
3 changes: 3 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4087,6 +4087,9 @@ async fn fix_codex_quarantine(
/// 重载 IDE 窗口
#[tauri::command]
async fn reload_ide_windows(use_window_reload: bool) -> Result<Vec<String>, String> {
if !cfg!(target_os = "macos") {
return Err("IDE 自动重载仅支持 macOS,请手动重载 IDE".into());
}
let ides = ide_control::detect_running_ides();
let mut reloaded = Vec::new();

Expand Down
4 changes: 2 additions & 2 deletions src-tauri/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2267,7 +2267,7 @@ fn mark_current_banned(state: &ProxyState) {
println!("[Proxy] 账号 {} 已标记为封号", name);
let _ = state.app_handle.emit("proxy-account-banned", &name);
// macOS 系统通知(可配置)
if store.settings.notify_on_switch {
if cfg!(target_os = "macos") && store.settings.notify_on_switch {
let notify_name = name.clone();
std::thread::spawn(move || {
let _ = std::process::Command::new("osascript")
Expand Down Expand Up @@ -2581,7 +2581,7 @@ fn do_switch(state: &ProxyState, new_id: &str, reason: SwitchReason) -> Result<(
}

// macOS 系统通知(可配置)
if notify_enabled {
if cfg!(target_os = "macos") && notify_enabled {
let from = from_name.unwrap_or_else(|| "无".to_string());
let notify_msg = format!("{} → {}", from, to_name);
std::thread::spawn(move || {
Expand Down
5 changes: 3 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import CachePanel from './components/CachePanel';
import { ConfirmModal } from './components/ConfirmModal';
import { RelayImportConfirm } from './components/RelayImportConfirm';
import './App.css';
import { isMacOS } from './platform';

type PageType = 'dashboard' | 'accounts' | 'proxy' | 'routes' | 'stats' | 'cache' | 'skills' | 'settings';

Expand Down Expand Up @@ -187,7 +188,7 @@ function App() {
// 执行真正的切换逻辑
const performSwitch = async (id: string) => {
await switchTo(id);
if (settings.auto_reload_ide) {
if (isMacOS && settings.auto_reload_ide) {
setTimeout(async () => {
await reloadIdeWindows(false);
}, 300);
Expand Down Expand Up @@ -425,7 +426,7 @@ function App() {
onForceOverwriteDisk={async () => {
try {
await invoke<string>('force_overwrite_disk_with_current');
if (settings.auto_reload_ide) {
if (isMacOS && settings.auto_reload_ide) {
await reloadIdeWindows(false);
}
checkSyncStatus();
Expand Down
9 changes: 6 additions & 3 deletions src/components/AccountList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Zap, RefreshCw, ArrowLeftRight, Trash2, Clock, UploadCloud, Plus, Gauge
import { Account, AppSettings, LunaReserveWindow, RelayUsageCache, SparkWindows, effectiveKind } from '../hooks/useAccounts';
import { invoke } from '@tauri-apps/api/core';
import { openUrl } from '@tauri-apps/plugin-opener';
import { isMacOS } from '../platform';
import { AntigravityQuota, type AntigravityModelQuota } from './AntigravityQuota';
import { AgyRelayModelQuotas, RelayQuotaWindows } from './RelayQuotaWindows';
import { relayCurrentState } from '../utils/relayCurrent';
Expand Down Expand Up @@ -842,11 +843,13 @@ export function AccountList({
</div>
<div className="toolbar-spacer" />
<button
className={`toolbar-icon-btn ${autoReload ? 'active-reload' : ''}`}
className={`toolbar-icon-btn ${isMacOS && autoReload ? 'active-reload' : ''}`}
onClick={() => setAutoReload(!autoReload)}
title={autoReload ? '关闭自动重载 IDE' : '开启自动重载 IDE'}
disabled={!isMacOS}
aria-pressed={isMacOS && autoReload}
title={!isMacOS ? 'IDE 自动重载仅支持 macOS,请手动重载 IDE' : autoReload ? '关闭自动重载 IDE' : '开启自动重载 IDE'}
>
<Zap size={16} fill={autoReload ? "currentColor" : "none"} />
<Zap size={16} fill={isMacOS && autoReload ? "currentColor" : "none"} />
</button>
{onAddAccount && (
<button
Expand Down
4 changes: 3 additions & 1 deletion src/components/Proxy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { Copy, Check, Save } from 'lucide-react';
import './Proxy.css';
import { isMacOS } from '../platform';

interface ProxyStatus {
enabled: boolean;
Expand Down Expand Up @@ -412,7 +413,8 @@ export function Proxy() {
<label className="toggle">
<input
type="checkbox"
checked={settings?.notify_on_switch ?? false}
checked={isMacOS && (settings?.notify_on_switch ?? false)}
disabled={!isMacOS}
onChange={async e => {
if (!settings) return;
const updated = { ...settings, notify_on_switch: e.target.checked };
Expand Down
11 changes: 7 additions & 4 deletions src/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { invoke } from '@tauri-apps/api/core';
import { Palette, Server, Monitor, Wrench, Save, Github, Radio, Smartphone, Search, X } from 'lucide-react';
import { Account, effectiveKind } from '../hooks/useAccounts';
import './Settings.css';
import { isMacOS } from '../platform';

interface AppSettings {
auto_reload_ide: boolean;
Expand Down Expand Up @@ -460,19 +461,20 @@ export function Settings({ accounts = [], onSetSessionAnchor }: SettingsProps =
<div className="setting-item">
<div className="setting-info">
<span className="setting-label">自动重载 IDE</span>
<span className="setting-desc">切换账号后自动重载 IDE 以应用新的 Token</span>
<span className="setting-desc">{isMacOS ? '切换账号后自动重载 IDE 以应用新的 Token' : 'IDE 自动重载仅支持 macOS,请手动重载 IDE'}</span>
</div>
<label className="toggle">
<input
type="checkbox"
checked={settings.auto_reload_ide}
checked={isMacOS && settings.auto_reload_ide}
disabled={!isMacOS}
onChange={e => updateField('auto_reload_ide', e.target.checked)}
/>
<span className="toggle-slider"></span>
</label>
</div>

{settings.auto_reload_ide && (
{isMacOS && settings.auto_reload_ide && (
<>
<div className="setting-item sub-item">
<div className="setting-info">
Expand Down Expand Up @@ -772,7 +774,8 @@ export function Settings({ accounts = [], onSetSessionAnchor }: SettingsProps =
<button
className="action-button warning"
onClick={handleRepair}
disabled={repairing}
disabled={!isMacOS || repairing}
title={!isMacOS ? '隔离属性修复仅适用于 macOS' : undefined}
>
{repairing ? '修复中...' : '立即修复'}
</button>
Expand Down
2 changes: 2 additions & 0 deletions src/platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const platform = typeof navigator === 'undefined' ? '' : navigator.platform;
export const isMacOS = platform.startsWith('Mac');