mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-03 02:59:07 +08:00
feat: implement store synchronization across windows (#5592)
- Added new IPC channels for store synchronization: StoreSync_Subscribe, StoreSync_Unsubscribe, StoreSync_OnUpdate, and StoreSync_BroadcastSync. - Integrated store sync service in various components, including the main IPC handler and renderer store. - Removed the MiniWindowReload IPC channel as it was no longer needed. - Updated the store configuration to support synchronization of specific state slices.
This commit is contained in:
parent
d639a99a5f
commit
b71798d3e1
@ -151,7 +151,6 @@ export enum IpcChannel {
|
|||||||
|
|
||||||
HideMiniWindow = 'hide-mini-window',
|
HideMiniWindow = 'hide-mini-window',
|
||||||
ShowMiniWindow = 'show-mini-window',
|
ShowMiniWindow = 'show-mini-window',
|
||||||
MiniWindowReload = 'miniwindow-reload',
|
|
||||||
|
|
||||||
ReduxStateChange = 'redux-state-change',
|
ReduxStateChange = 'redux-state-change',
|
||||||
ReduxStoreReady = 'redux-store-ready',
|
ReduxStoreReady = 'redux-store-ready',
|
||||||
@ -159,5 +158,11 @@ export enum IpcChannel {
|
|||||||
// Search Window
|
// Search Window
|
||||||
SearchWindow_Open = 'search-window:open',
|
SearchWindow_Open = 'search-window:open',
|
||||||
SearchWindow_Close = 'search-window:close',
|
SearchWindow_Close = 'search-window:close',
|
||||||
SearchWindow_OpenUrl = 'search-window:open-url'
|
SearchWindow_OpenUrl = 'search-window:open-url',
|
||||||
|
|
||||||
|
//Store Sync
|
||||||
|
StoreSync_Subscribe = 'store-sync:subscribe',
|
||||||
|
StoreSync_Unsubscribe = 'store-sync:unsubscribe',
|
||||||
|
StoreSync_OnUpdate = 'store-sync:on-update',
|
||||||
|
StoreSync_BroadcastSync = 'store-sync:broadcast-sync'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import ObsidianVaultService from './services/ObsidianVaultService'
|
|||||||
import { ProxyConfig, proxyManager } from './services/ProxyManager'
|
import { ProxyConfig, proxyManager } from './services/ProxyManager'
|
||||||
import { searchService } from './services/SearchService'
|
import { searchService } from './services/SearchService'
|
||||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||||
|
import storeSyncService from './services/StoreSyncService'
|
||||||
import { TrayService } from './services/TrayService'
|
import { TrayService } from './services/TrayService'
|
||||||
import { setOpenLinkExternal } from './services/WebviewService'
|
import { setOpenLinkExternal } from './services/WebviewService'
|
||||||
import { windowService } from './services/WindowService'
|
import { windowService } from './services/WindowService'
|
||||||
@ -31,7 +32,6 @@ import { getResourcePath } from './utils'
|
|||||||
import { decrypt, encrypt } from './utils/aes'
|
import { decrypt, encrypt } from './utils/aes'
|
||||||
import { getConfigDir, getFilesDir } from './utils/file'
|
import { getConfigDir, getFilesDir } from './utils/file'
|
||||||
import { compress, decompress } from './utils/zip'
|
import { compress, decompress } from './utils/zip'
|
||||||
|
|
||||||
const fileManager = new FileStorage()
|
const fileManager = new FileStorage()
|
||||||
const backupManager = new BackupManager()
|
const backupManager = new BackupManager()
|
||||||
const exportService = new ExportService(fileManager)
|
const exportService = new ExportService(fileManager)
|
||||||
@ -338,4 +338,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle(IpcChannel.Webview_SetOpenLinkExternal, (_, webviewId: number, isExternal: boolean) =>
|
ipcMain.handle(IpcChannel.Webview_SetOpenLinkExternal, (_, webviewId: number, isExternal: boolean) =>
|
||||||
setOpenLinkExternal(webviewId, isExternal)
|
setOpenLinkExternal(webviewId, isExternal)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// store sync
|
||||||
|
storeSyncService.registerIpcHandler()
|
||||||
}
|
}
|
||||||
|
|||||||
114
src/main/services/StoreSyncService.ts
Normal file
114
src/main/services/StoreSyncService.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
|
import type { StoreSyncAction } from '@types'
|
||||||
|
import { BrowserWindow, ipcMain } from 'electron'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StoreSyncService class manages Redux store synchronization between multiple windows in the main process
|
||||||
|
* It uses singleton pattern to ensure only one sync service instance exists in the application
|
||||||
|
*
|
||||||
|
* Main features:
|
||||||
|
* 1. Manages window subscriptions for store sync
|
||||||
|
* 2. Handles IPC communication for store sync between windows
|
||||||
|
* 3. Broadcasts Redux actions from one window to all other windows
|
||||||
|
* 4. Adds metadata to synced actions to prevent infinite sync loops
|
||||||
|
*/
|
||||||
|
export class StoreSyncService {
|
||||||
|
private static instance: StoreSyncService
|
||||||
|
private windowIds: number[] = []
|
||||||
|
private isIpcHandlerRegistered = false
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the singleton instance of StoreSyncService
|
||||||
|
*/
|
||||||
|
public static getInstance(): StoreSyncService {
|
||||||
|
if (!StoreSyncService.instance) {
|
||||||
|
StoreSyncService.instance = new StoreSyncService()
|
||||||
|
}
|
||||||
|
return StoreSyncService.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe a window to store sync
|
||||||
|
* @param windowId ID of the window to subscribe
|
||||||
|
*/
|
||||||
|
public subscribe(windowId: number): void {
|
||||||
|
if (!this.windowIds.includes(windowId)) {
|
||||||
|
this.windowIds.push(windowId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe a window from store sync
|
||||||
|
* @param windowId ID of the window to unsubscribe
|
||||||
|
*/
|
||||||
|
public unsubscribe(windowId: number): void {
|
||||||
|
this.windowIds = this.windowIds.filter((id) => id !== windowId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register IPC handlers for store sync communication
|
||||||
|
* Handles window subscription, unsubscription and action broadcasting
|
||||||
|
*/
|
||||||
|
public registerIpcHandler(): void {
|
||||||
|
if (this.isIpcHandlerRegistered) return
|
||||||
|
|
||||||
|
ipcMain.handle(IpcChannel.StoreSync_Subscribe, (event) => {
|
||||||
|
const windowId = BrowserWindow.fromWebContents(event.sender)?.id
|
||||||
|
if (windowId) {
|
||||||
|
this.subscribe(windowId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle(IpcChannel.StoreSync_Unsubscribe, (event) => {
|
||||||
|
const windowId = BrowserWindow.fromWebContents(event.sender)?.id
|
||||||
|
if (windowId) {
|
||||||
|
this.unsubscribe(windowId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle(IpcChannel.StoreSync_OnUpdate, (event, action: StoreSyncAction) => {
|
||||||
|
const sourceWindowId = BrowserWindow.fromWebContents(event.sender)?.id
|
||||||
|
|
||||||
|
if (!sourceWindowId) return
|
||||||
|
|
||||||
|
// Broadcast the action to all other windows
|
||||||
|
this.broadcastToOtherWindows(sourceWindowId, action)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast a Redux action to all other windows except the source
|
||||||
|
* @param sourceWindowId ID of the window that originated the action
|
||||||
|
* @param action Redux action to broadcast
|
||||||
|
*/
|
||||||
|
private broadcastToOtherWindows(sourceWindowId: number, action: StoreSyncAction): void {
|
||||||
|
// Add metadata to indicate this action came from sync
|
||||||
|
const syncAction = {
|
||||||
|
...action,
|
||||||
|
meta: {
|
||||||
|
...action.meta,
|
||||||
|
fromSync: true,
|
||||||
|
source: `windowId:${sourceWindowId}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to all windows except the source
|
||||||
|
this.windowIds.forEach((windowId) => {
|
||||||
|
if (windowId !== sourceWindowId) {
|
||||||
|
const targetWindow = BrowserWindow.fromId(windowId)
|
||||||
|
if (targetWindow && !targetWindow.isDestroyed()) {
|
||||||
|
targetWindow.webContents.send(IpcChannel.StoreSync_BroadcastSync, syncAction)
|
||||||
|
} else {
|
||||||
|
this.unsubscribe(windowId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export default StoreSyncService.getInstance()
|
||||||
@ -3,7 +3,7 @@ import { isDev, isLinux, isMac, isWin } from '@main/constant'
|
|||||||
import { getFilesDir } from '@main/utils/file'
|
import { getFilesDir } from '@main/utils/file'
|
||||||
import { IpcChannel } from '@shared/IpcChannel'
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
import { ThemeMode } from '@types'
|
import { ThemeMode } from '@types'
|
||||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, nativeTheme, shell } from 'electron'
|
import { app, BrowserWindow, Menu, MenuItem, nativeTheme, shell } from 'electron'
|
||||||
import Logger from 'electron-log'
|
import Logger from 'electron-log'
|
||||||
import windowStateKeeper from 'electron-window-state'
|
import windowStateKeeper from 'electron-window-state'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
@ -484,10 +484,6 @@ export class WindowService {
|
|||||||
this.miniWindow?.webContents.send(IpcChannel.ShowMiniWindow)
|
this.miniWindow?.webContents.send(IpcChannel.ShowMiniWindow)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on(IpcChannel.MiniWindowReload, () => {
|
|
||||||
this.miniWindow?.reload()
|
|
||||||
})
|
|
||||||
|
|
||||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
this.miniWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/src/windows/mini/index.html')
|
this.miniWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/src/windows/mini/index.html')
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -155,7 +155,6 @@ const api = {
|
|||||||
logout: () => ipcRenderer.invoke(IpcChannel.Copilot_Logout),
|
logout: () => ipcRenderer.invoke(IpcChannel.Copilot_Logout),
|
||||||
getUser: (token: string) => ipcRenderer.invoke(IpcChannel.Copilot_GetUser, token)
|
getUser: (token: string) => ipcRenderer.invoke(IpcChannel.Copilot_GetUser, token)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Binary related APIs
|
// Binary related APIs
|
||||||
isBinaryExist: (name: string) => ipcRenderer.invoke(IpcChannel.App_IsBinaryExist, name),
|
isBinaryExist: (name: string) => ipcRenderer.invoke(IpcChannel.App_IsBinaryExist, name),
|
||||||
getBinaryPath: (name: string) => ipcRenderer.invoke(IpcChannel.App_GetBinaryPath, name),
|
getBinaryPath: (name: string) => ipcRenderer.invoke(IpcChannel.App_GetBinaryPath, name),
|
||||||
@ -186,6 +185,11 @@ const api = {
|
|||||||
webview: {
|
webview: {
|
||||||
setOpenLinkExternal: (webviewId: number, isExternal: boolean) =>
|
setOpenLinkExternal: (webviewId: number, isExternal: boolean) =>
|
||||||
ipcRenderer.invoke(IpcChannel.Webview_SetOpenLinkExternal, webviewId, isExternal)
|
ipcRenderer.invoke(IpcChannel.Webview_SetOpenLinkExternal, webviewId, isExternal)
|
||||||
|
},
|
||||||
|
storeSync: {
|
||||||
|
subscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Subscribe),
|
||||||
|
unsubscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Unsubscribe),
|
||||||
|
onUpdate: (action: any) => ipcRenderer.invoke(IpcChannel.StoreSync_OnUpdate, action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import KeyvStorage from '@kangfenmao/keyv-storage'
|
|||||||
|
|
||||||
import { startAutoSync } from './services/BackupService'
|
import { startAutoSync } from './services/BackupService'
|
||||||
import { startNutstoreAutoSync } from './services/NutstoreService'
|
import { startNutstoreAutoSync } from './services/NutstoreService'
|
||||||
|
import storeSyncService from './services/StoreSyncService'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
|
|
||||||
function initSpinner() {
|
function initSpinner() {
|
||||||
@ -29,6 +30,11 @@ function initAutoSync() {
|
|||||||
}, 8000)
|
}, 8000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initStoreSync() {
|
||||||
|
storeSyncService.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
initSpinner()
|
initSpinner()
|
||||||
initKeyv()
|
initKeyv()
|
||||||
initAutoSync()
|
initAutoSync()
|
||||||
|
initStoreSync()
|
||||||
|
|||||||
136
src/renderer/src/services/StoreSyncService.ts
Normal file
136
src/renderer/src/services/StoreSyncService.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { Middleware } from '@reduxjs/toolkit'
|
||||||
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
|
import type { StoreSyncAction } from '@types'
|
||||||
|
|
||||||
|
type SyncOptions = {
|
||||||
|
syncList: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StoreSyncService class manages Redux store synchronization between multiple windows
|
||||||
|
* It uses singleton pattern to ensure only one sync service instance exists in the application
|
||||||
|
*
|
||||||
|
* Main features:
|
||||||
|
* 1. Synchronizes Redux actions between windows via IPC
|
||||||
|
* 2. Provides Redux middleware to intercept and broadcast actions that need syncing
|
||||||
|
* 3. Supports whitelist configuration for action types to sync
|
||||||
|
* 4. Handles window subscription and unsubscription logic
|
||||||
|
*/
|
||||||
|
export class StoreSyncService {
|
||||||
|
private static instance: StoreSyncService
|
||||||
|
private options: SyncOptions = {
|
||||||
|
syncList: []
|
||||||
|
}
|
||||||
|
private broadcastSyncRemover: (() => void) | null = null
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the singleton instance of StoreSyncService
|
||||||
|
*/
|
||||||
|
public static getInstance(): StoreSyncService {
|
||||||
|
if (!StoreSyncService.instance) {
|
||||||
|
StoreSyncService.instance = new StoreSyncService()
|
||||||
|
}
|
||||||
|
return StoreSyncService.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set sync options
|
||||||
|
* @param options Partial sync options
|
||||||
|
*/
|
||||||
|
public setOptions(options: Partial<SyncOptions>): void {
|
||||||
|
this.options = { ...this.options, ...options }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Redux middleware to intercept and broadcast actions
|
||||||
|
* Actions will not be broadcasted if they are not in whitelist or come from sync
|
||||||
|
*/
|
||||||
|
public createMiddleware(): Middleware {
|
||||||
|
return () => (next) => (action) => {
|
||||||
|
// Process the action normally first
|
||||||
|
const result = next(action)
|
||||||
|
|
||||||
|
// Check if this action came from sync or is a whitelisted action
|
||||||
|
const syncAction = action as StoreSyncAction
|
||||||
|
if (!syncAction.meta?.fromSync && this.shouldSyncAction(syncAction.type)) {
|
||||||
|
// Send to main process for broadcasting to other windows using the preload API
|
||||||
|
if (window.api?.storeSync) {
|
||||||
|
window.api.storeSync.onUpdate(syncAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if action type is in whitelist
|
||||||
|
* @param actionType Action type to check
|
||||||
|
* @returns Whether the action should be synced
|
||||||
|
*/
|
||||||
|
private shouldSyncAction(actionType: string): boolean {
|
||||||
|
// If no whitelist is specified, sync nothing
|
||||||
|
if (!this.options.syncList.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the action belongs to a store slice we want to sync
|
||||||
|
return this.options.syncList.some((prefix) => {
|
||||||
|
return actionType.startsWith(prefix)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to sync service
|
||||||
|
* Sets up IPC listener and registers cleanup on window close
|
||||||
|
*/
|
||||||
|
public subscribe(): void {
|
||||||
|
if (this.broadcastSyncRemover || !window.api?.storeSync) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.broadcastSyncRemover = window.electron.ipcRenderer.on(
|
||||||
|
IpcChannel.StoreSync_BroadcastSync,
|
||||||
|
(_, action: StoreSyncAction) => {
|
||||||
|
try {
|
||||||
|
console.log('StoreSync_BroadcastSync action', action)
|
||||||
|
|
||||||
|
// Dispatch to the store
|
||||||
|
if (window.store) {
|
||||||
|
window.store.dispatch(action)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error dispatching synced action:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
window.api.storeSync.subscribe()
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
this.unsubscribe()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from sync service
|
||||||
|
* Cleans up IPC listener and related resources
|
||||||
|
*/
|
||||||
|
public unsubscribe(): void {
|
||||||
|
if (window.api?.storeSync) {
|
||||||
|
window.api.storeSync.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.broadcastSyncRemover) {
|
||||||
|
this.broadcastSyncRemover()
|
||||||
|
this.broadcastSyncRemover = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export default StoreSyncService.getInstance()
|
||||||
@ -3,6 +3,7 @@ import { useDispatch, useSelector, useStore } from 'react-redux'
|
|||||||
import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, REHYDRATE } from 'redux-persist'
|
import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, REHYDRATE } from 'redux-persist'
|
||||||
import storage from 'redux-persist/lib/storage'
|
import storage from 'redux-persist/lib/storage'
|
||||||
|
|
||||||
|
import storeSyncService from '../services/StoreSyncService'
|
||||||
import agents from './agents'
|
import agents from './agents'
|
||||||
import assistants from './assistants'
|
import assistants from './assistants'
|
||||||
import backup from './backup'
|
import backup from './backup'
|
||||||
@ -52,6 +53,21 @@ const persistedReducer = persistReducer(
|
|||||||
rootReducer
|
rootReducer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the store sync service to synchronize specific state slices across all windows.
|
||||||
|
* For detailed implementation, see @renderer/services/StoreSyncService.ts
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - 'xxxx/' - Synchronizes the entire state slice
|
||||||
|
* - 'xxxx/sliceName' - Synchronizes a specific slice within the state
|
||||||
|
*
|
||||||
|
* To listen for store changes in a window:
|
||||||
|
* Call storeSyncService.subscribe() in the window's entryPoint.tsx
|
||||||
|
*/
|
||||||
|
storeSyncService.setOptions({
|
||||||
|
syncList: ['assistants/', 'settings/', 'llm/']
|
||||||
|
})
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
// @ts-ignore store type is unknown
|
// @ts-ignore store type is unknown
|
||||||
reducer: persistedReducer as typeof rootReducer,
|
reducer: persistedReducer as typeof rootReducer,
|
||||||
@ -60,7 +76,7 @@ const store = configureStore({
|
|||||||
serializableCheck: {
|
serializableCheck: {
|
||||||
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
|
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
|
||||||
}
|
}
|
||||||
})
|
}).concat(storeSyncService.createMiddleware())
|
||||||
},
|
},
|
||||||
devTools: true
|
devTools: true
|
||||||
})
|
})
|
||||||
@ -72,7 +88,6 @@ export const persistor = persistStore(store)
|
|||||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
||||||
export const useAppSelector = useSelector.withTypes<RootState>()
|
export const useAppSelector = useSelector.withTypes<RootState>()
|
||||||
export const useAppStore = useStore.withTypes<typeof store>()
|
export const useAppStore = useStore.withTypes<typeof store>()
|
||||||
|
|
||||||
window.store = store
|
window.store = store
|
||||||
|
|
||||||
export default store
|
export default store
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
|||||||
import { isLocalAi } from '@renderer/config/env'
|
import { isLocalAi } from '@renderer/config/env'
|
||||||
import { SYSTEM_MODELS } from '@renderer/config/models'
|
import { SYSTEM_MODELS } from '@renderer/config/models'
|
||||||
import { Model, Provider } from '@renderer/types'
|
import { Model, Provider } from '@renderer/types'
|
||||||
import { IpcChannel } from '@shared/IpcChannel'
|
|
||||||
import { uniqBy } from 'lodash'
|
import { uniqBy } from 'lodash'
|
||||||
|
|
||||||
type LlmSettings = {
|
type LlmSettings = {
|
||||||
@ -583,7 +582,6 @@ const llmSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setDefaultModel: (state, action: PayloadAction<{ model: Model }>) => {
|
setDefaultModel: (state, action: PayloadAction<{ model: Model }>) => {
|
||||||
state.defaultModel = action.payload.model
|
state.defaultModel = action.payload.model
|
||||||
window.electron.ipcRenderer.send(IpcChannel.MiniWindowReload)
|
|
||||||
},
|
},
|
||||||
setTopicNamingModel: (state, action: PayloadAction<{ model: Model }>) => {
|
setTopicNamingModel: (state, action: PayloadAction<{ model: Model }>) => {
|
||||||
state.topicNamingModel = action.payload.model
|
state.topicNamingModel = action.payload.model
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
|
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
|
||||||
import { CodeStyleVarious, LanguageVarious, MathEngine, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
import { CodeStyleVarious, LanguageVarious, MathEngine, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
||||||
import { IpcChannel } from '@shared/IpcChannel'
|
|
||||||
|
|
||||||
import { WebDAVSyncState } from './backup'
|
import { WebDAVSyncState } from './backup'
|
||||||
|
|
||||||
@ -256,7 +255,6 @@ const settingsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setLanguage: (state, action: PayloadAction<LanguageVarious>) => {
|
setLanguage: (state, action: PayloadAction<LanguageVarious>) => {
|
||||||
state.language = action.payload
|
state.language = action.payload
|
||||||
window.electron.ipcRenderer.send(IpcChannel.MiniWindowReload)
|
|
||||||
},
|
},
|
||||||
setTargetLanguage: (state, action: PayloadAction<TranslateLanguageVarious>) => {
|
setTargetLanguage: (state, action: PayloadAction<TranslateLanguageVarious>) => {
|
||||||
state.targetLanguage = action.payload
|
state.targetLanguage = action.payload
|
||||||
|
|||||||
@ -620,3 +620,12 @@ export interface Citation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MathEngine = 'KaTeX' | 'MathJax' | 'none'
|
export type MathEngine = 'KaTeX' | 'MathJax' | 'none'
|
||||||
|
|
||||||
|
export interface StoreSyncAction {
|
||||||
|
type: string
|
||||||
|
payload: any
|
||||||
|
meta?: {
|
||||||
|
fromSync?: boolean
|
||||||
|
source?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import '@renderer/assets/styles/index.scss'
|
|||||||
import '@ant-design/v5-patch-for-react-19'
|
import '@ant-design/v5-patch-for-react-19'
|
||||||
|
|
||||||
import KeyvStorage from '@kangfenmao/keyv-storage'
|
import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||||
|
import storeSyncService from '@renderer/services/StoreSyncService'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
|
||||||
import MiniWindowApp from './MiniWindowApp'
|
import MiniWindowApp from './MiniWindowApp'
|
||||||
@ -18,5 +19,8 @@ function initKeyv() {
|
|||||||
}
|
}
|
||||||
initKeyv()
|
initKeyv()
|
||||||
|
|
||||||
|
//subscribe to store sync
|
||||||
|
storeSyncService.subscribe()
|
||||||
|
|
||||||
const root = createRoot(document.getElementById('root') as HTMLElement)
|
const root = createRoot(document.getElementById('root') as HTMLElement)
|
||||||
root.render(<MiniWindowApp />)
|
root.render(<MiniWindowApp />)
|
||||||
|
|||||||
@ -216,7 +216,7 @@ const HomeWindow: FC = () => {
|
|||||||
setIsFirstMessage(false)
|
setIsFirstMessage(false)
|
||||||
setText('') // ✅ 清除输入框内容
|
setText('') // ✅ 清除输入框内容
|
||||||
},
|
},
|
||||||
[content, defaultAssistant]
|
[content, defaultAssistant, topic]
|
||||||
)
|
)
|
||||||
|
|
||||||
const clearClipboard = () => {
|
const clearClipboard = () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user