fix(ovms): add platform check to prevent errors on non-Windows systems (#12125)

* fix(ovms): make ovms manager windows-only and lazy load it

Add platform check in OvmsManager constructor to throw error on non-Windows platforms
Lazy load ovmsManager instance and handle IPC registration only on Windows
Update will-quit handler to conditionally cleanup ovms resources

* feat(preload): add windows-only OVMS API and improve type safety

Extract OVMS API methods into a separate windowsOnlyApi object for better organization
Add explicit return type for getDeviceType method

* feat(system): add system utils and refine ovms support check

- Add new system utility functions for device type, hostname and CPU name
- Refactor OVMS support check to require both Windows and Intel CPU
- Update IPC handlers to use new system utils and provide proper OVMS fallbacks

* Revert "feat(preload): add windows-only OVMS API and improve type safety"

This reverts commit d7c5c2b9a4.

* feat(ovms): add support check for ovms provider

Add new IPC channel and handler to check if OVMS is supported on the current system. This replaces the previous device type and CPU name checks with a more maintainable solution.

* fix(OvmsManager): improve intel cpu check for ovms manager

Move isOvmsSupported check before class definition and update error message to reflect intel cpu requirement

* fix: use isOvmsSupported flag for ovms cleanup check

Replace platform check with feature flag to properly determine if ovms cleanup should run

* fix: improve warning message for undefined ovmsManager

* fix(system): handle edge cases in getCpuName function

Add error handling and null checks to prevent crashes when CPU information is unavailable

* feat(runtime): add ovms support check during app init

Add isOvmsSupported state to runtime store and check support status during app initialization. Move ovms support check from ProviderList component to useAppInit hook for centralized management.
This commit is contained in:
Phantom 2025-12-31 22:24:53 +08:00 committed by GitHub
parent bc9eeb9f30
commit 33cdcaa558
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 102 additions and 23 deletions

View File

@ -364,6 +364,7 @@ export enum IpcChannel {
OCR_ListProviders = 'ocr:list-providers',
// OVMS
Ovms_IsSupported = 'ovms:is-supported',
Ovms_AddModel = 'ovms:add-model',
Ovms_StopAddModel = 'ovms:stop-addmodel',
Ovms_GetModels = 'ovms:get-models',

View File

@ -37,7 +37,7 @@ import { versionService } from './services/VersionService'
import { windowService } from './services/WindowService'
import { initWebviewHotkeys } from './services/WebviewService'
import { runAsyncFunction } from './utils'
import { ovmsManager } from './services/OvmsManager'
import { isOvmsSupported } from './services/OvmsManager'
const logger = loggerService.withContext('MainEntry')
@ -158,7 +158,7 @@ if (!app.requestSingleInstanceLock()) {
registerShortcuts(mainWindow)
registerIpc(mainWindow, app)
await registerIpc(mainWindow, app)
localTransferService.startDiscovery({ resetList: true })
replaceDevtoolsFont(mainWindow)
@ -248,7 +248,14 @@ if (!app.requestSingleInstanceLock()) {
app.on('will-quit', async () => {
// 简单的资源清理,不阻塞退出流程
await ovmsManager.stopOvms()
if (isOvmsSupported) {
const { ovmsManager } = await import('./services/OvmsManager')
if (ovmsManager) {
await ovmsManager.stopOvms()
} else {
logger.warn('Unexpected behavior: undefined ovmsManager, but OVMS should be supported.')
}
}
try {
await mcpService.cleanup()

View File

@ -59,7 +59,7 @@ import NotificationService from './services/NotificationService'
import * as NutstoreService from './services/NutstoreService'
import ObsidianVaultService from './services/ObsidianVaultService'
import { ocrService } from './services/ocr/OcrService'
import { ovmsManager } from './services/OvmsManager'
import { isOvmsSupported } from './services/OvmsManager'
import powerMonitorService from './services/PowerMonitorService'
import { proxyManager } from './services/ProxyManager'
import { pythonService } from './services/PythonService'
@ -97,6 +97,7 @@ import {
untildify
} from './utils/file'
import { updateAppDataConfig } from './utils/init'
import { getCpuName, getDeviceType, getHostname } from './utils/system'
import { compress, decompress } from './utils/zip'
const logger = loggerService.withContext('IPC')
@ -120,7 +121,7 @@ function extractPluginError(error: unknown): PluginError | null {
return null
}
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
export async function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const appUpdater = new AppUpdater()
const notificationService = new NotificationService()
@ -498,9 +499,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.Zip_Decompress, (_, text: Buffer) => decompress(text))
// system
ipcMain.handle(IpcChannel.System_GetDeviceType, () => (isMac ? 'mac' : isWin ? 'windows' : 'linux'))
ipcMain.handle(IpcChannel.System_GetHostname, () => require('os').hostname())
ipcMain.handle(IpcChannel.System_GetCpuName, () => require('os').cpus()[0].model)
ipcMain.handle(IpcChannel.System_GetDeviceType, getDeviceType)
ipcMain.handle(IpcChannel.System_GetHostname, getHostname)
ipcMain.handle(IpcChannel.System_GetCpuName, getCpuName)
ipcMain.handle(IpcChannel.System_CheckGitBash, () => {
if (!isWin) {
return true // Non-Windows systems don't need Git Bash
@ -974,15 +975,36 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.OCR_ListProviders, () => ocrService.listProviderIds())
// OVMS
ipcMain.handle(IpcChannel.Ovms_AddModel, (_, modelName: string, modelId: string, modelSource: string, task: string) =>
ovmsManager.addModel(modelName, modelId, modelSource, task)
)
ipcMain.handle(IpcChannel.Ovms_StopAddModel, () => ovmsManager.stopAddModel())
ipcMain.handle(IpcChannel.Ovms_GetModels, () => ovmsManager.getModels())
ipcMain.handle(IpcChannel.Ovms_IsRunning, () => ovmsManager.initializeOvms())
ipcMain.handle(IpcChannel.Ovms_GetStatus, () => ovmsManager.getOvmsStatus())
ipcMain.handle(IpcChannel.Ovms_RunOVMS, () => ovmsManager.runOvms())
ipcMain.handle(IpcChannel.Ovms_StopOVMS, () => ovmsManager.stopOvms())
ipcMain.handle(IpcChannel.Ovms_IsSupported, () => isOvmsSupported)
if (isOvmsSupported) {
const { ovmsManager } = await import('./services/OvmsManager')
if (ovmsManager) {
ipcMain.handle(
IpcChannel.Ovms_AddModel,
(_, modelName: string, modelId: string, modelSource: string, task: string) =>
ovmsManager.addModel(modelName, modelId, modelSource, task)
)
ipcMain.handle(IpcChannel.Ovms_StopAddModel, () => ovmsManager.stopAddModel())
ipcMain.handle(IpcChannel.Ovms_GetModels, () => ovmsManager.getModels())
ipcMain.handle(IpcChannel.Ovms_IsRunning, () => ovmsManager.initializeOvms())
ipcMain.handle(IpcChannel.Ovms_GetStatus, () => ovmsManager.getOvmsStatus())
ipcMain.handle(IpcChannel.Ovms_RunOVMS, () => ovmsManager.runOvms())
ipcMain.handle(IpcChannel.Ovms_StopOVMS, () => ovmsManager.stopOvms())
} else {
logger.error('Unexpected behavior: undefined ovmsManager, but OVMS should be supported.')
}
} else {
const fallback = () => {
throw new Error('OVMS is only supported on Windows with intel CPU.')
}
ipcMain.handle(IpcChannel.Ovms_AddModel, fallback)
ipcMain.handle(IpcChannel.Ovms_StopAddModel, fallback)
ipcMain.handle(IpcChannel.Ovms_GetModels, fallback)
ipcMain.handle(IpcChannel.Ovms_IsRunning, fallback)
ipcMain.handle(IpcChannel.Ovms_GetStatus, fallback)
ipcMain.handle(IpcChannel.Ovms_RunOVMS, fallback)
ipcMain.handle(IpcChannel.Ovms_StopOVMS, fallback)
}
// CherryAI
ipcMain.handle(IpcChannel.Cherryai_GetSignature, (_, params) => generateSignature(params))

View File

@ -3,6 +3,8 @@ import { homedir } from 'node:os'
import { promisify } from 'node:util'
import { loggerService } from '@logger'
import { isWin } from '@main/constant'
import { getCpuName } from '@main/utils/system'
import { HOME_CHERRY_DIR } from '@shared/config/constant'
import * as fs from 'fs-extra'
import * as path from 'path'
@ -11,6 +13,8 @@ const logger = loggerService.withContext('OvmsManager')
const execAsync = promisify(exec)
export const isOvmsSupported = isWin && getCpuName().toLowerCase().includes('intel')
interface OvmsProcess {
pid: number
path: string
@ -29,6 +33,12 @@ interface OvmsConfig {
class OvmsManager {
private ovms: OvmsProcess | null = null
constructor() {
if (!isOvmsSupported) {
throw new Error('OVMS Manager is only supported on Windows platform with Intel CPU.')
}
}
/**
* Recursively terminate a process and all its child processes
* @param pid Process ID to terminate
@ -563,4 +573,4 @@ class OvmsManager {
}
// Export singleton instance
export const ovmsManager = new OvmsManager()
export const ovmsManager = isOvmsSupported ? new OvmsManager() : undefined

19
src/main/utils/system.ts Normal file
View File

@ -0,0 +1,19 @@
import os from 'node:os'
import { isMac, isWin } from '@main/constant'
export const getDeviceType = () => (isMac ? 'mac' : isWin ? 'windows' : 'linux')
export const getHostname = () => os.hostname()
export const getCpuName = () => {
try {
const cpus = os.cpus()
if (!cpus || cpus.length === 0 || !cpus[0].model) {
return 'Unknown CPU'
}
return cpus[0].model
} catch {
return 'Unknown CPU'
}
}

View File

@ -340,6 +340,7 @@ const api = {
ipcRenderer.invoke(IpcChannel.VertexAI_ClearAuthCache, projectId, clientEmail)
},
ovms: {
isSupported: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.Ovms_IsSupported),
addModel: (modelName: string, modelId: string, modelSource: string, task: string) =>
ipcRenderer.invoke(IpcChannel.Ovms_AddModel, modelName, modelId, modelSource, task),
stopAddModel: () => ipcRenderer.invoke(IpcChannel.Ovms_StopAddModel),

View File

@ -10,7 +10,7 @@ import { useAppDispatch } from '@renderer/store'
import { useAppSelector } from '@renderer/store'
import { handleSaveData } from '@renderer/store'
import { selectMemoryConfig } from '@renderer/store/memory'
import { setAvatar, setFilesPath, setResourcesPath, setUpdateState } from '@renderer/store/runtime'
import { setAvatar, setFilesPath, setIsOvmsSupported, setResourcesPath, setUpdateState } from '@renderer/store/runtime'
import {
type ToolPermissionRequestPayload,
type ToolPermissionResultPayload,
@ -274,4 +274,17 @@ export function useAppInit() {
useEffect(() => {
checkDataLimit()
}, [])
useEffect(() => {
// Check once when initing
window.api.ovms
.isSupported()
.then((result) => {
dispatch(setIsOvmsSupported(result))
})
.catch((e) => {
logger.error('Failed to check isOvmsSupported. Fallback to false.', e as Error)
dispatch(setIsOvmsSupported(false))
})
}, [dispatch])
}

View File

@ -8,6 +8,7 @@ import {
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
import { ProviderAvatar } from '@renderer/components/ProviderAvatar'
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useTimer } from '@renderer/hooks/useTimer'
import ImageStorage from '@renderer/services/ImageStorage'
import type { Provider, ProviderType } from '@renderer/types'
@ -30,8 +31,6 @@ import UrlSchemaInfoPopup from './UrlSchemaInfoPopup'
const logger = loggerService.withContext('ProviderList')
const BUTTON_WRAPPER_HEIGHT = 50
const systemType = await window.api.system.getDeviceType()
const cpuName = await window.api.system.getCpuName()
const ProviderList: FC = () => {
const [searchParams, setSearchParams] = useSearchParams()
@ -44,6 +43,7 @@ const ProviderList: FC = () => {
const [dragging, setDragging] = useState(false)
const [providerLogos, setProviderLogos] = useState<Record<string, string>>({})
const listRef = useRef<DraggableVirtualListRef>(null)
const { isOvmsSupported } = useRuntime()
const setSelectedProvider = useCallback((provider: Provider) => {
startTransition(() => _setSelectedProvider(provider))
@ -278,7 +278,7 @@ const ProviderList: FC = () => {
}
const filteredProviders = providers.filter((provider) => {
if (provider.id === 'ovms' && (systemType !== 'windows' || !cpuName.toLowerCase().includes('intel'))) {
if (provider.id === 'ovms' && !isOvmsSupported) {
return false
}

View File

@ -73,6 +73,7 @@ export interface RuntimeState {
export: ExportState
chat: ChatState
websearch: WebSearchState
isOvmsSupported: boolean | undefined
}
export interface ExportState {
@ -115,7 +116,8 @@ const initialState: RuntimeState = {
},
websearch: {
activeSearches: {}
}
},
isOvmsSupported: undefined
}
const runtimeSlice = createSlice({
@ -161,6 +163,9 @@ const runtimeSlice = createSlice({
setExportState: (state, action: PayloadAction<Partial<ExportState>>) => {
state.export = { ...state.export, ...action.payload }
},
setIsOvmsSupported: (state, action: PayloadAction<boolean>) => {
state.isOvmsSupported = action.payload
},
// Chat related actions
toggleMultiSelectMode: (state, action: PayloadAction<boolean>) => {
state.chat.isMultiSelectMode = action.payload
@ -223,6 +228,7 @@ export const {
setResourcesPath,
setUpdateState,
setExportState,
setIsOvmsSupported,
// Chat related actions
toggleMultiSelectMode,
setSelectedMessageIds,