diff --git a/src-tauri/src/commands/maa_core.rs b/src-tauri/src/commands/maa_core.rs index 6b8d1e00..63db9e9e 100644 --- a/src-tauri/src/commands/maa_core.rs +++ b/src-tauri/src/commands/maa_core.rs @@ -575,6 +575,17 @@ pub async fn connect_controller_impl( let uuid_str = uuid.as_deref().unwrap_or(""); Controller::new_playcover(address, uuid_str).map_err(|e| e.to_string())? } + ControllerConfig::MacOS { + handle, + screencap_method, + input_method, + .. + } => { + let window_id = u32::try_from(*handle) + .map_err(|_| format!("Invalid macOS window handle '{}'", handle))?; + Controller::new_macos(window_id, *screencap_method, *input_method) + .map_err(|e| e.to_string())? + } ControllerConfig::Dummy { display_short_side, .. } => { @@ -627,6 +638,9 @@ pub async fn connect_controller_impl( | ControllerConfig::PlayCover { display_short_side, .. } + | ControllerConfig::MacOS { + display_short_side, .. + } | ControllerConfig::Dummy { display_short_side, .. } => display_short_side.unwrap_or(720), diff --git a/src-tauri/src/commands/types.rs b/src-tauri/src/commands/types.rs index f045b9e1..aa5c4e86 100644 --- a/src-tauri/src/commands/types.rs +++ b/src-tauri/src/commands/types.rs @@ -104,6 +104,13 @@ pub enum ControllerConfig { #[serde(default)] display_short_side: Option, }, + MacOS { + handle: u64, + screencap_method: u64, + input_method: u64, + #[serde(default)] + display_short_side: Option, + }, /// 空 controller:截图返回纯黑图、输入 no-op。 /// 用于在游戏未连接/已关闭时执行不依赖游戏画面的 MXU 特殊任务。 Dummy { diff --git a/src/components/ConnectionPanel.tsx b/src/components/ConnectionPanel.tsx index fae77c7d..9b5d4984 100644 --- a/src/components/ConnectionPanel.tsx +++ b/src/components/ConnectionPanel.tsx @@ -24,7 +24,12 @@ import type { AdbDevice, Win32Window, ControllerConfig } from '@/types/maa'; import type { ControllerItem, ResourceItem } from '@/types/interface'; import { computeResourcePaths } from '@/utils/resourcePath'; import { getProcessNameFromPath } from '@/utils/paths'; -import { parseWin32ScreencapMethod, parseWin32InputMethod } from '@/types/maa'; +import { + parseWin32ScreencapMethod, + parseWin32InputMethod, + parseMacOSScreencapMethod, + parseMacOSInputMethod, +} from '@/types/maa'; import { getInterfaceLangKey } from '@/i18n'; import { generateId } from '@/stores/helpers'; import { @@ -184,6 +189,16 @@ export function ConnectionPanel() { const currentController = controllers.find((c) => c.name === currentControllerName) || controllers[0]; const controllerType = currentController?.type; + const isDesktopWindowController = + controllerType === 'Win32' || controllerType === 'Gamepad' || controllerType === 'MacOS'; + const desktopWindowClassRegex = + controllerType === 'MacOS' + ? currentController?.macos?.class_regex + : currentController?.win32?.class_regex || currentController?.gamepad?.class_regex; + const desktopWindowRegex = + controllerType === 'MacOS' + ? currentController?.macos?.window_regex + : currentController?.win32?.window_regex || currentController?.gamepad?.window_regex; // 获取资源列表 const allResources = projectInterface?.resource || []; @@ -315,6 +330,7 @@ export function ConnectionPanel() { controllerType === 'Adb' || controllerType === 'Win32' || controllerType === 'Gamepad' || + controllerType === 'MacOS' || controllerType === 'WlRoots'; // 记录上一次的控制器名称,用于检测切换 @@ -348,7 +364,7 @@ export function ConnectionPanel() { const hasHistoricalDevice = savedDevice && ((controllerType === 'Adb' && savedDevice.adbDeviceName) || - ((controllerType === 'Win32' || controllerType === 'Gamepad') && savedDevice.windowName) || + (isDesktopWindowController && savedDevice.windowName) || (controllerType === 'WlRoots' && savedDevice.wlrSocketPath) || (controllerType === 'PlayCover' && savedDevice.playcoverAddress)); @@ -425,12 +441,11 @@ export function ConnectionPanel() { // 有保存设备但匹配失败,显示下拉框让用户选择 setShowDeviceDropdown(true); } - } else if (controllerType === 'Win32' || controllerType === 'Gamepad') { - const classRegex = - currentController.win32?.class_regex || currentController.gamepad?.class_regex; - const windowRegex = - currentController.win32?.window_regex || currentController.gamepad?.window_regex; - const windows = await maaService.findWin32Windows(classRegex, windowRegex); + } else if (isDesktopWindowController) { + const windows = await maaService.findWin32Windows( + desktopWindowClassRegex, + desktopWindowRegex, + ); setCachedWin32Windows(windows); // 自动连接策略(与 Adb 一致) @@ -577,6 +592,16 @@ export function ConnectionPanel() { }; deviceName = selectedWindow.window_name || selectedWindow.class_name; targetType = 'window'; + } else if (controllerType === 'MacOS' && selectedWindow) { + config = { + type: 'MacOS', + handle: selectedWindow.handle, + screencap_method: parseMacOSScreencapMethod(currentController.macos?.screencap || ''), + input_method: parseMacOSInputMethod(currentController.macos?.input || ''), + display_short_side: currentController?.display_short_side, + }; + deviceName = selectedWindow.window_name || selectedWindow.class_name; + targetType = 'window'; } else if (controllerType === 'WlRoots' && selectedWlrootsSocket) { config = { type: 'WlRoots', @@ -606,7 +631,7 @@ export function ConnectionPanel() { targetType = 'window'; } else { throw new Error( - controllerType === 'Win32' || controllerType === 'Gamepad' + isDesktopWindowController ? t('controller.selectWindow') : t('controller.selectDevice'), ); @@ -735,6 +760,7 @@ export function ConnectionPanel() { case 'Win32': case 'WlRoots': return ; + case 'MacOS': case 'PlayCover': return ; case 'Gamepad': @@ -758,7 +784,7 @@ export function ConnectionPanel() { } return t('controller.selectDevice'); } - if (controllerType === 'Win32' || controllerType === 'Gamepad') { + if (isDesktopWindowController) { if (selectedWindow) { return selectedWindow.window_name || selectedWindow.class_name; } @@ -904,6 +930,14 @@ export function ConnectionPanel() { keyboard_method: parseWin32InputMethod(currentController?.win32?.keyboard || ''), display_short_side: currentController?.display_short_side, }; + } else if (controllerType === 'MacOS') { + config = { + type: 'MacOS', + handle: win.handle, + screencap_method: parseMacOSScreencapMethod(currentController?.macos?.screencap || ''), + input_method: parseMacOSInputMethod(currentController?.macos?.input || ''), + display_short_side: currentController?.display_short_side, + }; } else { config = { type: 'Gamepad', @@ -914,8 +948,10 @@ export function ConnectionPanel() { await connectControllerInternal(config, win.window_name || win.class_name, 'window'); - // 连接成功后异步获取进程路径(Win32 和 Gamepad 都基于窗口句柄) - void fetchAndStoreProcessPath(win.handle); + // 连接成功后异步获取进程路径(仅 Windows 窗口句柄支持该查询) + if (controllerType === 'Win32' || controllerType === 'Gamepad') { + void fetchAndStoreProcessPath(win.handle); + } } catch (err) { setDeviceError(err instanceof Error ? err.message : t('controller.connectionFailed')); setIsConnected(false); @@ -1003,12 +1039,11 @@ export function ConnectionPanel() { if (devices.length > 0) { setShowDeviceDropdown(true); } - } else if (controllerType === 'Win32' || controllerType === 'Gamepad') { - const classRegex = - currentController.win32?.class_regex || currentController.gamepad?.class_regex; - const windowRegex = - currentController.win32?.window_regex || currentController.gamepad?.window_regex; - const windows = await maaService.findWin32Windows(classRegex, windowRegex); + } else if (isDesktopWindowController) { + const windows = await maaService.findWin32Windows( + desktopWindowClassRegex, + desktopWindowRegex, + ); setCachedWin32Windows(windows); // 尝试匹配保存的窗口名称 @@ -1090,7 +1125,7 @@ export function ConnectionPanel() { return []; } - if (controllerType === 'Win32' || controllerType === 'Gamepad') { + if (isDesktopWindowController) { // 如果缓存中有窗口,使用缓存列表 if (cachedWin32Windows.length > 0) { return cachedWin32Windows.map((window) => ({ @@ -1154,7 +1189,7 @@ export function ConnectionPanel() { // 判断是否可以连接 const canConnect = () => { if (controllerType === 'Adb') return !!selectedAdbDevice; - if (controllerType === 'Win32' || controllerType === 'Gamepad') return !!selectedWindow; + if (isDesktopWindowController) return !!selectedWindow; if (controllerType === 'WlRoots') return !!selectedWlrootsSocket; if (controllerType === 'PlayCover') return playcoverAddress.trim().length > 0; return false; @@ -1505,7 +1540,7 @@ export function ConnectionPanel() {
{isSearching ? t('common.loading') - : controllerType === 'Win32' || controllerType === 'Gamepad' + : isDesktopWindowController ? t('controller.noWindows') : t('controller.noDevices')}
@@ -1526,7 +1561,7 @@ export function ConnectionPanel() { : 'hover:bg-bg-hover hover:border-accent', )} title={ - controllerType === 'Win32' || controllerType === 'Gamepad' + isDesktopWindowController ? t('controller.refreshWindows') : t('controller.refreshDevices') } diff --git a/src/components/DeviceSelector.tsx b/src/components/DeviceSelector.tsx index 3b378533..3a55f6ec 100644 --- a/src/components/DeviceSelector.tsx +++ b/src/components/DeviceSelector.tsx @@ -19,7 +19,12 @@ import { maaService } from '@/services/maaService'; import { useAppStore } from '@/stores/appStore'; import type { AdbDevice, Win32Window, ControllerConfig } from '@/types/maa'; import type { ControllerItem } from '@/types/interface'; -import { parseWin32ScreencapMethod, parseWin32InputMethod } from '@/types/maa'; +import { + parseWin32ScreencapMethod, + parseWin32InputMethod, + parseMacOSScreencapMethod, + parseMacOSInputMethod, +} from '@/types/maa'; import { loggers } from '@/utils/logger'; const log = loggers.device; @@ -101,6 +106,16 @@ export function DeviceSelector({ }, [showDropdown]); const controllerType = controllerDef.type; + const isDesktopWindowController = + controllerType === 'Win32' || controllerType === 'Gamepad' || controllerType === 'MacOS'; + const desktopWindowClassRegex = + controllerType === 'MacOS' + ? controllerDef.macos?.class_regex + : controllerDef.win32?.class_regex || controllerDef.gamepad?.class_regex; + const desktopWindowRegex = + controllerType === 'MacOS' + ? controllerDef.macos?.window_regex + : controllerDef.win32?.window_regex || controllerDef.gamepad?.window_regex; // PlayCover 地址输入 const [playcoverAddress, setPlaycoverAddress] = useState('127.0.0.1:1717'); @@ -143,6 +158,7 @@ export function DeviceSelector({ controllerType === 'Adb' || controllerType === 'Win32' || controllerType === 'WlRoots' || + controllerType === 'MacOS' || controllerType === 'Gamepad'; // 初始化 MaaFramework(如果还没初始化) @@ -180,11 +196,11 @@ export function DeviceSelector({ if (devices.length > 0) { setShowDropdown(true); } - } else if (controllerType === 'Win32' || controllerType === 'Gamepad') { - const classRegex = controllerDef.win32?.class_regex || controllerDef.gamepad?.class_regex; - const windowRegex = - controllerDef.win32?.window_regex || controllerDef.gamepad?.window_regex; - const windows = await maaService.findWin32Windows(classRegex, windowRegex); + } else if (isDesktopWindowController) { + const windows = await maaService.findWin32Windows( + desktopWindowClassRegex, + desktopWindowRegex, + ); setCachedWin32Windows(windows); if (windows.length === 1) { setSelectedWindow(windows[0]); @@ -246,6 +262,14 @@ export function DeviceSelector({ keyboard_method: parseWin32InputMethod(controllerDef.win32?.keyboard || ''), display_short_side: controllerDef.display_short_side, }; + } else if (controllerType === 'MacOS' && selectedWindow) { + config = { + type: 'MacOS', + handle: selectedWindow.handle, + screencap_method: parseMacOSScreencapMethod(controllerDef.macos?.screencap || ''), + input_method: parseMacOSInputMethod(controllerDef.macos?.input || ''), + display_short_side: controllerDef.display_short_side, + }; } else if (controllerType === 'WlRoots' && selectedWlrootsSocket) { config = { type: 'WlRoots', @@ -276,7 +300,7 @@ export function DeviceSelector({ if (controllerType === 'Adb' && selectedAdbDevice) { deviceName = selectedAdbDevice.name || selectedAdbDevice.address; targetType = 'device'; - } else if ((controllerType === 'Win32' || controllerType === 'Gamepad') && selectedWindow) { + } else if (isDesktopWindowController && selectedWindow) { deviceName = selectedWindow.window_name || selectedWindow.class_name; targetType = 'window'; } else if (controllerType === 'WlRoots' && selectedWlrootsSocket) { @@ -317,7 +341,7 @@ export function DeviceSelector({ if (controllerType === 'Adb' && selectedAdbDevice) { return `${selectedAdbDevice.name} (${selectedAdbDevice.address})`; } - if ((controllerType === 'Win32' || controllerType === 'Gamepad') && selectedWindow) { + if (isDesktopWindowController && selectedWindow) { return selectedWindow.window_name || selectedWindow.class_name; } if (controllerType === 'WlRoots' && selectedWlrootsSocket) { @@ -396,6 +420,14 @@ export function DeviceSelector({ keyboard_method: parseWin32InputMethod(controllerDef.win32?.keyboard || ''), display_short_side: controllerDef.display_short_side, }; + } else if (controllerType === 'MacOS') { + config = { + type: 'MacOS', + handle: win.handle, + screencap_method: parseMacOSScreencapMethod(controllerDef.macos?.screencap || ''), + input_method: parseMacOSInputMethod(controllerDef.macos?.input || ''), + display_short_side: controllerDef.display_short_side, + }; } else { config = { type: 'Gamepad', @@ -470,7 +502,7 @@ export function DeviceSelector({ onClick: () => handleSelectAdbDevice(device), })); } - if (controllerType === 'Win32' || controllerType === 'Gamepad') { + if (isDesktopWindowController) { return cachedWin32Windows.map((window) => ({ id: String(window.handle), name: window.window_name || '(无标题)', @@ -494,7 +526,7 @@ export function DeviceSelector({ // 判断是否可以连接 const canConnect = () => { if (controllerType === 'Adb') return !!selectedAdbDevice; - if (controllerType === 'Win32' || controllerType === 'Gamepad') return !!selectedWindow; + if (isDesktopWindowController) return !!selectedWindow; if (controllerType === 'WlRoots') return !!selectedWlrootsSocket; if (controllerType === 'PlayCover') return playcoverAddress.trim().length > 0; return false; @@ -510,6 +542,7 @@ export function DeviceSelector({ case 'Win32': case 'WlRoots': return ; + case 'MacOS': case 'PlayCover': return ; case 'Gamepad': @@ -530,6 +563,8 @@ export function DeviceSelector({ return t('controller.wlroots'); case 'PlayCover': return t('controller.playcover'); + case 'MacOS': + return t('controller.macos'); case 'Gamepad': return t('controller.gamepad'); default: @@ -672,7 +707,7 @@ export function DeviceSelector({ : 'hover:bg-bg-hover hover:border-accent', )} title={ - controllerType === 'Win32' || controllerType === 'Gamepad' + isDesktopWindowController ? t('controller.refreshWindows') : t('controller.refreshDevices') } diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index fbcb0f60..c3ff5d8c 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -19,9 +19,14 @@ import { getMxuSpecialTask, shouldSkipMxuScreenshot } from '@/types/specialTasks import { splitTasksIntoThreeSegments } from '@/utils/taskSegmentation'; import type { TaskConfig, ControllerConfig } from '@/types/maa'; import { normalizeAgentConfigs } from '@/types/interface'; -import { parseWin32ScreencapMethod, parseWin32InputMethod } from '@/types/maa'; +import { + parseWin32ScreencapMethod, + parseWin32InputMethod, + parseMacOSScreencapMethod, + parseMacOSInputMethod, +} from '@/types/maa'; import { SchedulePanel } from './SchedulePanel'; -import type { Instance, TaskItem } from '@/types/interface'; +import type { ControllerItem, Instance, TaskItem } from '@/types/interface'; import { resolveI18nText } from '@/services/contentResolver'; import { getInterfaceLangKey } from '@/i18n'; import { PermissionModal } from './toolbar/PermissionModal'; @@ -48,6 +53,53 @@ interface ToolbarProps { // 自动连接阶段 type AutoConnectPhase = 'idle' | 'searching' | 'connecting' | 'loading_resource'; +const isDesktopWindowControllerType = (controllerType: string | undefined) => + controllerType === 'Win32' || controllerType === 'Gamepad' || controllerType === 'MacOS'; + +const getDesktopWindowRegex = (controller: ControllerItem) => ({ + classRegex: + controller.type === 'MacOS' + ? controller.macos?.class_regex + : controller.win32?.class_regex || controller.gamepad?.class_regex, + windowRegex: + controller.type === 'MacOS' + ? controller.macos?.window_regex + : controller.win32?.window_regex || controller.gamepad?.window_regex, +}); + +const buildDesktopWindowControllerConfig = ( + controller: ControllerItem, + handle: number, +): ControllerConfig | null => { + if (controller.type === 'Win32') { + return { + type: 'Win32', + handle, + screencap_method: parseWin32ScreencapMethod(controller.win32?.screencap || ''), + mouse_method: parseWin32InputMethod(controller.win32?.mouse || ''), + keyboard_method: parseWin32InputMethod(controller.win32?.keyboard || ''), + display_short_side: controller.display_short_side, + }; + } + if (controller.type === 'MacOS') { + return { + type: 'MacOS', + handle, + screencap_method: parseMacOSScreencapMethod(controller.macos?.screencap || ''), + input_method: parseMacOSInputMethod(controller.macos?.input || ''), + display_short_side: controller.display_short_side, + }; + } + if (controller.type === 'Gamepad') { + return { + type: 'Gamepad', + handle, + display_short_side: controller.display_short_side, + }; + } + return null; +}; + export function Toolbar({ showAddPanel, onToggleAddPanel, className }: ToolbarProps) { const { t } = useTranslation(); const { @@ -410,7 +462,7 @@ export function Toolbar({ showAddPanel, onToggleAddPanel, className }: ToolbarPr const shouldWaitAfterPreActions = !!controller; if (shouldWaitAfterPreActions && controller) { const controllerType = controller.type; - const isWindowType = controllerType === 'Win32' || controllerType === 'Gamepad'; + const isWindowType = isDesktopWindowControllerType(controllerType); log.info(`实例 ${targetInstance.name}: 等待${isWindowType ? '窗口' : '设备'}就绪...`); if (isWindowType) { addLog(targetId, { @@ -441,11 +493,8 @@ export function Toolbar({ showAddPanel, onToggleAddPanel, className }: ToolbarPr } else { deviceFound = devices.length > 0; } - } else if (controllerType === 'Win32' || controllerType === 'Gamepad') { - const classRegex = - controller.win32?.class_regex || controller.gamepad?.class_regex; - const windowRegex = - controller.win32?.window_regex || controller.gamepad?.window_regex; + } else if (isDesktopWindowControllerType(controllerType)) { + const { classRegex, windowRegex } = getDesktopWindowRegex(controller); const windows = await maaService.findWin32Windows(classRegex, windowRegex); if (savedDevice?.windowName) { deviceFound = windows.some((w) => w.window_name === savedDevice.windowName); @@ -498,11 +547,8 @@ export function Toolbar({ showAddPanel, onToggleAddPanel, className }: ToolbarPr }), }); } - } else if (controllerType === 'Win32' || controllerType === 'Gamepad') { - const classRegex = - controller.win32?.class_regex || controller.gamepad?.class_regex; - const windowRegex = - controller.win32?.window_regex || controller.gamepad?.window_regex; + } else if (isDesktopWindowControllerType(controllerType)) { + const { classRegex, windowRegex } = getDesktopWindowRegex(controller); const windows = await maaService.findWin32Windows(classRegex, windowRegex); if (windows.length > 0) { addLog(targetId, { @@ -619,35 +665,15 @@ export function Toolbar({ showAddPanel, onToggleAddPanel, className }: ToolbarPr }; deviceName = matchedDevice.name || matchedDevice.address; targetType = 'device'; - } else if ( - (controllerType === 'Win32' || controllerType === 'Gamepad') && - savedDevice.windowName - ) { - const classRegex = controller.win32?.class_regex || controller.gamepad?.class_regex; - const windowRegex = - controller.win32?.window_regex || controller.gamepad?.window_regex; + } else if (isDesktopWindowControllerType(controllerType) && savedDevice.windowName) { + const { classRegex, windowRegex } = getDesktopWindowRegex(controller); const windows = await maaService.findWin32Windows(classRegex, windowRegex); const matchedWindow = windows.find((w) => w.window_name === savedDevice.windowName); if (!matchedWindow) { log.warn(`实例 ${targetInstance.name}: 未找到窗口 ${savedDevice.windowName}`); return false; } - if (controllerType === 'Win32') { - config = { - type: 'Win32', - handle: matchedWindow.handle, - screencap_method: parseWin32ScreencapMethod(controller.win32?.screencap || ''), - mouse_method: parseWin32InputMethod(controller.win32?.mouse || ''), - keyboard_method: parseWin32InputMethod(controller.win32?.keyboard || ''), - display_short_side: controller.display_short_side, - }; - } else { - config = { - type: 'Gamepad', - handle: matchedWindow.handle, - display_short_side: controller.display_short_side, - }; - } + config = buildDesktopWindowControllerConfig(controller, matchedWindow.handle); deviceName = matchedWindow.window_name || matchedWindow.class_name; targetType = 'window'; } else if (controllerType === 'WlRoots' && savedDevice.wlrSocketPath) { @@ -709,10 +735,8 @@ export function Toolbar({ showAddPanel, onToggleAddPanel, className }: ToolbarPr }; deviceName = firstDevice.name || firstDevice.address; targetType = 'device'; - } else if (controllerType === 'Win32' || controllerType === 'Gamepad') { - const classRegex = controller.win32?.class_regex || controller.gamepad?.class_regex; - const windowRegex = - controller.win32?.window_regex || controller.gamepad?.window_regex; + } else if (isDesktopWindowControllerType(controllerType)) { + const { classRegex, windowRegex } = getDesktopWindowRegex(controller); const windows = await maaService.findWin32Windows(classRegex, windowRegex); if (windows.length === 0) { log.warn(`实例 ${targetInstance.name}: 未搜索到任何窗口`); @@ -731,22 +755,7 @@ export function Toolbar({ showAddPanel, onToggleAddPanel, className }: ToolbarPr name: firstWindow.window_name || firstWindow.class_name, }), }); - if (controllerType === 'Win32') { - config = { - type: 'Win32', - handle: firstWindow.handle, - screencap_method: parseWin32ScreencapMethod(controller.win32?.screencap || ''), - mouse_method: parseWin32InputMethod(controller.win32?.mouse || ''), - keyboard_method: parseWin32InputMethod(controller.win32?.keyboard || ''), - display_short_side: controller.display_short_side, - }; - } else { - config = { - type: 'Gamepad', - handle: firstWindow.handle, - display_short_side: controller.display_short_side, - }; - } + config = buildDesktopWindowControllerConfig(controller, firstWindow.handle); deviceName = firstWindow.window_name || firstWindow.class_name; targetType = 'window'; } else if (controllerType === 'WlRoots') { diff --git a/src/components/connection/useDeviceConnection.ts b/src/components/connection/useDeviceConnection.ts index 0131eb96..4a96bbb2 100644 --- a/src/components/connection/useDeviceConnection.ts +++ b/src/components/connection/useDeviceConnection.ts @@ -3,7 +3,12 @@ import { useTranslation } from 'react-i18next'; import { maaService } from '@/services/maaService'; import { useAppStore } from '@/stores/appStore'; import type { AdbDevice, Win32Window, ControllerConfig } from '@/types/maa'; -import { parseWin32ScreencapMethod, parseWin32InputMethod } from '@/types/maa'; +import { + parseWin32ScreencapMethod, + parseWin32InputMethod, + parseMacOSScreencapMethod, + parseMacOSInputMethod, +} from '@/types/maa'; import type { ControllerItem } from '@/types/interface'; import { startGlobalCallbackListener, waitForCtrlResult } from './callbackCache'; @@ -46,6 +51,16 @@ export function useDeviceConnection({ const [playcoverAddress, setPlaycoverAddress] = useState( activeInstance?.savedDevice?.playcoverAddress || '127.0.0.1:1717', ); + const isDesktopWindowController = + controllerType === 'Win32' || controllerType === 'Gamepad' || controllerType === 'MacOS'; + const desktopWindowClassRegex = + controllerType === 'MacOS' + ? currentController?.macos?.class_regex + : currentController?.win32?.class_regex || currentController?.gamepad?.class_regex; + const desktopWindowRegex = + controllerType === 'MacOS' + ? currentController?.macos?.window_regex + : currentController?.win32?.window_regex || currentController?.gamepad?.window_regex; const deviceDropdownRef = useRef(null); const deviceMenuRef = useRef(null); @@ -121,12 +136,11 @@ export function useDeviceConnection({ } else if (devices.length > 0) { setShowDeviceDropdown(true); } - } else if (controllerType === 'Win32' || controllerType === 'Gamepad') { - const classRegex = - currentController.win32?.class_regex || currentController.gamepad?.class_regex; - const windowRegex = - currentController.win32?.window_regex || currentController.gamepad?.window_regex; - const windows = await maaService.findWin32Windows(classRegex, windowRegex); + } else if (isDesktopWindowController) { + const windows = await maaService.findWin32Windows( + desktopWindowClassRegex, + desktopWindowRegex, + ); setCachedWin32Windows(windows); let autoSelected: Win32Window | null = null; @@ -173,6 +187,9 @@ export function useDeviceConnection({ currentController, controllerType, activeInstance?.savedDevice, + isDesktopWindowController, + desktopWindowClassRegex, + desktopWindowRegex, setCachedAdbDevices, setCachedWin32Windows, setCachedWlrootsSockets, @@ -268,6 +285,14 @@ export function useDeviceConnection({ keyboard_method: parseWin32InputMethod(currentController?.win32?.keyboard || ''), display_short_side: currentController?.display_short_side, }; + } else if (controllerType === 'MacOS') { + config = { + type: 'MacOS', + handle: win.handle, + screencap_method: parseMacOSScreencapMethod(currentController?.macos?.screencap || ''), + input_method: parseMacOSInputMethod(currentController?.macos?.input || ''), + display_short_side: currentController?.display_short_side, + }; } else { config = { type: 'Gamepad', @@ -399,7 +424,7 @@ export function useDeviceConnection({ } return t('controller.selectDevice'); } - if (controllerType === 'Win32' || controllerType === 'Gamepad') { + if (isDesktopWindowController) { if (selectedWindow) { return selectedWindow.window_name || selectedWindow.class_name; } @@ -420,6 +445,7 @@ export function useDeviceConnection({ return t('controller.selectDevice'); }, [ controllerType, + isDesktopWindowController, selectedAdbDevice, selectedWindow, selectedWlrootsSocket, @@ -430,11 +456,18 @@ export function useDeviceConnection({ // 判断是否可以连接 const canConnect = useCallback(() => { if (controllerType === 'Adb') return !!selectedAdbDevice; - if (controllerType === 'Win32' || controllerType === 'Gamepad') return !!selectedWindow; + if (isDesktopWindowController) return !!selectedWindow; if (controllerType === 'WlRoots') return !!selectedWlrootsSocket; if (controllerType === 'PlayCover') return playcoverAddress.trim().length > 0; return false; - }, [controllerType, selectedAdbDevice, selectedWindow, selectedWlrootsSocket, playcoverAddress]); + }, [ + controllerType, + isDesktopWindowController, + selectedAdbDevice, + selectedWindow, + selectedWlrootsSocket, + playcoverAddress, + ]); return { // 状态 diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 3f6dc994..deb96168 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -365,6 +365,7 @@ export default { win32: 'Windows Window', wlroots: 'WlRoots (Linux)', playcover: 'PlayCover (macOS)', + macos: 'macOS Window', gamepad: 'Gamepad', connecting: 'Connecting...', connected: 'Connected', diff --git a/src/i18n/locales/ja-JP.ts b/src/i18n/locales/ja-JP.ts index d8201a00..b29bdb59 100644 --- a/src/i18n/locales/ja-JP.ts +++ b/src/i18n/locales/ja-JP.ts @@ -359,6 +359,7 @@ export default { win32: 'Windows ウィンドウ', wlroots: 'WlRoots (Linux)', playcover: 'PlayCover (macOS)', + macos: 'macOS ウィンドウ', gamepad: 'ゲームパッド', connecting: '接続中...', connected: '接続済み', diff --git a/src/i18n/locales/ko-KR.ts b/src/i18n/locales/ko-KR.ts index 2eceaef6..2d0b6d5f 100644 --- a/src/i18n/locales/ko-KR.ts +++ b/src/i18n/locales/ko-KR.ts @@ -357,6 +357,7 @@ export default { win32: 'Windows 창', wlroots: 'WlRoots (Linux)', playcover: 'PlayCover (macOS)', + macos: 'macOS 창', gamepad: '게임패드', connecting: '연결 중...', connected: '연결됨', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 19a43da6..02a210f5 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -355,6 +355,7 @@ export default { win32: 'Windows 窗口', wlroots: 'WlRoots (Linux)', playcover: 'PlayCover (macOS)', + macos: 'macOS 窗口', gamepad: '游戏手柄', connecting: '连接中...', connected: '已连接', diff --git a/src/i18n/locales/zh-TW.ts b/src/i18n/locales/zh-TW.ts index e0638a07..770a8c99 100644 --- a/src/i18n/locales/zh-TW.ts +++ b/src/i18n/locales/zh-TW.ts @@ -351,6 +351,7 @@ export default { win32: 'Windows 視窗', wlroots: 'WlRoots (Linux)', playcover: 'PlayCover (macOS)', + macos: 'macOS 視窗', gamepad: '遊戲控制器', connecting: '連接中...', connected: '已連接', diff --git a/src/services/interfaceLoader.ts b/src/services/interfaceLoader.ts index 0ee6bcba..853500c3 100644 --- a/src/services/interfaceLoader.ts +++ b/src/services/interfaceLoader.ts @@ -327,9 +327,10 @@ function getUnsupportedControllerTypes(os: string): Set { unsupported.add('Win32'); unsupported.add('Gamepad'); } - // 非 macOS 系统不支持 PlayCover + // 非 macOS 系统不支持 PlayCover 和 macOS 原生窗口控制器 if (!isMacOS) { unsupported.add('PlayCover'); + unsupported.add('MacOS'); } // 非 Linux 系统不支持 WlRoots if (!isLinux) { diff --git a/src/types/interface.ts b/src/types/interface.ts index 45ff1a1d..e4645b93 100644 --- a/src/types/interface.ts +++ b/src/types/interface.ts @@ -58,7 +58,7 @@ export function normalizeAgentConfigs( return Array.isArray(agent) ? agent : [agent]; } -export type ControllerType = 'Adb' | 'Win32' | 'WlRoots' | 'PlayCover' | 'Gamepad'; +export type ControllerType = 'Adb' | 'Win32' | 'WlRoots' | 'PlayCover' | 'MacOS' | 'Gamepad'; export interface ControllerItem { name: string; @@ -78,6 +78,7 @@ export interface ControllerItem { win32?: Win32Config; wlroots?: WlRootsConfig; playcover?: PlayCoverConfig; + macos?: MacOSConfig; gamepad?: GamepadConfig; } @@ -98,6 +99,13 @@ export interface PlayCoverConfig { uuid?: string; } +export interface MacOSConfig { + class_regex?: string; + window_regex?: string; + input?: string; + screencap?: string; +} + export interface GamepadConfig { class_regex?: string; window_regex?: string; diff --git a/src/types/maa.ts b/src/types/maa.ts index 465762f4..4d0e6ee9 100644 --- a/src/types/maa.ts +++ b/src/types/maa.ts @@ -53,6 +53,15 @@ export interface PlayCoverControllerConfig { display_short_side?: number; } +/** macOS 原生窗口控制器配置 */ +export interface MacOSControllerConfig { + type: 'MacOS'; + handle: number; + screencap_method: number; + input_method: number; + display_short_side?: number; +} + /** Gamepad 控制器配置 */ export interface GamepadControllerConfig { type: 'Gamepad'; @@ -75,6 +84,7 @@ export type ControllerConfig = | Win32ControllerConfig | WlRootsControllerConfig | PlayCoverControllerConfig + | MacOSControllerConfig | GamepadControllerConfig | DummyControllerConfig; @@ -151,6 +161,30 @@ export const Win32InputMethodNames: Record = { PostMessageWithWindowPos: Win32InputMethod.PostMessageWithWindowPos, }; +/** macOS 截图方法 */ +export const MacOSScreencapMethod = { + None: 0n, + ScreenCaptureKit: 1n, +} as const; + +/** macOS 输入方法 */ +export const MacOSInputMethod = { + None: 0n, + GlobalEvent: 1n, + PostToPid: 1n << 1n, +} as const; + +/** macOS 截图方法名称映射 */ +export const MacOSScreencapMethodNames: Record = { + ScreenCaptureKit: MacOSScreencapMethod.ScreenCaptureKit, +}; + +/** macOS 输入方法名称映射 */ +export const MacOSInputMethodNames: Record = { + GlobalEvent: MacOSInputMethod.GlobalEvent, + PostToPid: MacOSInputMethod.PostToPid, +}; + /** 解析 Win32 截图方法名称,支持单个字符串或字符串数组(数组时按位或合并) */ export function parseWin32ScreencapMethod(name: string | string[]): number { if (Array.isArray(name)) { @@ -178,6 +212,24 @@ export function parseWin32InputMethod(name: string): number { return Number(Win32InputMethod.Seize); } +/** 解析 macOS 截图方法名称 */ +export function parseMacOSScreencapMethod(name: string): number { + const method = MacOSScreencapMethodNames[name]; + if (method !== undefined) { + return Number(method); + } + return Number(MacOSScreencapMethod.ScreenCaptureKit); +} + +/** 解析 macOS 输入方法名称 */ +export function parseMacOSInputMethod(name: string): number { + const method = MacOSInputMethodNames[name]; + if (method !== undefined) { + return Number(method); + } + return Number(MacOSInputMethod.GlobalEvent); +} + /** Agent 配置(用于启动子进程) */ export interface AgentConfig { child_exec: string; diff --git a/src/utils/useMaaCallbackLogger.ts b/src/utils/useMaaCallbackLogger.ts index 8c58c60c..98ccdb3b 100644 --- a/src/utils/useMaaCallbackLogger.ts +++ b/src/utils/useMaaCallbackLogger.ts @@ -161,7 +161,7 @@ function inferCtrlInfoFromInstance(instanceId: string): { if (!controller) return { type: undefined, name: undefined }; // 根据控制器类型确定类型和名称 - if (controller.type === 'Win32' || controller.type === 'Gamepad') { + if (controller.type === 'Win32' || controller.type === 'Gamepad' || controller.type === 'MacOS') { return { type: 'window', name: savedDevice?.windowName }; } else if (controller.type === 'Adb') { return { type: 'device', name: savedDevice?.adbDeviceName };