diff --git a/package.json b/package.json index 9785143c57..972a3924d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "1.6.0-beta.7", + "version": "2.0.0-alpha", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", diff --git a/packages/shared/data/cache/cacheSchemas.ts b/packages/shared/data/cache/cacheSchemas.ts index eeaa365e12..9be41126c0 100644 --- a/packages/shared/data/cache/cacheSchemas.ts +++ b/packages/shared/data/cache/cacheSchemas.ts @@ -1,22 +1,144 @@ +import type * as CacheValueTypes from './cacheValueTypes' + +/** + * Use cache schema for renderer hook + */ + +export type UseCacheSchema = { + // App state + 'app.dist.update_state': CacheValueTypes.CacheAppUpdateState + 'app.user.avatar': string + + // Chat context + 'chat.multi_select_mode': boolean + 'chat.selected_message_ids': string[] + 'chat.generating': boolean + 'chat.websearch.searching': boolean + 'chat.websearch.active_searches': CacheValueTypes.CacheActiveSearches + + // Minapp management + 'minapp.opened_keep_alive': CacheValueTypes.CacheMinAppType[] + 'minapp.current_id': string + 'minapp.show': boolean + 'minapp.opened_oneoff': CacheValueTypes.CacheMinAppType | null + + // Topic management + 'topic.active': CacheValueTypes.CacheTopic | null + 'topic.renaming': string[] + 'topic.newly_renamed': string[] + + // Test keys (for dataRefactorTest window) + // TODO: remove after testing + 'test-hook-memory-1': string + 'test-ttl-cache': string + 'test-protected-cache': string + 'test-deep-equal': { nested: { count: number }; tags: string[] } + 'test-performance': number + 'test-multi-hook': string + 'concurrent-test-1': number + 'concurrent-test-2': number + 'large-data-test': Record + 'test-number-cache': number + 'test-object-cache': { name: string; count: number; active: boolean } +} + +export const DefaultUseCache: UseCacheSchema = { + // App state + 'app.dist.update_state': { + info: null, + checking: false, + downloading: false, + downloaded: false, + downloadProgress: 0, + available: false + }, + 'app.user.avatar': '', + + // Chat context + 'chat.multi_select_mode': false, + 'chat.selected_message_ids': [], + 'chat.generating': false, + 'chat.websearch.searching': false, + 'chat.websearch.active_searches': {}, + + // Minapp management + 'minapp.opened_keep_alive': [], + 'minapp.current_id': '', + 'minapp.show': false, + 'minapp.opened_oneoff': null, + + // Topic management + 'topic.active': null, + 'topic.renaming': [], + 'topic.newly_renamed': [], + + // Test keys (for dataRefactorTest window) + // TODO: remove after testing + 'test-hook-memory-1': 'default-memory-value', + 'test-ttl-cache': 'test-ttl-cache', + 'test-protected-cache': 'protected-value', + 'test-deep-equal': { nested: { count: 0 }, tags: ['initial'] }, + 'test-performance': 0, + 'test-multi-hook': 'hook-1-default', + 'concurrent-test-1': 0, + 'concurrent-test-2': 0, + 'large-data-test': {}, + 'test-number-cache': 42, + 'test-object-cache': { name: 'test', count: 0, active: true } +} + +/** + * Use shared cache schema for renderer hook + */ +export type UseSharedCacheSchema = { + 'example-key': string + + // Test keys (for dataRefactorTest window) + // TODO: remove after testing + 'test-hook-shared-1': string + 'test-multi-hook': string + 'concurrent-shared': number +} + +export const DefaultUseSharedCache: UseSharedCacheSchema = { + 'example-key': 'example default value', + + // Test keys (for dataRefactorTest window) + // TODO: remove after testing + 'concurrent-shared': 0, + 'test-hook-shared-1': 'default-shared-value', + 'test-multi-hook': 'hook-3-shared' +} + /** * Persist cache schema defining allowed keys and their value types * This ensures type safety and prevents key conflicts */ -export interface PersistCacheSchema { +export type RendererPersistCacheSchema = { + 'example-key': string + + // Test keys (for dataRefactorTest window) + // TODO: remove after testing 'example-1': string - 'example-2': number - 'example-3': boolean - 'example-4': { a: string; b: number; c: boolean } + 'example-2': string + 'example-3': string + 'example-4': string } -export const DefaultPersistCache: PersistCacheSchema = { - 'example-1': 'example-1', - 'example-2': 1, - 'example-3': true, - 'example-4': { a: 'example-4', b: 4, c: false } +export const DefaultRendererPersistCache: RendererPersistCacheSchema = { + 'example-key': 'example default value', + + // Test keys (for dataRefactorTest window) + // TODO: remove after testing + 'example-1': 'example default value', + 'example-2': 'example default value', + 'example-3': 'example default value', + 'example-4': 'example default value' } /** - * Type-safe persist cache key + * Type-safe cache key */ -export type PersistCacheKey = keyof PersistCacheSchema +export type RendererPersistCacheKey = keyof RendererPersistCacheSchema +export type UseCacheKey = keyof UseCacheSchema +export type UseSharedCacheKey = keyof UseSharedCacheSchema diff --git a/packages/shared/data/cache/cacheValueTypes.ts b/packages/shared/data/cache/cacheValueTypes.ts new file mode 100644 index 0000000000..095f46a131 --- /dev/null +++ b/packages/shared/data/cache/cacheValueTypes.ts @@ -0,0 +1,18 @@ +import type { MinAppType, Topic, WebSearchStatus } from '@types' +import type { UpdateInfo } from 'builder-util-runtime' + +export type CacheAppUpdateState = { + info: UpdateInfo | null + checking: boolean + downloading: boolean + downloaded: boolean + downloadProgress: number + available: boolean +} + +export type CacheActiveSearches = Record + +// For cache schema, we use any for complex types to avoid circular dependencies +// The actual type checking will be done at runtime by the cache system +export type CacheMinAppType = MinAppType +export type CacheTopic = Topic diff --git a/src/renderer/src/components/MinApp/MinApp.tsx b/src/renderer/src/components/MinApp/MinApp.tsx index 3a7255d199..b3a51102b5 100644 --- a/src/renderer/src/components/MinApp/MinApp.tsx +++ b/src/renderer/src/components/MinApp/MinApp.tsx @@ -5,14 +5,11 @@ import { loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateDefaultMinApps } from import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinapps } from '@renderer/hooks/useMinapps' import { useNavbarPosition } from '@renderer/hooks/useNavbar' -import { useRuntime } from '@renderer/hooks/useRuntime' -import { setOpenedKeepAliveMinapps } from '@renderer/store/runtime' import { MinAppType } from '@renderer/types' import type { MenuProps } from 'antd' import { Dropdown } from 'antd' import { FC } from 'react' import { useTranslation } from 'react-i18next' -import { useDispatch } from 'react-redux' import { useNavigate } from 'react-router-dom' import styled from 'styled-components' @@ -28,9 +25,18 @@ const logger = loggerService.withContext('App') const MinApp: FC = ({ app, onClick, size = 60, isLast }) => { const { openMinappKeepAlive } = useMinappPopup() const { t } = useTranslation() - const { minapps, pinned, disabled, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps() - const { openedKeepAliveMinapps, currentMinappId, minappShow } = useRuntime() - const dispatch = useDispatch() + const { + minapps, + pinned, + disabled, + openedKeepAliveMinapps, + currentMinappId, + minappShow, + setOpenedKeepAliveMinapps, + updateMinapps, + updateDisabledMinapps, + updatePinnedMinapps + } = useMinapps() const navigate = useNavigate() const isPinned = pinned.some((p) => p.id === app.id) const isVisible = minapps.some((m) => m.id === app.id) @@ -76,7 +82,7 @@ const MinApp: FC = ({ app, onClick, size = 60, isLast }) => { updatePinnedMinapps(newPinned) // 更新 openedKeepAliveMinapps const newOpenedKeepAliveMinapps = openedKeepAliveMinapps.filter((item) => item.id !== app.id) - dispatch(setOpenedKeepAliveMinapps(newOpenedKeepAliveMinapps)) + setOpenedKeepAliveMinapps(newOpenedKeepAliveMinapps) } }, ...(app.type === 'Custom' diff --git a/src/renderer/src/components/MinApp/MinAppTabsPool.tsx b/src/renderer/src/components/MinApp/MinAppTabsPool.tsx index 44ad9995bd..da11991676 100644 --- a/src/renderer/src/components/MinApp/MinAppTabsPool.tsx +++ b/src/renderer/src/components/MinApp/MinAppTabsPool.tsx @@ -1,7 +1,7 @@ import { loggerService } from '@logger' import WebviewContainer from '@renderer/components/MinApp/WebviewContainer' +import { useMinapps } from '@renderer/hooks/useMinapps' import { useNavbarPosition } from '@renderer/hooks/useNavbar' -import { useRuntime } from '@renderer/hooks/useRuntime' import { getWebviewLoaded, setWebviewLoaded } from '@renderer/utils/webviewStateManager' import { WebviewTag } from 'electron' import React, { useEffect, useRef } from 'react' @@ -21,7 +21,7 @@ import styled from 'styled-components' const logger = loggerService.withContext('MinAppTabsPool') const MinAppTabsPool: React.FC = () => { - const { openedKeepAliveMinapps, currentMinappId } = useRuntime() + const { openedKeepAliveMinapps, currentMinappId } = useMinapps() const { isTopNavbar } = useNavbarPosition() const location = useLocation() diff --git a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx index a915e820c7..b66f666128 100644 --- a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx +++ b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx @@ -20,7 +20,6 @@ import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinapps } from '@renderer/hooks/useMinapps' import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor' import { useNavbarPosition } from '@renderer/hooks/useNavbar' -import { useRuntime } from '@renderer/hooks/useRuntime' import { useTimer } from '@renderer/hooks/useTimer' import { MinAppType } from '@renderer/types' import { delay } from '@renderer/utils' @@ -141,10 +140,10 @@ const GoogleLoginTip = ({ /** The main container for MinApp popup */ const MinappPopupContainer: React.FC = () => { - const { openedKeepAliveMinapps, openedOneOffMinapp, currentMinappId, minappShow } = useRuntime() const [minappsOpenLinkExternal, setMinappsOpenLinkExternal] = usePreference('feature.minapp.open_link_external') const { closeMinapp, hideMinappPopup } = useMinappPopup() - const { pinned, updatePinnedMinapps } = useMinapps() + const { pinned, updatePinnedMinapps, openedKeepAliveMinapps, openedOneOffMinapp, currentMinappId, minappShow } = + useMinapps() const { t } = useTranslation() const backgroundColor = useNavBackgroundColor() const { isTopNavbar } = useNavbarPosition() diff --git a/src/renderer/src/components/MinApp/TopViewMinappContainer.tsx b/src/renderer/src/components/MinApp/TopViewMinappContainer.tsx index 1279673e1c..90fda92d8a 100644 --- a/src/renderer/src/components/MinApp/TopViewMinappContainer.tsx +++ b/src/renderer/src/components/MinApp/TopViewMinappContainer.tsx @@ -1,9 +1,9 @@ import MinappPopupContainer from '@renderer/components/MinApp/MinappPopupContainer' +import { useMinapps } from '@renderer/hooks/useMinapps' import { useNavbarPosition } from '@renderer/hooks/useNavbar' -import { useRuntime } from '@renderer/hooks/useRuntime' const TopViewMinappContainer = () => { - const { openedKeepAliveMinapps, openedOneOffMinapp } = useRuntime() + const { openedKeepAliveMinapps, openedOneOffMinapp } = useMinapps() const { isLeftNavbar } = useNavbarPosition() const isCreate = openedKeepAliveMinapps.length > 0 || openedOneOffMinapp !== null diff --git a/src/renderer/src/components/Popups/UserPopup.tsx b/src/renderer/src/components/Popups/UserPopup.tsx index fe424a8c12..210bd7b013 100644 --- a/src/renderer/src/components/Popups/UserPopup.tsx +++ b/src/renderer/src/components/Popups/UserPopup.tsx @@ -1,10 +1,9 @@ +import { cacheService } from '@data/CacheService' import { usePreference } from '@data/hooks/usePreference' import DefaultAvatar from '@renderer/assets/images/avatar.png' import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar' import useAvatar from '@renderer/hooks/useAvatar' import ImageStorage from '@renderer/services/ImageStorage' -import { useAppDispatch } from '@renderer/store' -import { setAvatar } from '@renderer/store/runtime' import { compressImage, isEmoji } from '@renderer/utils' import { Avatar, Dropdown, Input, Modal, Popover, Upload } from 'antd' import React, { useState } from 'react' @@ -26,7 +25,6 @@ const PopupContainer: React.FC = ({ resolve }) => { const [emojiPickerOpen, setEmojiPickerOpen] = useState(false) const [dropdownOpen, setDropdownOpen] = useState(false) const { t } = useTranslation() - const dispatch = useAppDispatch() const avatar = useAvatar() const onOk = () => { @@ -46,7 +44,7 @@ const PopupContainer: React.FC = ({ resolve }) => { // set emoji string await ImageStorage.set('avatar', emoji) // update avatar display - dispatch(setAvatar(emoji)) + cacheService.set('avatar', emoji) setEmojiPickerOpen(false) } catch (error: any) { window.toast.error(error.message) @@ -55,7 +53,7 @@ const PopupContainer: React.FC = ({ resolve }) => { const handleReset = async () => { try { await ImageStorage.set('avatar', DefaultAvatar) - dispatch(setAvatar(DefaultAvatar)) + cacheService.set('avatar', DefaultAvatar) setDropdownOpen(false) } catch (error: any) { window.toast.error(error.message) @@ -80,7 +78,7 @@ const PopupContainer: React.FC = ({ resolve }) => { const compressedFile = await compressImage(_file) await ImageStorage.set('avatar', compressedFile) } - dispatch(setAvatar(await ImageStorage.get('avatar'))) + cacheService.set('avatar', await ImageStorage.get('avatar')) setDropdownOpen(false) } catch (error: any) { window.toast.error(error.message) diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx index 8f88c8d0c1..330e2dab9b 100644 --- a/src/renderer/src/components/app/Navbar.tsx +++ b/src/renderer/src/components/app/Navbar.tsx @@ -1,8 +1,8 @@ import { isLinux, isMac, isWin } from '@renderer/config/constant' import { useFullscreen } from '@renderer/hooks/useFullscreen' +import { useMinapps } from '@renderer/hooks/useMinapps' import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor' import { useNavbarPosition } from '@renderer/hooks/useNavbar' -import { useRuntime } from '@renderer/hooks/useRuntime' import type { FC, PropsWithChildren } from 'react' import type { HTMLAttributes } from 'react' import styled from 'styled-components' @@ -15,7 +15,7 @@ export const Navbar: FC = ({ children, ...props }) => { const backgroundColor = useNavBackgroundColor() const isFullscreen = useFullscreen() const { isTopNavbar } = useNavbarPosition() - const { minappShow } = useRuntime() + const { minappShow } = useMinapps() if (isTopNavbar) { return null diff --git a/src/renderer/src/components/app/PinnedMinapps.tsx b/src/renderer/src/components/app/PinnedMinapps.tsx index b43386b375..06ae2ccdce 100644 --- a/src/renderer/src/components/app/PinnedMinapps.tsx +++ b/src/renderer/src/components/app/PinnedMinapps.tsx @@ -3,7 +3,6 @@ import { useTheme } from '@renderer/context/ThemeProvider' import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinapps } from '@renderer/hooks/useMinapps' import { useNavbarPosition } from '@renderer/hooks/useNavbar' -import { useRuntime } from '@renderer/hooks/useRuntime' import { MinAppType } from '@renderer/types' import type { MenuProps } from 'antd' import { Dropdown, Tooltip } from 'antd' @@ -16,7 +15,7 @@ import MinAppIcon from '../Icons/MinAppIcon' /** Tabs of opened minapps in sidebar */ export const SidebarOpenedMinappTabs: FC = () => { - const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime() + const { minappShow, openedKeepAliveMinapps, currentMinappId } = useMinapps() const { openMinappKeepAlive, hideMinappPopup, closeMinapp, closeAllMinapps } = useMinappPopup() const [showOpenedMinappsInSidebar] = usePreference('feature.minapp.show_opened_in_sidebar') const { theme } = useTheme() @@ -105,9 +104,8 @@ export const SidebarOpenedMinappTabs: FC = () => { } export const SidebarPinnedApps: FC = () => { - const { pinned, updatePinnedMinapps } = useMinapps() + const { pinned, updatePinnedMinapps, minappShow, openedKeepAliveMinapps, currentMinappId } = useMinapps() const { t } = useTranslation() - const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime() const { theme } = useTheme() const { openMinappKeepAlive } = useMinappPopup() const { isTopNavbar } = useNavbarPosition() diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx index 1370ae6c8c..c4f98babca 100644 --- a/src/renderer/src/components/app/Sidebar.tsx +++ b/src/renderer/src/components/app/Sidebar.tsx @@ -7,8 +7,8 @@ import useAvatar from '@renderer/hooks/useAvatar' import { useFullscreen } from '@renderer/hooks/useFullscreen' import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinapps } from '@renderer/hooks/useMinapps' +import { modelGenerating } from '@renderer/hooks/useModel' import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor' -import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' import { getSidebarIconLabel, getThemeModeLabel } from '@renderer/i18n/label' import { isEmoji } from '@renderer/utils' @@ -39,8 +39,7 @@ import { SidebarOpenedMinappTabs, SidebarPinnedApps } from './PinnedMinapps' const Sidebar: FC = () => { const { hideMinappPopup } = useMinappPopup() - const { minappShow } = useRuntime() - const { pinned } = useMinapps() + const { pinned, minappShow } = useMinapps() const [visibleSidebarIcons] = usePreference('ui.sidebar.icons.visible') const { pathname } = useLocation() @@ -122,10 +121,11 @@ const Sidebar: FC = () => { const MainMenus: FC = () => { const { hideMinappPopup } = useMinappPopup() + const { minappShow } = useMinapps() + const { pathname } = useLocation() const [visibleSidebarIcons] = usePreference('ui.sidebar.icons.visible') const { defaultPaintingProvider } = useSettings() - const { minappShow } = useRuntime() const navigate = useNavigate() const { theme } = useTheme() diff --git a/src/renderer/src/data/CacheService.ts b/src/renderer/src/data/CacheService.ts index c358082e78..a69f4e4699 100644 --- a/src/renderer/src/data/CacheService.ts +++ b/src/renderer/src/data/CacheService.ts @@ -1,6 +1,13 @@ import { loggerService } from '@logger' -import type { PersistCacheKey, PersistCacheSchema } from '@shared/data/cache/cacheSchemas' -import { DefaultPersistCache } from '@shared/data/cache/cacheSchemas' +import type { + RendererPersistCacheKey, + RendererPersistCacheSchema, + UseCacheKey, + UseCacheSchema, + UseSharedCacheKey, + UseSharedCacheSchema +} from '@shared/data/cache/cacheSchemas' +import { DefaultRendererPersistCache } from '@shared/data/cache/cacheSchemas' import type { CacheEntry, CacheSubscriber, CacheSyncMessage } from '@shared/data/cache/cacheTypes' const STORAGE_PERSIST_KEY = 'cs_cache_persist' @@ -28,7 +35,7 @@ export class CacheService { // Three-layer cache system private memoryCache = new Map() // Cross-component cache private sharedCache = new Map() // Cross-window cache (local copy) - private persistCache = new Map() // Persistent cache + private persistCache = new Map() // Persistent cache // Hook reference tracking private activeHooks = new Set() @@ -66,10 +73,13 @@ export class CacheService { /** * Get value from memory cache */ - get(key: string): T | undefined { + get(key: K): UseCacheSchema[K] + get(key: Exclude): T | undefined + get(key: string): any { const entry = this.memoryCache.get(key) - if (!entry) return undefined - + if (entry === undefined) { + return undefined + } // Check TTL (lazy cleanup) if (entry.expireAt && Date.now() > entry.expireAt) { this.memoryCache.delete(key) @@ -77,13 +87,15 @@ export class CacheService { return undefined } - return entry.value as T + return entry.value } /** * Set value in memory cache */ - set(key: string, value: T, ttl?: number): void { + set(key: K, value: UseCacheSchema[K]): void + set(key: Exclude, value: T, ttl?: number): void + set(key: string, value: any, ttl?: number): void { const existingEntry = this.memoryCache.get(key) // Value comparison optimization @@ -99,7 +111,7 @@ export class CacheService { return // Skip notification } - const entry: CacheEntry = { + const entry: CacheEntry = { value, expireAt: ttl ? Date.now() + ttl : undefined } @@ -112,9 +124,14 @@ export class CacheService { /** * Check if key exists in memory cache */ + + has(key: K): boolean + has(key: Exclude): boolean has(key: string): boolean { const entry = this.memoryCache.get(key) - if (!entry) return false + if (entry === undefined) { + return false + } // Check TTL if (entry.expireAt && Date.now() > entry.expireAt) { @@ -129,6 +146,8 @@ export class CacheService { /** * Delete from memory cache */ + delete(key: K): boolean + delete(key: Exclude): boolean delete(key: string): boolean { // Check if key is being used by hooks if (this.activeHooks.has(key)) { @@ -151,6 +170,8 @@ export class CacheService { /** * Check if a key has TTL set (for warning purposes) */ + hasTTL(key: K): boolean + hasTTL(key: Exclude): boolean hasTTL(key: string): boolean { const entry = this.memoryCache.get(key) return entry?.expireAt !== undefined @@ -159,6 +180,8 @@ export class CacheService { /** * Check if a shared cache key has TTL set (for warning purposes) */ + hasSharedTTL(key: K): boolean + hasSharedTTL(key: Exclude): boolean hasSharedTTL(key: string): boolean { const entry = this.sharedCache.get(key) return entry?.expireAt !== undefined @@ -169,7 +192,9 @@ export class CacheService { /** * Get value from shared cache */ - getShared(key: string): T | undefined { + getShared(key: K): UseSharedCacheSchema[K] + getShared(key: Exclude): T | undefined + getShared(key: string): any { const entry = this.sharedCache.get(key) if (!entry) return undefined @@ -180,13 +205,15 @@ export class CacheService { return undefined } - return entry.value as T + return entry.value } /** * Set value in shared cache */ - setShared(key: string, value: T, ttl?: number): void { + setShared(key: K, value: UseSharedCacheSchema[K]): void + setShared(key: Exclude, value: T, ttl?: number): void + setShared(key: string, value: any, ttl?: number): void { const existingEntry = this.sharedCache.get(key) // Value comparison optimization @@ -209,7 +236,7 @@ export class CacheService { return // Skip local update and notification } - const entry: CacheEntry = { + const entry: CacheEntry = { value, expireAt: ttl ? Date.now() + ttl : undefined } @@ -231,6 +258,8 @@ export class CacheService { /** * Check if key exists in shared cache */ + hasShared(key: K): boolean + hasShared(key: Exclude): boolean hasShared(key: string): boolean { const entry = this.sharedCache.get(key) if (!entry) return false @@ -248,6 +277,8 @@ export class CacheService { /** * Delete from shared cache */ + deleteShared(key: K): boolean + deleteShared(key: Exclude): boolean deleteShared(key: string): boolean { // Check if key is being used by hooks if (this.activeHooks.has(key)) { @@ -279,14 +310,14 @@ export class CacheService { /** * Get value from persist cache */ - getPersist(key: K): PersistCacheSchema[K] { + getPersist(key: K): RendererPersistCacheSchema[K] { const value = this.persistCache.get(key) if (value !== undefined) { return value } // Fallback to default value if somehow missing - const defaultValue = DefaultPersistCache[key] + const defaultValue = DefaultRendererPersistCache[key] this.persistCache.set(key, defaultValue) this.schedulePersistSave() logger.warn(`Missing persist cache key "${key}", using default value`) @@ -296,7 +327,7 @@ export class CacheService { /** * Set value in persist cache */ - setPersist(key: K, value: PersistCacheSchema[K]): void { + setPersist(key: K, value: RendererPersistCacheSchema[K]): void { const existingValue = this.persistCache.get(key) // Use deep comparison for persist cache (usually objects) @@ -323,7 +354,7 @@ export class CacheService { /** * Check if key exists in persist cache */ - hasPersist(key: PersistCacheKey): boolean { + hasPersist(key: RendererPersistCacheKey): boolean { return this.persistCache.has(key) } @@ -424,8 +455,8 @@ export class CacheService { */ private loadPersistCache(): void { // First, initialize with default values - for (const [key, defaultValue] of Object.entries(DefaultPersistCache)) { - this.persistCache.set(key as PersistCacheKey, defaultValue) + for (const [key, defaultValue] of Object.entries(DefaultRendererPersistCache)) { + this.persistCache.set(key as RendererPersistCacheKey, defaultValue) } try { @@ -440,7 +471,7 @@ export class CacheService { const data = JSON.parse(stored) // Only load keys that exist in schema, overriding defaults - const schemaKeys = Object.keys(DefaultPersistCache) as PersistCacheKey[] + const schemaKeys = Object.keys(DefaultRendererPersistCache) as RendererPersistCacheKey[] for (const key of schemaKeys) { if (key in data) { this.persistCache.set(key, data[key]) @@ -536,7 +567,7 @@ export class CacheService { this.notifySubscribers(message.key) } else if (message.type === 'persist') { // Update persist cache (other windows only update memory, not localStorage) - this.persistCache.set(message.key as PersistCacheKey, message.value) + this.persistCache.set(message.key as RendererPersistCacheKey, message.value) this.notifySubscribers(message.key) } }) diff --git a/src/renderer/src/data/hooks/useCache.ts b/src/renderer/src/data/hooks/useCache.ts index 3c147b9179..96a6dce958 100644 --- a/src/renderer/src/data/hooks/useCache.ts +++ b/src/renderer/src/data/hooks/useCache.ts @@ -1,8 +1,15 @@ import { cacheService } from '@data/CacheService' import { loggerService } from '@logger' -import type { PersistCacheKey, PersistCacheSchema } from '@shared/data/cache/cacheSchemas' +import type { + RendererPersistCacheKey, + RendererPersistCacheSchema, + UseCacheKey, + UseCacheSchema, + UseSharedCacheKey, + UseSharedCacheSchema +} from '@shared/data/cache/cacheSchemas' +import { DefaultUseCache, DefaultUseSharedCache } from '@shared/data/cache/cacheSchemas' import { useCallback, useEffect, useSyncExternalStore } from 'react' - const logger = loggerService.withContext('useCache') /** @@ -15,23 +22,32 @@ const logger = loggerService.withContext('useCache') * - TTL support with warning when used * * @param key - Cache key - * @param defaultValue - Default value (set automatically if not exists) + * @param initValue - Default value (set automatically if not exists) * @returns [value, setValue] */ -export function useCache(key: string, defaultValue?: T): [T | undefined, (value: T) => void] { +export function useCache( + key: K, + initValue?: UseCacheSchema[K] +): [UseCacheSchema[K], (value: UseCacheSchema[K]) => void] { // Subscribe to cache changes const value = useSyncExternalStore( useCallback((callback) => cacheService.subscribe(key, callback), [key]), - useCallback(() => cacheService.get(key), [key]), - useCallback(() => cacheService.get(key), [key]) // SSR snapshot + useCallback(() => cacheService.get(key), [key]), + useCallback(() => cacheService.get(key), [key]) // SSR snapshot ) // Set default value if not exists useEffect(() => { - if (defaultValue !== undefined && !cacheService.has(key)) { - cacheService.set(key, defaultValue) + if (cacheService.has(key)) { + return } - }, [key, defaultValue]) + + if (initValue === undefined) { + cacheService.set(key, DefaultUseCache[key]) + } else { + cacheService.set(key, initValue) + } + }, [key, initValue]) // Register hook lifecycle useEffect(() => { @@ -49,13 +65,13 @@ export function useCache(key: string, defaultValue?: T): [T | undefined, (val }, [key]) const setValue = useCallback( - (newValue: T) => { + (newValue: UseCacheSchema[K]) => { cacheService.set(key, newValue) }, [key] ) - return [value ?? defaultValue, setValue] + return [value ?? initValue ?? DefaultUseCache[key], setValue] } /** @@ -68,23 +84,32 @@ export function useCache(key: string, defaultValue?: T): [T | undefined, (val * - Hook lifecycle management * * @param key - Cache key - * @param defaultValue - Default value (set automatically if not exists) + * @param initValue - Default value (set automatically if not exists) * @returns [value, setValue] */ -export function useSharedCache(key: string, defaultValue?: T): [T | undefined, (value: T) => void] { +export function useSharedCache( + key: K, + initValue?: UseSharedCacheSchema[K] +): [UseSharedCacheSchema[K], (value: UseSharedCacheSchema[K]) => void] { // Subscribe to cache changes const value = useSyncExternalStore( useCallback((callback) => cacheService.subscribe(key, callback), [key]), - useCallback(() => cacheService.getShared(key), [key]), - useCallback(() => cacheService.getShared(key), [key]) // SSR snapshot + useCallback(() => cacheService.getShared(key), [key]), + useCallback(() => cacheService.getShared(key), [key]) // SSR snapshot ) // Set default value if not exists useEffect(() => { - if (defaultValue !== undefined && !cacheService.hasShared(key)) { - cacheService.setShared(key, defaultValue) + if (cacheService.hasShared(key)) { + return } - }, [key, defaultValue]) + + if (initValue === undefined) { + cacheService.setShared(key, DefaultUseSharedCache[key]) + } else { + cacheService.setShared(key, initValue) + } + }, [key, initValue]) // Register hook lifecycle useEffect(() => { @@ -102,13 +127,13 @@ export function useSharedCache(key: string, defaultValue?: T): [T | undefined }, [key]) const setValue = useCallback( - (newValue: T) => { + (newValue: UseSharedCacheSchema[K]) => { cacheService.setShared(key, newValue) }, [key] ) - return [value ?? defaultValue, setValue] + return [value ?? initValue ?? DefaultUseSharedCache[key], setValue] } /** @@ -123,9 +148,9 @@ export function useSharedCache(key: string, defaultValue?: T): [T | undefined * @param key - Predefined persist cache key * @returns [value, setValue] */ -export function usePersistCache( +export function usePersistCache( key: K -): [PersistCacheSchema[K], (value: PersistCacheSchema[K]) => void] { +): [RendererPersistCacheSchema[K], (value: RendererPersistCacheSchema[K]) => void] { // Subscribe to cache changes const value = useSyncExternalStore( useCallback((callback) => cacheService.subscribe(key, callback), [key]), @@ -140,7 +165,7 @@ export function usePersistCache( }, [key]) const setValue = useCallback( - (newValue: PersistCacheSchema[K]) => { + (newValue: RendererPersistCacheSchema[K]) => { cacheService.setPersist(key, newValue) }, [key] diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts index 2c39b80692..14a6fc5525 100644 --- a/src/renderer/src/hooks/useAppInit.ts +++ b/src/renderer/src/hooks/useAppInit.ts @@ -1,9 +1,11 @@ +import { cacheService } from '@data/CacheService' import { usePreference } from '@data/hooks/usePreference' import { loggerService } from '@logger' import { isMac } from '@renderer/config/constant' import { isLocalAi } from '@renderer/config/env' import { useTheme } from '@renderer/context/ThemeProvider' import db from '@renderer/databases' +import { useAppUpdateHandler, useAppUpdateState } from '@renderer/hooks/useAppUpdate' import i18n from '@renderer/i18n' import KnowledgeQueue from '@renderer/queue/KnowledgeQueue' import MemoryService from '@renderer/services/MemoryService' @@ -11,7 +13,6 @@ 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 { delay, runAsyncFunction } from '@renderer/utils' import { checkDataLimit } from '@renderer/utils' import { defaultLanguage } from '@shared/config/constant' @@ -21,9 +22,8 @@ import { useEffect } from 'react' import { useDefaultModel } from './useAssistant' import useFullScreenNotice from './useFullScreenNotice' +import { useMinapps } from './useMinapps' import { useNavbarPosition } from './useNavbar' -import { useRuntime } from './useRuntime' -import useUpdateHandler from './useUpdateHandler' const logger = loggerService.withContext('useAppInit') export function useAppInit() { @@ -38,9 +38,10 @@ export function useAppInit() { const [enableDataCollection] = usePreference('app.privacy.data_collection.enabled') const { isTopNavbar } = useNavbarPosition() - const { minappShow } = useRuntime() + const { minappShow } = useMinapps() + const { updateAppUpdateState } = useAppUpdateState() const { setDefaultModel, setQuickModel, setTranslateModel } = useDefaultModel() - const avatar = useLiveQuery(() => db.settings.get('image://avatar')) + const savedAvatar = useLiveQuery(() => db.settings.get('image://avatar')) const { theme } = useTheme() const memoryConfig = useAppSelector(selectMemoryConfig) @@ -67,12 +68,12 @@ export function useAppInit() { }) }, []) - useUpdateHandler() + useAppUpdateHandler() useFullScreenNotice() useEffect(() => { - avatar?.value && dispatch(setAvatar(avatar.value)) - }, [avatar, dispatch]) + savedAvatar?.value && cacheService.set('avatar', savedAvatar.value) + }, [savedAvatar, dispatch]) useEffect(() => { runAsyncFunction(async () => { @@ -80,10 +81,10 @@ export function useAppInit() { if (isPackaged && autoCheckUpdate) { await delay(2) const { updateInfo } = await window.api.checkForUpdate() - dispatch(setUpdateState({ info: updateInfo })) + updateAppUpdateState({ info: updateInfo }) } }) - }, [dispatch, autoCheckUpdate]) + }, [autoCheckUpdate, updateAppUpdateState]) useEffect(() => { if (proxyMode === 'system') { @@ -125,8 +126,8 @@ export function useAppInit() { useEffect(() => { // set files path window.api.getAppInfo().then((info) => { - dispatch(setFilesPath(info.filesPath)) - dispatch(setResourcesPath(info.resourcesPath)) + cacheService.set('filesPath', info.filesPath) + cacheService.set('resourcesPath', info.resourcesPath) }) }, [dispatch]) diff --git a/src/renderer/src/hooks/useUpdateHandler.ts b/src/renderer/src/hooks/useAppUpdate.ts similarity index 55% rename from src/renderer/src/hooks/useUpdateHandler.ts rename to src/renderer/src/hooks/useAppUpdate.ts index 0b5282f0c3..cc085fc960 100644 --- a/src/renderer/src/hooks/useUpdateHandler.ts +++ b/src/renderer/src/hooks/useAppUpdate.ts @@ -1,15 +1,28 @@ +import { useCache } from '@data/hooks/useCache' import { NotificationService } from '@renderer/services/NotificationService' -import { useAppDispatch } from '@renderer/store' -import { setUpdateState } from '@renderer/store/runtime' import { uuid } from '@renderer/utils' +import type { CacheAppUpdateState } from '@shared/data/cache/cacheValueTypes' import { IpcChannel } from '@shared/IpcChannel' import type { ProgressInfo, UpdateInfo } from 'builder-util-runtime' import { useEffect } from 'react' import { useTranslation } from 'react-i18next' +export const useAppUpdateState = () => { + const [appUpdateState, setAppUpdateState] = useCache('app.dist.update_state') -export default function useUpdateHandler() { - const dispatch = useAppDispatch() + const updateAppUpdateState = (state: Partial) => { + setAppUpdateState({ ...appUpdateState, ...state }) + } + + return { + appUpdateState, + updateAppUpdateState + } +} + +//TODO: 这个函数是从useUpdateHandler中复制过来的,是v2数据重构时调整的,但这个函数本身需要重构和优化(并不需要用在use中)。by fullex +export function useAppUpdateHandler() { const { t } = useTranslation() + const { updateAppUpdateState } = useAppUpdateState() const notificationService = NotificationService.getInstance() useEffect(() => { @@ -19,7 +32,7 @@ export default function useUpdateHandler() { const removers = [ ipcRenderer.on(IpcChannel.UpdateNotAvailable, () => { - dispatch(setUpdateState({ checking: false })) + updateAppUpdateState({ checking: false }) if (window.location.hash.includes('settings/about')) { window.toast.success(t('settings.about.updateNotAvailable')) } @@ -34,48 +47,38 @@ export default function useUpdateHandler() { source: 'update', channel: 'system' }) - dispatch( - setUpdateState({ - checking: false, - downloading: true, - info: releaseInfo, - available: true - }) - ) + updateAppUpdateState({ + checking: false, + downloading: true, + info: releaseInfo, + available: true + }) }), ipcRenderer.on(IpcChannel.DownloadUpdate, () => { - dispatch( - setUpdateState({ - checking: false, - downloading: true - }) - ) + updateAppUpdateState({ + checking: false, + downloading: true + }) }), ipcRenderer.on(IpcChannel.DownloadProgress, (_, progress: ProgressInfo) => { - dispatch( - setUpdateState({ - downloading: progress.percent < 100, - downloadProgress: progress.percent - }) - ) + updateAppUpdateState({ + downloading: progress.percent < 100, + downloadProgress: progress.percent + }) }), ipcRenderer.on(IpcChannel.UpdateDownloaded, (_, releaseInfo: UpdateInfo) => { - dispatch( - setUpdateState({ - downloading: false, - info: releaseInfo, - downloaded: true - }) - ) + updateAppUpdateState({ + downloading: false, + info: releaseInfo, + downloaded: true + }) }), ipcRenderer.on(IpcChannel.UpdateError, (_, error) => { - dispatch( - setUpdateState({ - checking: false, - downloading: false, - downloadProgress: 0 - }) - ) + updateAppUpdateState({ + checking: false, + downloading: false, + downloadProgress: 0 + }) if (window.location.hash.includes('settings/about')) { window.modal.info({ title: t('settings.about.updateError'), @@ -86,5 +89,5 @@ export default function useUpdateHandler() { }) ] return () => removers.forEach((remover) => remover()) - }, [dispatch, notificationService, t]) + }, [notificationService, t, updateAppUpdateState]) } diff --git a/src/renderer/src/hooks/useAvatar.ts b/src/renderer/src/hooks/useAvatar.ts index 0c57d15782..73528279ef 100644 --- a/src/renderer/src/hooks/useAvatar.ts +++ b/src/renderer/src/hooks/useAvatar.ts @@ -1,5 +1,7 @@ -import { useAppSelector } from '@renderer/store' +import { useCache } from '@data/hooks/useCache' +import { UserAvatar } from '@renderer/config/env' export default function useAvatar() { - return useAppSelector((state) => state.runtime.avatar) + const [avatar] = useCache('app.user.avatar', UserAvatar) + return avatar } diff --git a/src/renderer/src/hooks/useChatContext.ts b/src/renderer/src/hooks/useChatContext.ts index 415ab0c616..d4a2183fee 100644 --- a/src/renderer/src/hooks/useChatContext.ts +++ b/src/renderer/src/hooks/useChatContext.ts @@ -1,46 +1,49 @@ +import { useCache } from '@data/hooks/useCache' import { loggerService } from '@logger' import { useMessageOperations } from '@renderer/hooks/useMessageOperations' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { RootState } from '@renderer/store' import { messageBlocksSelectors } from '@renderer/store/messageBlock' import { selectMessagesForTopic } from '@renderer/store/newMessage' -import { setActiveTopic, setSelectedMessageIds, toggleMultiSelectMode } from '@renderer/store/runtime' +// import { setActiveTopic, setSelectedMessageIds, toggleMultiSelectMode } from '@renderer/store/runtime' import { Topic } from '@renderer/types' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useDispatch, useSelector, useStore } from 'react-redux' - +import { useStore } from 'react-redux' const logger = loggerService.withContext('useChatContext') export const useChatContext = (activeTopic: Topic) => { const { t } = useTranslation() - const dispatch = useDispatch() const store = useStore() const { deleteMessage } = useMessageOperations(activeTopic) + const [isMultiSelectMode, setIsMultiSelectMode] = useCache('chat.multi_select_mode') + const [selectedMessageIds, setSelectedMessageIds] = useCache('chat.selected_message_ids') + const [, setActiveTopic] = useCache('topic.active') + const [messageRefs, setMessageRefs] = useState>(new Map()) - const isMultiSelectMode = useSelector((state: RootState) => state.runtime.chat.isMultiSelectMode) - const selectedMessageIds = useSelector((state: RootState) => state.runtime.chat.selectedMessageIds) - - useEffect(() => { - const unsubscribe = EventEmitter.on(EVENT_NAMES.CHANGE_TOPIC, () => { - dispatch(toggleMultiSelectMode(false)) - }) - return () => unsubscribe() - }, [dispatch]) - - useEffect(() => { - dispatch(setActiveTopic(activeTopic)) - }, [dispatch, activeTopic]) - const handleToggleMultiSelectMode = useCallback( (value: boolean) => { - dispatch(toggleMultiSelectMode(value)) + setIsMultiSelectMode(value) + if (!value) { + setSelectedMessageIds([]) + } }, - [dispatch] + [setIsMultiSelectMode, setSelectedMessageIds] ) + useEffect(() => { + const unsubscribe = EventEmitter.on(EVENT_NAMES.CHANGE_TOPIC, () => { + handleToggleMultiSelectMode(false) + }) + return () => unsubscribe() + }, [handleToggleMultiSelectMode]) + + useEffect(() => { + setActiveTopic(activeTopic) + }, [activeTopic, setActiveTopic]) + const registerMessageElement = useCallback((id: string, element: HTMLElement | null) => { setMessageRefs((prev) => { const newRefs = new Map(prev) @@ -81,17 +84,15 @@ export const useChatContext = (activeTopic: Topic) => { const handleSelectMessage = useCallback( (messageId: string, selected: boolean) => { - dispatch( - setSelectedMessageIds( - selected - ? selectedMessageIds.includes(messageId) - ? selectedMessageIds - : [...selectedMessageIds, messageId] - : selectedMessageIds.filter((id) => id !== messageId) - ) + setSelectedMessageIds( + selected + ? selectedMessageIds.includes(messageId) + ? selectedMessageIds + : [...selectedMessageIds, messageId] + : selectedMessageIds.filter((id) => id !== messageId) ) }, - [dispatch, selectedMessageIds] + [selectedMessageIds, setSelectedMessageIds] ) const handleMultiSelectAction = useCallback( diff --git a/src/renderer/src/hooks/useMinappPopup.ts b/src/renderer/src/hooks/useMinappPopup.ts index 22c5bd337f..3f473dcfd1 100644 --- a/src/renderer/src/hooks/useMinappPopup.ts +++ b/src/renderer/src/hooks/useMinappPopup.ts @@ -1,14 +1,7 @@ import { usePreference } from '@data/hooks/usePreference' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' -import { useRuntime } from '@renderer/hooks/useRuntime' +import { useMinapps } from '@renderer/hooks/useMinapps' import TabsService from '@renderer/services/TabsService' -import { useAppDispatch } from '@renderer/store' -import { - setCurrentMinappId, - setMinappShow, - setOpenedKeepAliveMinapps, - setOpenedOneOffMinapp -} from '@renderer/store/runtime' import { MinAppType } from '@renderer/types' import { clearWebviewState } from '@renderer/utils/webviewStateManager' import { LRUCache } from 'lru-cache' @@ -31,8 +24,15 @@ let minAppsCache: LRUCache * const { openedKeepAliveMinapps, openedOneOffMinapp, minappShow } = useRuntime() */ export const useMinappPopup = () => { - const dispatch = useAppDispatch() - const { openedKeepAliveMinapps, openedOneOffMinapp, minappShow } = useRuntime() + const { + openedKeepAliveMinapps, + openedOneOffMinapp, + minappShow, + setOpenedKeepAliveMinapps, + setOpenedOneOffMinapp, + setCurrentMinappId, + setMinappShow + } = useMinapps() const [maxKeepAliveMinapps] = usePreference('feature.minapp.max_keep_alive') const createLRUCache = useCallback(() => { @@ -50,15 +50,15 @@ export const useMinappPopup = () => { } // Update Redux state - dispatch(setOpenedKeepAliveMinapps(Array.from(minAppsCache.values()))) + setOpenedKeepAliveMinapps(Array.from(minAppsCache.values())) }, onInsert: () => { - dispatch(setOpenedKeepAliveMinapps(Array.from(minAppsCache.values()))) + setOpenedKeepAliveMinapps(Array.from(minAppsCache.values())) }, updateAgeOnGet: true, updateAgeOnHas: true }) - }, [dispatch, maxKeepAliveMinapps]) + }, [maxKeepAliveMinapps, setOpenedKeepAliveMinapps]) // 缓存不存在 if (!minAppsCache) { @@ -89,23 +89,23 @@ export const useMinappPopup = () => { // 如果小程序已经打开,只切换显示 if (openedKeepAliveMinapps.some((item) => item.id === app.id)) { - dispatch(setCurrentMinappId(app.id)) - dispatch(setMinappShow(true)) + setCurrentMinappId(app.id) + setMinappShow(true) return } - dispatch(setOpenedOneOffMinapp(null)) - dispatch(setCurrentMinappId(app.id)) - dispatch(setMinappShow(true)) + setOpenedOneOffMinapp(null) + setCurrentMinappId(app.id) + setMinappShow(true) return } //if the minapp is not keep alive, open it as one-off minapp - dispatch(setOpenedOneOffMinapp(app)) - dispatch(setCurrentMinappId(app.id)) - dispatch(setMinappShow(true)) + setOpenedOneOffMinapp(app) + setCurrentMinappId(app.id) + setMinappShow(true) return }, - [dispatch, openedKeepAliveMinapps] + [openedKeepAliveMinapps, setOpenedOneOffMinapp, setCurrentMinappId, setMinappShow] ) /** a wrapper of openMinapp(app, true) */ @@ -133,14 +133,14 @@ export const useMinappPopup = () => { if (openedKeepAliveMinapps.some((item) => item.id === appid)) { minAppsCache.delete(appid) } else if (openedOneOffMinapp?.id === appid) { - dispatch(setOpenedOneOffMinapp(null)) + setOpenedOneOffMinapp(null) } - dispatch(setCurrentMinappId('')) - dispatch(setMinappShow(false)) + setCurrentMinappId('') + setMinappShow(false) return }, - [dispatch, openedKeepAliveMinapps, openedOneOffMinapp] + [openedKeepAliveMinapps, openedOneOffMinapp, setOpenedOneOffMinapp, setCurrentMinappId, setMinappShow] ) /** Close all minapps (popup hides and all minapps unloaded) */ @@ -148,22 +148,22 @@ export const useMinappPopup = () => { // minAppsCache.clear 会多次调用 dispose 方法 // 重新创建一个 LRU Cache 替换 minAppsCache = createLRUCache() - dispatch(setOpenedKeepAliveMinapps([])) - dispatch(setOpenedOneOffMinapp(null)) - dispatch(setCurrentMinappId('')) - dispatch(setMinappShow(false)) - }, [dispatch, createLRUCache]) + setOpenedKeepAliveMinapps([]) + setOpenedOneOffMinapp(null) + setCurrentMinappId('') + setMinappShow(false) + }, [createLRUCache, setOpenedKeepAliveMinapps, setOpenedOneOffMinapp, setCurrentMinappId, setMinappShow]) /** Hide the minapp popup (only one-off minapp unloaded) */ const hideMinappPopup = useCallback(() => { if (!minappShow) return if (openedOneOffMinapp) { - dispatch(setOpenedOneOffMinapp(null)) - dispatch(setCurrentMinappId('')) + setOpenedOneOffMinapp(null) + setCurrentMinappId('') } - dispatch(setMinappShow(false)) - }, [dispatch, minappShow, openedOneOffMinapp]) + setMinappShow(false) + }, [minappShow, openedOneOffMinapp, setOpenedOneOffMinapp, setCurrentMinappId, setMinappShow]) return { openMinapp, diff --git a/src/renderer/src/hooks/useMinapps.ts b/src/renderer/src/hooks/useMinapps.ts index 4a8fe5a440..08800cfffa 100644 --- a/src/renderer/src/hooks/useMinapps.ts +++ b/src/renderer/src/hooks/useMinapps.ts @@ -1,3 +1,4 @@ +import { useCache } from '@data/hooks/useCache' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { RootState, useAppDispatch, useAppSelector } from '@renderer/store' import { setDisabledMinApps, setMinApps, setPinnedMinApps } from '@renderer/store/minapps' @@ -7,10 +8,23 @@ export const useMinapps = () => { const { enabled, disabled, pinned } = useAppSelector((state: RootState) => state.minapps) const dispatch = useAppDispatch() + const [openedKeepAliveMinapps, setOpenedKeepAliveMinapps] = useCache('minapp.opened_keep_alive') + const [currentMinappId, setCurrentMinappId] = useCache('minapp.current_id') + const [minappShow, setMinappShow] = useCache('minapp.show') + const [openedOneOffMinapp, setOpenedOneOffMinapp] = useCache('minapp.opened_oneoff') + return { minapps: enabled.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app), disabled: disabled.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app), pinned: pinned.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app), + openedKeepAliveMinapps, + currentMinappId, + minappShow, + openedOneOffMinapp, + setOpenedKeepAliveMinapps, + setCurrentMinappId, + setMinappShow, + setOpenedOneOffMinapp, updateMinapps: (minapps: MinAppType[]) => { dispatch(setMinApps(minapps)) }, diff --git a/src/renderer/src/hooks/useModel.ts b/src/renderer/src/hooks/useModel.ts index 27962e7cef..77fac3383f 100644 --- a/src/renderer/src/hooks/useModel.ts +++ b/src/renderer/src/hooks/useModel.ts @@ -1,3 +1,5 @@ +import { cacheService } from '@data/CacheService' +import i18n from '@renderer/i18n' import store from '@renderer/store' import { useProviders } from './useProvider' @@ -25,3 +27,14 @@ export function getModel(id?: string, providerId?: string) { } }) } + +export function modelGenerating() { + const generating = cacheService.get('generating') ?? false + + if (generating) { + window.toast.warning(i18n.t('message.switch.disabled')) + return Promise.reject() + } + + return Promise.resolve() +} diff --git a/src/renderer/src/hooks/useRuntime.ts b/src/renderer/src/hooks/useRuntime.ts index 184e090230..cb4159c840 100644 --- a/src/renderer/src/hooks/useRuntime.ts +++ b/src/renderer/src/hooks/useRuntime.ts @@ -2,20 +2,8 @@ * Data Refactor, notes by fullex * //TODO @deprecated this file will be removed */ -import i18n from '@renderer/i18n' -import store, { useAppSelector } from '@renderer/store' +import { useAppSelector } from '@renderer/store' export function useRuntime() { return useAppSelector((state) => state.runtime) } - -export function modelGenerating() { - const generating = store.getState().runtime.generating - - if (generating) { - window.toast.warning(i18n.t('message.switch.disabled')) - return Promise.reject() - } - - return Promise.resolve() -} diff --git a/src/renderer/src/hooks/useTopic.ts b/src/renderer/src/hooks/useTopic.ts index 4e30745d49..730c6d4a19 100644 --- a/src/renderer/src/hooks/useTopic.ts +++ b/src/renderer/src/hooks/useTopic.ts @@ -1,3 +1,4 @@ +import { cacheService } from '@data/CacheService' import { preferenceService } from '@data/PreferenceService' import db from '@renderer/databases' import i18n from '@renderer/i18n' @@ -6,7 +7,6 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { deleteMessageFiles } from '@renderer/services/MessagesService' import store from '@renderer/store' import { updateTopic } from '@renderer/store/assistants' -import { setNewlyRenamedTopics, setRenamingTopics } from '@renderer/store/runtime' import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk' import { Assistant, Topic } from '@renderer/types' import { findMainTextBlocks } from '@renderer/utils/messageUtils/find' @@ -71,9 +71,9 @@ export async function getTopicById(topicId: string) { * 开始重命名指定话题 */ export const startTopicRenaming = (topicId: string) => { - const currentIds = store.getState().runtime.chat.renamingTopics + const currentIds = cacheService.get('renamingTopics') ?? [] if (!currentIds.includes(topicId)) { - store.dispatch(setRenamingTopics([...currentIds, topicId])) + cacheService.set('renamingTopics', [...currentIds, topicId]) } } @@ -81,20 +81,26 @@ export const startTopicRenaming = (topicId: string) => { * 完成重命名指定话题 */ export const finishTopicRenaming = (topicId: string) => { - const state = store.getState() - // 1. 立即从 renamingTopics 移除 - const currentRenaming = state.runtime.chat.renamingTopics - store.dispatch(setRenamingTopics(currentRenaming.filter((id) => id !== topicId))) + const renamingTopics = cacheService.get('renamingTopics') + if (renamingTopics && renamingTopics.includes(topicId)) { + cacheService.set( + 'renamingTopics', + renamingTopics.filter((id) => id !== topicId) + ) + } // 2. 立即添加到 newlyRenamedTopics - const currentNewlyRenamed = state.runtime.chat.newlyRenamedTopics - store.dispatch(setNewlyRenamedTopics([...currentNewlyRenamed, topicId])) + const currentNewlyRenamed = cacheService.get('newlyRenamedTopics') ?? [] + cacheService.set('newlyRenamedTopics', [...currentNewlyRenamed, topicId]) // 3. 延迟从 newlyRenamedTopics 移除 setTimeout(() => { - const current = store.getState().runtime.chat.newlyRenamedTopics - store.dispatch(setNewlyRenamedTopics(current.filter((id) => id !== topicId))) + const current = cacheService.get('newlyRenamedTopics') ?? [] + cacheService.set( + 'newlyRenamedTopics', + current.filter((id) => id !== topicId) + ) }, 700) } diff --git a/src/renderer/src/pages/agents/index.ts b/src/renderer/src/pages/agents/index.ts index 2a190c6b86..04274a6fa3 100644 --- a/src/renderer/src/pages/agents/index.ts +++ b/src/renderer/src/pages/agents/index.ts @@ -1,11 +1,10 @@ +import { cacheService } from '@data/CacheService' import { loggerService } from '@logger' -import { useRuntime } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' import store from '@renderer/store' import { Agent } from '@renderer/types' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' - const logger = loggerService.withContext('useSystemAgents') let _agents: Agent[] = [] @@ -24,7 +23,7 @@ export const getAgentsFromSystemAgents = (systemAgents: any) => { export function useSystemAgents() { const { defaultAgent } = useSettings() const [agents, setAgents] = useState([]) - const { resourcesPath } = useRuntime() + const resourcesPath = cacheService.get('resourcesPath') ?? '' const { agentssubscribeUrl } = store.getState().settings const { i18n } = useTranslation() const currentLanguage = i18n.language diff --git a/src/renderer/src/pages/history/components/TopicMessages.tsx b/src/renderer/src/pages/history/components/TopicMessages.tsx index 741933edad..4c7c05aa90 100644 --- a/src/renderer/src/pages/history/components/TopicMessages.tsx +++ b/src/renderer/src/pages/history/components/TopicMessages.tsx @@ -3,12 +3,13 @@ import { usePreference } from '@data/hooks/usePreference' import { HStack } from '@renderer/components/Layout' import SearchPopup from '@renderer/components/Popups/SearchPopup' import { MessageEditingProvider } from '@renderer/context/MessageEditingContext' +import { modelGenerating } from '@renderer/hooks/useModel' import useScrollPosition from '@renderer/hooks/useScrollPosition' import { useTimer } from '@renderer/hooks/useTimer' import { getTopicById } from '@renderer/hooks/useTopic' import { getAssistantById } from '@renderer/services/AssistantService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' -import { isGenerating, locateToMessage } from '@renderer/services/MessagesService' +import { locateToMessage } from '@renderer/services/MessagesService' import NavigationService from '@renderer/services/NavigationService' import { Topic } from '@renderer/types' import { classNames, runAsyncFunction } from '@renderer/utils' @@ -47,7 +48,7 @@ const TopicMessages: FC = ({ topic: _topic, ...props }) => { } const onContinueChat = async (topic: Topic) => { - await isGenerating() + await modelGenerating() SearchPopup.hide() const assistant = getAssistantById(topic.assistantId) navigate('/', { state: { assistant, topic } }) diff --git a/src/renderer/src/pages/home/ChatNavbar.tsx b/src/renderer/src/pages/home/ChatNavbar.tsx index 5a4df0874a..3edd93fb57 100644 --- a/src/renderer/src/pages/home/ChatNavbar.tsx +++ b/src/renderer/src/pages/home/ChatNavbar.tsx @@ -3,7 +3,7 @@ import { NavbarHeader } from '@renderer/components/app/Navbar' import { HStack } from '@renderer/components/Layout' import SearchPopup from '@renderer/components/Popups/SearchPopup' import { useAssistant } from '@renderer/hooks/useAssistant' -import { modelGenerating } from '@renderer/hooks/useRuntime' +import { modelGenerating } from '@renderer/hooks/useModel' import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 3772beb17c..f77ade2da1 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -1,4 +1,5 @@ import { HolderOutlined } from '@ant-design/icons' +import { useCache } from '@data/hooks/useCache' import { usePreference } from '@data/hooks/usePreference' import { loggerService } from '@logger' import { QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel' @@ -18,7 +19,7 @@ import db from '@renderer/databases' import { useAssistant } from '@renderer/hooks/useAssistant' import { useKnowledgeBases } from '@renderer/hooks/useKnowledge' import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations' -import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime' +import { modelGenerating } from '@renderer/hooks/useModel' import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts' import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon' import { useTimer } from '@renderer/hooks/useTimer' @@ -33,8 +34,7 @@ import { spanManagerService } from '@renderer/services/SpanManagerService' import { estimateTextTokens as estimateTxtTokens, estimateUserPromptUsage } from '@renderer/services/TokenService' import { translateText } from '@renderer/services/TranslateService' import WebSearchService from '@renderer/services/WebSearchService' -import { useAppDispatch, useAppSelector } from '@renderer/store' -import { setSearching } from '@renderer/store/runtime' +import { useAppDispatch } from '@renderer/store' import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk' import { Assistant, FileType, FileTypes, KnowledgeBase, KnowledgeItem, Model, Topic } from '@renderer/types' import type { MessageInputBaseParams } from '@renderer/types/newMessage' @@ -100,7 +100,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const { t } = useTranslation() const { getLanguageByLangcode } = useTranslate() const containerRef = useRef(null) - const { searching } = useRuntime() + const [searching, setSearching] = useCache('chat.websearch.searching') const { pauseMessages } = useMessageOperations(topic) const loading = useTopicLoading(topic) const dispatch = useAppDispatch() @@ -115,7 +115,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const startDragY = useRef(0) const startHeight = useRef(0) const { bases: knowledgeBases } = useKnowledgeBases() - const isMultiSelectMode = useAppSelector((state) => state.runtime.chat.isMultiSelectMode) + const [isMultiSelectMode] = useCache('chat.multi_select_mode') const isVisionAssistant = useMemo(() => isVisionModel(model), [model]) const isGenerateImageAssistant = useMemo(() => isGenerateImageModel(model), [model]) const { setTimeoutTimer } = useTimer() @@ -911,7 +911,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = disabled={searching} onPaste={(e) => onPaste(e.nativeEvent)} onClick={() => { - searching && dispatch(setSearching(false)) + searching && setSearching(false) quickPanel.close() }} /> diff --git a/src/renderer/src/pages/home/Messages/Blocks/CitationBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/CitationBlock.tsx index 3f96c6f99a..7a4aae7cd0 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/CitationBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/CitationBlock.tsx @@ -1,3 +1,4 @@ +import { cacheService } from '@data/CacheService' import { GroundingMetadata } from '@google/genai' import Spinner from '@renderer/components/Spinner' import type { RootState } from '@renderer/store' @@ -14,7 +15,7 @@ import CitationsList from '../CitationsList' function CitationBlock({ block }: { block: CitationMessageBlock }) { const { t } = useTranslation() const formattedCitations = useSelector((state: RootState) => selectFormattedCitationsByBlockId(state, block.id)) - const { websearch } = useSelector((state: RootState) => state.runtime) + // const { websearch } = useSelector((state: RootState) => state.runtime) const message = useSelector((state: RootState) => state.messages.entities[block.messageId]) const userMessageId = message?.askId || block.messageId // 如果没有 askId 则回退到 messageId @@ -29,7 +30,7 @@ function CitationBlock({ block }: { block: CitationMessageBlock }) { }, [formattedCitations, block.knowledge, block.memories, hasGeminiBlock]) const getWebSearchStatusText = (requestId: string) => { - const status = websearch.activeSearches[requestId] ?? { phase: 'default' } + const status = cacheService.get('activeSearches')?.[requestId] ?? { phase: 'default' } switch (status.phase) { case 'fetch_complete': diff --git a/src/renderer/src/pages/home/Navbar.tsx b/src/renderer/src/pages/home/Navbar.tsx index 7d132aca13..1181c3e768 100644 --- a/src/renderer/src/pages/home/Navbar.tsx +++ b/src/renderer/src/pages/home/Navbar.tsx @@ -4,7 +4,7 @@ import { HStack } from '@renderer/components/Layout' import SearchPopup from '@renderer/components/Popups/SearchPopup' import { isLinux, isWin } from '@renderer/config/constant' import { useAssistant } from '@renderer/hooks/useAssistant' -import { modelGenerating } from '@renderer/hooks/useRuntime' +import { modelGenerating } from '@renderer/hooks/useModel' import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index aca37e25b1..47d9b7d406 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -1,3 +1,4 @@ +import { useCache } from '@data/hooks/useCache' import { usePreference } from '@data/hooks/usePreference' import { useMultiplePreferences } from '@data/hooks/usePreference' import { DraggableVirtualList } from '@renderer/components/DraggableList' @@ -8,15 +9,13 @@ import SaveToKnowledgePopup from '@renderer/components/Popups/SaveToKnowledgePop import { isMac } from '@renderer/config/constant' import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant' import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit' +import { modelGenerating } from '@renderer/hooks/useModel' import { useNotesSettings } from '@renderer/hooks/useNotesSettings' -import { modelGenerating } from '@renderer/hooks/useRuntime' import { finishTopicRenaming, startTopicRenaming, TopicManager } from '@renderer/hooks/useTopic' import { fetchMessagesSummary } from '@renderer/services/ApiService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' -import store from '@renderer/store' import { RootState } from '@renderer/store' import { newMessagesActions } from '@renderer/store/newMessage' -import { setGenerating } from '@renderer/store/runtime' import { Assistant, Topic } from '@renderer/types' import { classNames, removeSpecialCharactersForFileName } from '@renderer/utils' import { copyTopicAsMarkdown, copyTopicAsPlainText } from '@renderer/utils/copy' @@ -52,7 +51,6 @@ import { FC, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' - // const logger = loggerService.withContext('TopicsTab') interface Props { @@ -67,15 +65,18 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, const [showTopicTime] = usePreference('topic.tab.show_time') const [pinTopicsToTop] = usePreference('topic.tab.pin_to_top') + const [, setGenerating] = useCache('chat.generating') + const { t } = useTranslation() const { notesPath } = useNotesSettings() const { assistants } = useAssistants() const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id) - const renamingTopics = useSelector((state: RootState) => state.runtime.chat.renamingTopics) + const [renamingTopics] = useCache('topic.renaming') + const [newlyRenamedTopics] = useCache('topic.newly_renamed') + const topicLoadingQuery = useSelector((state: RootState) => state.messages.loadingByTopic) const topicFulfilledQuery = useSelector((state: RootState) => state.messages.fulfilledByTopic) - const newlyRenamedTopics = useSelector((state: RootState) => state.runtime.chat.newlyRenamedTopics) const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)' @@ -132,11 +133,14 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, deleteTimerRef.current = setTimeout(() => setDeletingTopicId(null), 2000) }, []) - const onClearMessages = useCallback((topic: Topic) => { - // window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true) - store.dispatch(setGenerating(false)) - EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES, topic) - }, []) + const onClearMessages = useCallback( + (topic: Topic) => { + // window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true) + setGenerating(false) + EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES, topic) + }, + [setGenerating] + ) const handleConfirmDelete = useCallback( async (topic: Topic, e: React.MouseEvent) => { diff --git a/src/renderer/src/pages/home/components/UpdateAppButton.tsx b/src/renderer/src/pages/home/components/UpdateAppButton.tsx index ba0ad1c20d..e16f90c471 100644 --- a/src/renderer/src/pages/home/components/UpdateAppButton.tsx +++ b/src/renderer/src/pages/home/components/UpdateAppButton.tsx @@ -1,21 +1,21 @@ import { SyncOutlined } from '@ant-design/icons' import { usePreference } from '@data/hooks/usePreference' -import { useRuntime } from '@renderer/hooks/useRuntime' +import { useAppUpdateState } from '@renderer/hooks/useAppUpdate' import { Button } from 'antd' import { FC } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' const UpdateAppButton: FC = () => { - const { update } = useRuntime() + const { appUpdateState } = useAppUpdateState() const [autoCheckUpdate] = usePreference('app.dist.auto_update.enabled') const { t } = useTranslation() - if (!update) { + if (!appUpdateState) { return null } - if (!update.downloaded || !autoCheckUpdate) { + if (!appUpdateState.downloaded || !autoCheckUpdate) { return null } diff --git a/src/renderer/src/pages/launchpad/LaunchpadPage.tsx b/src/renderer/src/pages/launchpad/LaunchpadPage.tsx index ae7ee55c79..d284f19b68 100644 --- a/src/renderer/src/pages/launchpad/LaunchpadPage.tsx +++ b/src/renderer/src/pages/launchpad/LaunchpadPage.tsx @@ -1,6 +1,5 @@ import App from '@renderer/components/MinApp/MinApp' import { useMinapps } from '@renderer/hooks/useMinapps' -import { useRuntime } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' import { Code, FileSearch, Folder, Languages, LayoutGrid, NotepadText, Palette, Sparkle } from 'lucide-react' import { FC, useMemo } from 'react' @@ -12,8 +11,7 @@ const LaunchpadPage: FC = () => { const navigate = useNavigate() const { t } = useTranslation() const { defaultPaintingProvider } = useSettings() - const { pinned } = useMinapps() - const { openedKeepAliveMinapps } = useRuntime() + const { pinned, openedKeepAliveMinapps } = useMinapps() const appMenuItems = [ { diff --git a/src/renderer/src/pages/paintings/AihubmixPage.tsx b/src/renderer/src/pages/paintings/AihubmixPage.tsx index a6cf4a108d..60a49664c9 100644 --- a/src/renderer/src/pages/paintings/AihubmixPage.tsx +++ b/src/renderer/src/pages/paintings/AihubmixPage.tsx @@ -1,4 +1,5 @@ import { PlusOutlined, RedoOutlined } from '@ant-design/icons' +import { useCache } from '@data/hooks/useCache' import { usePreference } from '@data/hooks/usePreference' import { loggerService } from '@logger' import AiProvider from '@renderer/aiCore' @@ -13,12 +14,9 @@ import { LanguagesEnum } from '@renderer/config/translate' import { useTheme } from '@renderer/context/ThemeProvider' import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' -import { useRuntime } from '@renderer/hooks/useRuntime' import { getProviderLabel } from '@renderer/i18n/label' import FileManager from '@renderer/services/FileManager' import { translateText } from '@renderer/services/TranslateService' -import { useAppDispatch } from '@renderer/store' -import { setGenerating } from '@renderer/store/runtime' import type { FileMetadata } from '@renderer/types' import type { PaintingAction, PaintingsState } from '@renderer/types' import { getErrorMessage, uuid } from '@renderer/utils' @@ -90,8 +88,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { } } }) - const dispatch = useAppDispatch() - const { generating } = useRuntime() + const [generating, setGenerating] = useCache('chat.generating') const navigate = useNavigate() const location = useLocation() const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space') @@ -186,7 +183,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { const controller = new AbortController() setAbortController(controller) setIsLoading(true) - dispatch(setGenerating(true)) + setGenerating(true) let body: string | FormData = '' let headers: Record = { @@ -303,7 +300,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { handleError(error) } finally { setIsLoading(false) - dispatch(setGenerating(false)) + setGenerating(false) setAbortController(null) } } else { @@ -523,7 +520,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { handleError(error) } finally { setIsLoading(false) - dispatch(setGenerating(false)) + setGenerating(false) setAbortController(null) } } diff --git a/src/renderer/src/pages/paintings/DmxapiPage.tsx b/src/renderer/src/pages/paintings/DmxapiPage.tsx index 2ffc9fd958..61d8a8995c 100644 --- a/src/renderer/src/pages/paintings/DmxapiPage.tsx +++ b/src/renderer/src/pages/paintings/DmxapiPage.tsx @@ -1,4 +1,5 @@ import { PlusOutlined, RedoOutlined } from '@ant-design/icons' +import { useCache } from '@data/hooks/useCache' import DMXAPIToImg from '@renderer/assets/images/providers/DMXAPI-to-img.webp' import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar' import { HStack } from '@renderer/components/Layout' @@ -7,11 +8,8 @@ import { isMac } from '@renderer/config/constant' import { getProviderLogo } from '@renderer/config/providers' import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' -import { useRuntime } from '@renderer/hooks/useRuntime' import { getProviderLabel } from '@renderer/i18n/label' import FileManager from '@renderer/services/FileManager' -import { useAppDispatch } from '@renderer/store' -import { setGenerating } from '@renderer/store/runtime' import type { FileMetadata } from '@renderer/types' import { convertToBase64, uuid } from '@renderer/utils' import { DmxapiPainting } from '@types' @@ -71,8 +69,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { const [currentImageIndex, setCurrentImageIndex] = useState(0) const [isLoading, setIsLoading] = useState(false) const [abortController, setAbortController] = useState(null) - const dispatch = useAppDispatch() - const { generating } = useRuntime() + const [generating, setGenerating] = useCache('chat.generating') const navigate = useNavigate() const location = useLocation() @@ -558,7 +555,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { // 设置请求状态 const controller = new AbortController() setAbortController(controller) - dispatch(setGenerating(true)) + setGenerating(true) // 准备请求配置 const requestConfig = await prepareRequestConfig(prompt, painting) @@ -602,7 +599,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { } finally { // 清理状态 setIsLoading(false) - dispatch(setGenerating(false)) + setGenerating(false) setAbortController(null) } } diff --git a/src/renderer/src/pages/paintings/NewApiPage.tsx b/src/renderer/src/pages/paintings/NewApiPage.tsx index 2a44fc8ba0..36f479bbed 100644 --- a/src/renderer/src/pages/paintings/NewApiPage.tsx +++ b/src/renderer/src/pages/paintings/NewApiPage.tsx @@ -1,4 +1,5 @@ import { PlusOutlined } from '@ant-design/icons' +import { useCache } from '@data/hooks/useCache' import { usePreference } from '@data/hooks/usePreference' import { loggerService } from '@logger' import AiProvider from '@renderer/aiCore' @@ -12,7 +13,6 @@ import { LanguagesEnum } from '@renderer/config/translate' import { useTheme } from '@renderer/context/ThemeProvider' import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' -import { useRuntime } from '@renderer/hooks/useRuntime' import { getPaintingsBackgroundOptionsLabel, getPaintingsImageSizeOptionsLabel, @@ -24,8 +24,6 @@ import PaintingsList from '@renderer/pages/paintings/components/PaintingsList' import { DEFAULT_PAINTING, MODELS, SUPPORTED_MODELS } from '@renderer/pages/paintings/config/NewApiConfig' import FileManager from '@renderer/services/FileManager' import { translateText } from '@renderer/services/TranslateService' -import { useAppDispatch } from '@renderer/store' -import { setGenerating } from '@renderer/store/runtime' import type { PaintingAction, PaintingsState } from '@renderer/types' import { FileMetadata } from '@renderer/types' import { getErrorMessage, uuid } from '@renderer/utils' @@ -81,8 +79,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => { } } }) - const dispatch = useAppDispatch() - const { generating } = useRuntime() + const [generating, setGenerating] = useCache('chat.generating') const navigate = useNavigate() const location = useLocation() const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space') @@ -251,7 +248,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => { const controller = new AbortController() setAbortController(controller) setIsLoading(true) - dispatch(setGenerating(true)) + setGenerating(true) let body: string | FormData = '' const headers: Record = { @@ -343,7 +340,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => { handleError(error) } finally { setIsLoading(false) - dispatch(setGenerating(false)) + setGenerating(false) setAbortController(null) } } diff --git a/src/renderer/src/pages/paintings/SiliconPage.tsx b/src/renderer/src/pages/paintings/SiliconPage.tsx index 5522814571..9271a51cb7 100644 --- a/src/renderer/src/pages/paintings/SiliconPage.tsx +++ b/src/renderer/src/pages/paintings/SiliconPage.tsx @@ -1,4 +1,5 @@ import { PlusOutlined, RedoOutlined } from '@ant-design/icons' +import { useCache } from '@data/hooks/useCache' import { usePreference } from '@data/hooks/usePreference' import { loggerService } from '@logger' import AiProvider from '@renderer/aiCore' @@ -17,13 +18,10 @@ import { LanguagesEnum } from '@renderer/config/translate' import { useTheme } from '@renderer/context/ThemeProvider' import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' -import { useRuntime } from '@renderer/hooks/useRuntime' import { getProviderLabel } from '@renderer/i18n/label' import { getProviderByModel } from '@renderer/services/AssistantService' import FileManager from '@renderer/services/FileManager' import { translateText } from '@renderer/services/TranslateService' -import { useAppDispatch } from '@renderer/store' -import { setGenerating } from '@renderer/store/runtime' import type { FileMetadata, Painting } from '@renderer/types' import { getErrorMessage, uuid } from '@renderer/utils' import { Button, Input, InputNumber, Radio, Select, Slider, Switch, Tooltip } from 'antd' @@ -128,8 +126,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => { const [isLoading, setIsLoading] = useState(false) const [abortController, setAbortController] = useState(null) - const dispatch = useAppDispatch() - const { generating } = useRuntime() + const [generating, setGenerating] = useCache('chat.generating') const navigate = useNavigate() const location = useLocation() @@ -196,7 +193,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => { const controller = new AbortController() setAbortController(controller) setIsLoading(true) - dispatch(setGenerating(true)) + setGenerating(true) const AI = new AiProvider(provider) if (!painting.model) { @@ -255,7 +252,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => { } } finally { setIsLoading(false) - dispatch(setGenerating(false)) + setGenerating(false) setAbortController(null) } } diff --git a/src/renderer/src/pages/paintings/TokenFluxPage.tsx b/src/renderer/src/pages/paintings/TokenFluxPage.tsx index 26a188d38a..7933cea4d3 100644 --- a/src/renderer/src/pages/paintings/TokenFluxPage.tsx +++ b/src/renderer/src/pages/paintings/TokenFluxPage.tsx @@ -1,4 +1,5 @@ import { PlusOutlined } from '@ant-design/icons' +import { useCache } from '@data/hooks/useCache' import { usePreference } from '@data/hooks/usePreference' import { loggerService } from '@logger' import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar' @@ -9,12 +10,9 @@ import { getProviderLogo } from '@renderer/config/providers' import { LanguagesEnum } from '@renderer/config/translate' import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' -import { useRuntime } from '@renderer/hooks/useRuntime' import { getProviderLabel } from '@renderer/i18n/label' import FileManager from '@renderer/services/FileManager' import { translateText } from '@renderer/services/TranslateService' -import { useAppDispatch } from '@renderer/store' -import { setGenerating } from '@renderer/store/runtime' import type { TokenFluxPainting } from '@renderer/types' import { getErrorMessage, uuid } from '@renderer/utils' import { Avatar, Button, Select, Tooltip } from 'antd' @@ -38,6 +36,7 @@ import TokenFluxService from './utils/TokenFluxService' const logger = loggerService.withContext('TokenFluxPage') const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => { + const [generating, setGenerating] = useCache('chat.generating') const [models, setModels] = useState([]) const [selectedModel, setSelectedModel] = useState(null) const [formData, setFormData] = useState>({}) @@ -70,8 +69,6 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => { } }) - const dispatch = useAppDispatch() - const { generating } = useRuntime() const navigate = useNavigate() const location = useLocation() const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space') @@ -163,7 +160,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => { const controller = new AbortController() setAbortController(controller) setIsLoading(true) - dispatch(setGenerating(true)) + setGenerating(true) try { const requestBody = { @@ -197,12 +194,12 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => { } setIsLoading(false) - dispatch(setGenerating(false)) + setGenerating(false) setAbortController(null) } catch (error: unknown) { handleError(error) setIsLoading(false) - dispatch(setGenerating(false)) + setGenerating(false) setAbortController(null) } } @@ -210,7 +207,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => { const onCancel = () => { abortController?.abort() setIsLoading(false) - dispatch(setGenerating(false)) + setGenerating(false) setAbortController(null) } diff --git a/src/renderer/src/pages/paintings/ZhipuPage.tsx b/src/renderer/src/pages/paintings/ZhipuPage.tsx index ea8d7cfcea..0b19c43876 100644 --- a/src/renderer/src/pages/paintings/ZhipuPage.tsx +++ b/src/renderer/src/pages/paintings/ZhipuPage.tsx @@ -1,4 +1,5 @@ import { PlusOutlined } from '@ant-design/icons' +import { useCache } from '@data/hooks/useCache' import AiProvider from '@renderer/aiCore' import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar' import { HStack } from '@renderer/components/Layout' @@ -7,11 +8,8 @@ import { isMac } from '@renderer/config/constant' import { getProviderLogo } from '@renderer/config/providers' import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' -import { useRuntime } from '@renderer/hooks/useRuntime' import { getProviderLabel } from '@renderer/i18n/label' import FileManager from '@renderer/services/FileManager' -import { useAppDispatch } from '@renderer/store' -import { setGenerating } from '@renderer/store/runtime' import { getErrorMessage, uuid } from '@renderer/utils' import { Avatar, Button, InputNumber, Radio, Select } from 'antd' import TextArea from 'antd/es/input/TextArea' @@ -70,8 +68,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => { const [currentImageIndex, setCurrentImageIndex] = useState(0) const [isLoading, setIsLoading] = useState(false) const [abortController, setAbortController] = useState(null) - const dispatch = useAppDispatch() - const { generating } = useRuntime() + const [generating, setGenerating] = useCache('chat.generating') const navigate = useNavigate() const location = useLocation() @@ -118,7 +115,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => { } setIsLoading(true) - dispatch(setGenerating(true)) + setGenerating(true) const controller = new AbortController() setAbortController(controller) @@ -225,7 +222,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => { } } finally { setIsLoading(false) - dispatch(setGenerating(false)) + setGenerating(false) setAbortController(null) } } diff --git a/src/renderer/src/pages/settings/AboutSettings.tsx b/src/renderer/src/pages/settings/AboutSettings.tsx index cdd2ba460e..698b1b531b 100644 --- a/src/renderer/src/pages/settings/AboutSettings.tsx +++ b/src/renderer/src/pages/settings/AboutSettings.tsx @@ -4,11 +4,12 @@ import IndicatorLight from '@renderer/components/IndicatorLight' import { HStack } from '@renderer/components/Layout' import { APP_NAME, AppLogo } from '@renderer/config/env' import { useTheme } from '@renderer/context/ThemeProvider' +import { useAppUpdateState } from '@renderer/hooks/useAppUpdate' import { useMinappPopup } from '@renderer/hooks/useMinappPopup' -import { useRuntime } from '@renderer/hooks/useRuntime' +// import { useRuntime } from '@renderer/hooks/useRuntime' import i18n from '@renderer/i18n' -import { handleSaveData, useAppDispatch } from '@renderer/store' -import { setUpdateState } from '@renderer/store/runtime' +import { handleSaveData } from '@renderer/store' +// import { setUpdateState as setAppUpdateState } from '@renderer/store/runtime' import { runAsyncFunction } from '@renderer/utils' import { UpgradeChannel } from '@shared/data/preference/preferenceTypes' import { ThemeMode } from '@shared/data/preference/preferenceTypes' @@ -33,23 +34,25 @@ const AboutSettings: FC = () => { const [isPortable, setIsPortable] = useState(false) const { t } = useTranslation() const { theme } = useTheme() - const dispatch = useAppDispatch() - const { update } = useRuntime() + // const dispatch = useAppDispatch() + // const { update } = useRuntime() const { openMinapp } = useMinappPopup() + const { appUpdateState, updateAppUpdateState } = useAppUpdateState() + const onCheckUpdate = debounce( async () => { - if (update.checking || update.downloading) { + if (appUpdateState.checking || appUpdateState.downloading) { return } - if (update.downloaded) { + if (appUpdateState.downloaded) { await handleSaveData() window.api.showUpdateDialog() return } - dispatch(setUpdateState({ checking: true })) + updateAppUpdateState({ checking: true }) try { await window.api.checkForUpdate() @@ -57,7 +60,7 @@ const AboutSettings: FC = () => { window.toast.error(t('settings.about.updateError')) } - dispatch(setUpdateState({ checking: false })) + updateAppUpdateState({ checking: false }) }, 2000, { leading: true, trailing: false } @@ -112,16 +115,14 @@ const AboutSettings: FC = () => { } setTestChannel(value) // Clear update info when switching upgrade channel - dispatch( - setUpdateState({ - available: false, - info: null, - downloaded: false, - checking: false, - downloading: false, - downloadProgress: 0 - }) - ) + updateAppUpdateState({ + available: false, + info: null, + downloaded: false, + checking: false, + downloading: false, + downloadProgress: 0 + }) } // Get available test version options based on current version @@ -142,16 +143,14 @@ const AboutSettings: FC = () => { const handleSetTestPlan = (value: boolean) => { setTestPlan(value) - dispatch( - setUpdateState({ - available: false, - info: null, - downloaded: false, - checking: false, - downloading: false, - downloadProgress: 0 - }) - ) + updateAppUpdateState({ + available: false, + info: null, + downloaded: false, + checking: false, + downloading: false, + downloadProgress: 0 + }) if (value === true) { setTestChannel(getTestChannel()) @@ -196,11 +195,11 @@ const AboutSettings: FC = () => { onOpenWebsite('https://github.com/CherryHQ/cherry-studio')}> - {update.downloadProgress > 0 && ( + {appUpdateState.downloadProgress > 0 && ( { {!isPortable && ( - {update.downloading + loading={appUpdateState.checking} + disabled={appUpdateState.downloading || appUpdateState.checking}> + {appUpdateState.downloading ? t('settings.about.downloading') - : update.available + : appUpdateState.available ? t('settings.about.checkUpdate.available') : t('settings.about.checkUpdate.label')} @@ -268,19 +267,19 @@ const AboutSettings: FC = () => { )} - {update.info && update.available && ( + {appUpdateState.info && appUpdateState.available && ( - {t('settings.about.updateAvailable', { version: update.info.version })} + {t('settings.about.updateAvailable', { version: appUpdateState.info.version })} - {typeof update.info.releaseNotes === 'string' - ? update.info.releaseNotes.replace(/\n/g, '\n\n') - : update.info.releaseNotes?.map((note) => note.note).join('\n')} + {typeof appUpdateState.info.releaseNotes === 'string' + ? appUpdateState.info.releaseNotes.replace(/\n/g, '\n\n') + : appUpdateState.info.releaseNotes?.map((note) => note.note).join('\n')} diff --git a/src/renderer/src/pages/translate/TranslatePage.tsx b/src/renderer/src/pages/translate/TranslatePage.tsx index 91171fd441..d6f9db8fec 100644 --- a/src/renderer/src/pages/translate/TranslatePage.tsx +++ b/src/renderer/src/pages/translate/TranslatePage.tsx @@ -18,7 +18,7 @@ import useTranslate from '@renderer/hooks/useTranslate' import { estimateTextTokens } from '@renderer/services/TokenService' import { saveTranslateHistory, translateText } from '@renderer/services/TranslateService' import { useAppDispatch, useAppSelector } from '@renderer/store' -import { setTranslateAbortKey, setTranslating as setTranslatingAction } from '@renderer/store/runtime' +// import { setTranslateAbortKey, setTranslating as setTranslatingAction } from '@renderer/store/runtime' import { setTranslatedContent as setTranslatedContentAction, setTranslateInput } from '@renderer/store/translate' import { type AutoDetectionMethod, @@ -88,11 +88,13 @@ const TranslatePage: FC = () => { const [autoDetectionMethod, setAutoDetectionMethod] = useState('franc') const [isProcessing, setIsProcessing] = useState(false) + const [translating, setTranslating] = useState(false) + const [abortKey, setTranslateAbortKey] = useState('') // redux states const text = useAppSelector((state) => state.translate.translateInput) const translatedContent = useAppSelector((state) => state.translate.translatedContent) - const translating = useAppSelector((state) => state.runtime.translating) - const abortKey = useAppSelector((state) => state.runtime.translateAbortKey) + // const translating = useAppSelector((state) => state.runtime.translating) + // const abortKey = useAppSelector((state) => state.runtime.translateAbortKey) // ref const contentContainerRef = useRef(null) @@ -126,12 +128,12 @@ const TranslatePage: FC = () => { [dispatch] ) - const setTranslating = useCallback( - (translating: boolean) => { - dispatch(setTranslatingAction(translating)) - }, - [dispatch] - ) + // const setTranslating = useCallback( + // (translating: boolean) => { + // dispatch(setTranslatingAction(translating)) + // }, + // [dispatch] + // ) // 控制复制行为 const onCopy = useCallback(async () => { @@ -163,7 +165,7 @@ const TranslatePage: FC = () => { let translated: string const abortKey = uuid() - dispatch(setTranslateAbortKey(abortKey)) + setTranslateAbortKey(abortKey) try { translated = await translateText(text, actualTargetLanguage, throttle(setTranslatedContent, 100), abortKey) @@ -200,7 +202,7 @@ const TranslatePage: FC = () => { window.toast.error(t('translate.error.unknown') + ': ' + formatErrorMessage(e)) } }, - [autoCopy, dispatch, onCopy, setTimeoutTimer, setTranslatedContent, setTranslating, t, translating] + [autoCopy, onCopy, setTimeoutTimer, setTranslatedContent, setTranslating, t, translating] ) // 控制翻译按钮是否可用 diff --git a/src/renderer/src/services/FileManager.ts b/src/renderer/src/services/FileManager.ts index ec32535da7..83c5352e5a 100644 --- a/src/renderer/src/services/FileManager.ts +++ b/src/renderer/src/services/FileManager.ts @@ -1,7 +1,7 @@ +import { cacheService } from '@data/CacheService' import { loggerService } from '@logger' import db from '@renderer/databases' import i18n from '@renderer/i18n' -import store from '@renderer/store' import { FileMetadata } from '@renderer/types' import { getFileDirectory } from '@renderer/utils' import dayjs from 'dayjs' @@ -81,7 +81,7 @@ class FileManager { const file = await db.files.get(id) if (file) { - const filesPath = store.getState().runtime.filesPath + const filesPath = cacheService.get('filesPath') ?? '' file.path = filesPath + '/' + file.id + file.ext } @@ -89,7 +89,7 @@ class FileManager { } static getFilePath(file: FileMetadata) { - const filesPath = store.getState().runtime.filesPath + const filesPath = cacheService.get('filesPath') ?? '' return filesPath + '/' + file.id + file.ext } @@ -137,7 +137,7 @@ class FileManager { } static getFileUrl(file: FileMetadata) { - const filesPath = store.getState().runtime.filesPath + const filesPath = cacheService.get('filesPath') ?? '' return 'file://' + filesPath + '/' + file.name } diff --git a/src/renderer/src/services/MessagesService.ts b/src/renderer/src/services/MessagesService.ts index bbb5263c1a..a3abf8ab4c 100644 --- a/src/renderer/src/services/MessagesService.ts +++ b/src/renderer/src/services/MessagesService.ts @@ -1,8 +1,8 @@ import { loggerService } from '@logger' import SearchPopup from '@renderer/components/Popups/SearchPopup' import { DEFAULT_CONTEXTCOUNT, MAX_CONTEXT_COUNT, UNLIMITED_CONTEXT_COUNT } from '@renderer/config/constant' +import { modelGenerating } from '@renderer/hooks/useModel' import { getTopicById } from '@renderer/hooks/useTopic' -import i18n from '@renderer/i18n' import { fetchMessagesSummary } from '@renderer/services/ApiService' import store from '@renderer/store' import { messageBlocksSelectors, removeManyBlocks } from '@renderer/store/messageBlock' @@ -67,16 +67,8 @@ export function deleteMessageFiles(message: Message) { }) } -export function isGenerating() { - return new Promise((resolve, reject) => { - const generating = store.getState().runtime.generating - generating && window.toast.warning(i18n.t('message.switch.disabled')) - generating ? reject(false) : resolve(true) - }) -} - export async function locateToMessage(navigate: NavigateFunction, message: Message) { - await isGenerating() + await modelGenerating() SearchPopup.hide() const assistant = getAssistantById(message.assistantId) diff --git a/src/renderer/src/services/WebSearchService.ts b/src/renderer/src/services/WebSearchService.ts index 176b0cc37c..557d08da0f 100644 --- a/src/renderer/src/services/WebSearchService.ts +++ b/src/renderer/src/services/WebSearchService.ts @@ -1,10 +1,10 @@ +import { cacheService } from '@data/CacheService' import { loggerService } from '@logger' import { DEFAULT_WEBSEARCH_RAG_DOCUMENT_COUNT } from '@renderer/config/constant' import i18n from '@renderer/i18n' import WebSearchEngineProvider from '@renderer/providers/WebSearchProvider' import { addSpan, endSpan } from '@renderer/services/SpanManagerService' import store from '@renderer/store' -import { setWebSearchStatus } from '@renderer/store/runtime' import { CompressionConfig, WebSearchState } from '@renderer/store/websearch' import { KnowledgeBase, @@ -202,12 +202,15 @@ class WebSearchService { * 设置网络搜索状态 */ private async setWebSearchStatus(requestId: string, status: WebSearchStatus, delayMs?: number) { - store.dispatch(setWebSearchStatus({ requestId, status })) + const activeSearches = cacheService.get('chat.websearch.active_searches') + activeSearches[requestId] = status + + cacheService.set('chat.websearch.active_searches', activeSearches) + if (delayMs) { await new Promise((resolve) => setTimeout(resolve, delayMs)) } } - /** * 确保搜索压缩知识库存在并配置正确 */ diff --git a/src/renderer/src/store/runtime.ts b/src/renderer/src/store/runtime.ts index 1565e43b27..a915c675ee 100644 --- a/src/renderer/src/store/runtime.ts +++ b/src/renderer/src/store/runtime.ts @@ -1,6 +1,10 @@ +/** + * Data Refactor, notes by fullex + * //TODO @deprecated this file will be removed + */ + import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { AppLogo, UserAvatar } from '@renderer/config/env' -import type { MinAppType, Topic, WebSearchStatus } from '@renderer/types' +import type { Topic, WebSearchStatus } from '@renderer/types' import type { UpdateInfo } from 'builder-util-runtime' export interface ChatState { @@ -27,24 +31,24 @@ export interface UpdateState { } export interface RuntimeState { - avatar: string - generating: boolean - translating: boolean - translateAbortKey?: string - /** whether the minapp popup is shown */ - minappShow: boolean - /** the minapps that are opened and should be keep alive */ - openedKeepAliveMinapps: MinAppType[] - /** the minapp that is opened for one time */ - openedOneOffMinapp: MinAppType | null - /** the current minapp id */ - currentMinappId: string - searching: boolean - filesPath: string - resourcesPath: string - update: UpdateState - export: ExportState - chat: ChatState + // avatar: string + // generating: boolean + // translating: boolean + // translateAbortKey?: string + // /** whether the minapp popup is shown */ + // minappShow: boolean + // /** the minapps that are opened and should be keep alive */ + // openedKeepAliveMinapps: MinAppType[] + // /** the minapp that is opened for one time */ + // openedOneOffMinapp: MinAppType | null + // /** the current minapp id */ + // currentMinappId: string + // searching: boolean + // filesPath: string + // resourcesPath: string + // update: UpdateState + // export: ExportState + // chat: ChatState websearch: WebSearchState } @@ -53,34 +57,34 @@ export interface ExportState { } const initialState: RuntimeState = { - avatar: UserAvatar, - generating: false, - translating: false, - minappShow: false, - openedKeepAliveMinapps: [], - openedOneOffMinapp: null, - currentMinappId: '', - searching: false, - filesPath: '', - resourcesPath: '', - update: { - info: null, - checking: false, - downloading: false, - downloaded: false, - downloadProgress: 0, - available: false - }, - export: { - isExporting: false - }, - chat: { - isMultiSelectMode: false, - selectedMessageIds: [], - activeTopic: null, - renamingTopics: [], - newlyRenamedTopics: [] - }, + // avatar: UserAvatar, + // generating: false, + // translating: false, + // minappShow: false, + // openedKeepAliveMinapps: [], + // openedOneOffMinapp: null, + // currentMinappId: '', + // searching: false, + // filesPath: '', + // resourcesPath: '', + // update: { + // info: null, + // checking: false, + // downloading: false, + // downloaded: false, + // downloadProgress: 0, + // available: false + // }, + // export: { + // isExporting: false + // }, + // chat: { + // isMultiSelectMode: false, + // selectedMessageIds: [], + // activeTopic: null, + // renamingTopics: [], + // newlyRenamedTopics: [] + // }, websearch: { activeSearches: {} } @@ -90,101 +94,105 @@ const runtimeSlice = createSlice({ name: 'runtime', initialState, reducers: { - setAvatar: (state, action: PayloadAction) => { - state.avatar = action.payload || AppLogo - }, - setGenerating: (state, action: PayloadAction) => { - state.generating = action.payload - }, - setTranslating: (state, action: PayloadAction) => { - state.translating = action.payload - }, - setTranslateAbortKey: (state, action: PayloadAction) => { - state.translateAbortKey = action.payload - }, - setMinappShow: (state, action: PayloadAction) => { - state.minappShow = action.payload - }, - setOpenedKeepAliveMinapps: (state, action: PayloadAction) => { - state.openedKeepAliveMinapps = action.payload - }, - setOpenedOneOffMinapp: (state, action: PayloadAction) => { - state.openedOneOffMinapp = action.payload - }, - setCurrentMinappId: (state, action: PayloadAction) => { - state.currentMinappId = action.payload - }, - setSearching: (state, action: PayloadAction) => { - state.searching = action.payload - }, - setFilesPath: (state, action: PayloadAction) => { - state.filesPath = action.payload - }, - setResourcesPath: (state, action: PayloadAction) => { - state.resourcesPath = action.payload - }, - setUpdateState: (state, action: PayloadAction>) => { - state.update = { ...state.update, ...action.payload } - }, - setExportState: (state, action: PayloadAction>) => { - state.export = { ...state.export, ...action.payload } - }, - // Chat related actions - toggleMultiSelectMode: (state, action: PayloadAction) => { - state.chat.isMultiSelectMode = action.payload - if (!action.payload) { - state.chat.selectedMessageIds = [] - } - }, - setSelectedMessageIds: (state, action: PayloadAction) => { - state.chat.selectedMessageIds = action.payload - }, - setActiveTopic: (state, action: PayloadAction) => { - state.chat.activeTopic = action.payload - }, - setRenamingTopics: (state, action: PayloadAction) => { - state.chat.renamingTopics = action.payload - }, - setNewlyRenamedTopics: (state, action: PayloadAction) => { - state.chat.newlyRenamedTopics = action.payload - }, - // WebSearch related actions - setActiveSearches: (state, action: PayloadAction>) => { - state.websearch.activeSearches = action.payload - }, - setWebSearchStatus: (state, action: PayloadAction<{ requestId: string; status: WebSearchStatus }>) => { - const { requestId, status } = action.payload - if (status.phase === 'default') { - delete state.websearch.activeSearches[requestId] - } - state.websearch.activeSearches[requestId] = status + // setAvatar: (state, action: PayloadAction) => { + // state.avatar = action.payload || AppLogo + // }, + // setGenerating: (state, action: PayloadAction) => { + // state.generating = action.payload + // }, + // setTranslating: (state, action: PayloadAction) => { + // state.translating = action.payload + // }, + // setTranslateAbortKey: (state, action: PayloadAction) => { + // state.translateAbortKey = action.payload + // }, + // setMinappShow: (state, action: PayloadAction) => { + // state.minappShow = action.payload + // }, + // setOpenedKeepAliveMinapps: (state, action: PayloadAction) => { + // state.openedKeepAliveMinapps = action.payload + // }, + // setOpenedOneOffMinapp: (state, action: PayloadAction) => { + // state.openedOneOffMinapp = action.payload + // }, + // setCurrentMinappId: (state, action: PayloadAction) => { + // state.currentMinappId = action.payload + // }, + // setSearching: (state, action: PayloadAction) => { + // state.searching = action.payload + // }, + // setFilesPath: (state, action: PayloadAction) => { + // state.filesPath = action.payload + // }, + // setResourcesPath: (state, action: PayloadAction) => { + // state.resourcesPath = action.payload + // }, + // setUpdateState: (state, action: PayloadAction>) => { + // state.update = { ...state.update, ...action.payload } + // }, + // setExportState: (state, action: PayloadAction>) => { + // state.export = { ...state.export, ...action.payload } + // }, + // // Chat related actions + // toggleMultiSelectMode: (state, action: PayloadAction) => { + // state.chat.isMultiSelectMode = action.payload + // if (!action.payload) { + // state.chat.selectedMessageIds = [] + // } + // }, + // setSelectedMessageIds: (state, action: PayloadAction) => { + // state.chat.selectedMessageIds = action.payload + // }, + // setActiveTopic: (state, action: PayloadAction) => { + // state.chat.activeTopic = action.payload + // }, + // setRenamingTopics: (state, action: PayloadAction) => { + // state.chat.renamingTopics = action.payload + // }, + // setNewlyRenamedTopics: (state, action: PayloadAction) => { + // state.chat.newlyRenamedTopics = action.payload + // }, + // // WebSearch related actions + // setActiveSearches: (state, action: PayloadAction>) => { + // state.websearch.activeSearches = action.payload + // }, + // setWebSearchStatus: (state, action: PayloadAction<{ requestId: string; status: WebSearchStatus }>) => { + // const { requestId, status } = action.payload + // if (status.phase === 'default') { + // delete state.websearch.activeSearches[requestId] + // } + // state.websearch.activeSearches[requestId] = status + // }, + setPlaceholder: (state, action: PayloadAction>) => { + state = { ...state, ...action.payload } } } }) export const { - setAvatar, - setGenerating, - setTranslating, - setTranslateAbortKey, - setMinappShow, - setOpenedKeepAliveMinapps, - setOpenedOneOffMinapp, - setCurrentMinappId, - setSearching, - setFilesPath, - setResourcesPath, - setUpdateState, - setExportState, - // Chat related actions - toggleMultiSelectMode, - setSelectedMessageIds, - setActiveTopic, - setRenamingTopics, - setNewlyRenamedTopics, - // WebSearch related actions - setActiveSearches, - setWebSearchStatus + // setAvatar, + // setGenerating, + // setTranslating, + // setTranslateAbortKey, + // setMinappShow, + // setOpenedKeepAliveMinapps, + // setOpenedOneOffMinapp, + // setCurrentMinappId, + // setSearching, + // setFilesPath, + // setResourcesPath, + // setUpdateState, + // setExportState, + // // Chat related actions + // toggleMultiSelectMode, + // setSelectedMessageIds, + // setActiveTopic, + // setRenamingTopics, + // setNewlyRenamedTopics, + // // WebSearch related actions + // setActiveSearches, + // setWebSearchStatus, + setPlaceholder } = runtimeSlice.actions export default runtimeSlice.reducer diff --git a/src/renderer/src/utils/export.ts b/src/renderer/src/utils/export.ts index e647cc7d0e..0df2f67602 100644 --- a/src/renderer/src/utils/export.ts +++ b/src/renderer/src/utils/export.ts @@ -5,8 +5,6 @@ import i18n from '@renderer/i18n' import { getProviderLabel } from '@renderer/i18n/label' import { getMessageTitle } from '@renderer/services/MessagesService' import { createNote } from '@renderer/services/NotesService' -import store from '@renderer/store' -import { setExportState } from '@renderer/store/runtime' import type { Topic } from '@renderer/types' import type { Message } from '@renderer/types/newMessage' import { NotesTreeNode } from '@renderer/types/note' @@ -20,12 +18,11 @@ import { appendBlocks } from 'notion-helper' const logger = loggerService.withContext('Utils:export') -// 全局的导出状态获取函数 -const getExportState = () => store.getState().runtime.export.isExporting +let exportState = false -// 全局的导出状态设置函数,使用 dispatch 保障 Redux 状态更新正确 +const getExportState = () => exportState const setExportingState = (isExporting: boolean) => { - store.dispatch(setExportState({ isExporting })) + exportState = isExporting } /** diff --git a/src/renderer/src/windows/dataRefactorTest/TestApp.tsx b/src/renderer/src/windows/dataRefactorTest/TestApp.tsx index 5757db127d..f083c7737d 100644 --- a/src/renderer/src/windows/dataRefactorTest/TestApp.tsx +++ b/src/renderer/src/windows/dataRefactorTest/TestApp.tsx @@ -2,7 +2,7 @@ import { AppLogo } from '@renderer/config/env' import { usePreference } from '@renderer/data/hooks/usePreference' import { loggerService } from '@renderer/services/LoggerService' import { ThemeMode } from '@shared/data/preference/preferenceTypes' -import { Button, Card, Col, Divider, Layout, Row, Space, Typography, Tabs } from 'antd' +import { Button, Card, Col, Divider, Layout, Row, Space, Tabs, Typography } from 'antd' import { Activity, AlertTriangle, Database, FlaskConical, Settings, TestTube, TrendingUp, Zap } from 'lucide-react' import React from 'react' import styled from 'styled-components' @@ -104,11 +104,12 @@ const TestApp: React.FC = () => { - 此测试窗口用于验证数据重构项目的各项功能,包括 PreferenceService、CacheService、DataApiService 和相关 React hooks - 的完整测试套件。 + 此测试窗口用于验证数据重构项目的各项功能,包括 PreferenceService、CacheService、DataApiService + 和相关 React hooks 的完整测试套件。 - PreferenceService 测试使用真实的偏好设置系统,CacheService 测试使用三层缓存架构,DataApiService 测试使用专用的测试路由和假数据。 + PreferenceService 测试使用真实的偏好设置系统,CacheService 测试使用三层缓存架构,DataApiService + 测试使用专用的测试路由和假数据。 📋 跨窗口测试指南:在一个窗口中修改偏好设置,观察其他窗口是否实时同步更新。 @@ -403,39 +404,39 @@ const Container = styled.div` const StyledTabs = styled(Tabs)<{ $isDark: boolean }>` .ant-tabs-nav { - background: ${props => props.$isDark ? '#262626' : '#fafafa'}; + background: ${(props) => (props.$isDark ? '#262626' : '#fafafa')}; border-radius: 6px 6px 0 0; margin-bottom: 0; } .ant-tabs-tab { - color: ${props => props.$isDark ? '#d9d9d9' : '#666'} !important; + color: ${(props) => (props.$isDark ? '#d9d9d9' : '#666')} !important; &:hover { - color: ${props => props.$isDark ? '#fff' : '#000'} !important; + color: ${(props) => (props.$isDark ? '#fff' : '#000')} !important; } &.ant-tabs-tab-active { - color: ${props => props.$isDark ? '#1890ff' : '#1890ff'} !important; + color: ${(props) => (props.$isDark ? '#1890ff' : '#1890ff')} !important; .ant-tabs-tab-btn { - color: ${props => props.$isDark ? '#1890ff' : '#1890ff'} !important; + color: ${(props) => (props.$isDark ? '#1890ff' : '#1890ff')} !important; } } } .ant-tabs-ink-bar { - background: ${props => props.$isDark ? '#1890ff' : '#1890ff'}; + background: ${(props) => (props.$isDark ? '#1890ff' : '#1890ff')}; } .ant-tabs-content { - background: ${props => props.$isDark ? '#1f1f1f' : '#fff'}; + background: ${(props) => (props.$isDark ? '#1f1f1f' : '#fff')}; border-radius: 0 0 6px 6px; padding: 24px 0; } .ant-tabs-tabpane { - color: ${props => props.$isDark ? '#fff' : '#000'}; + color: ${(props) => (props.$isDark ? '#fff' : '#000')}; } ` diff --git a/src/renderer/src/windows/dataRefactorTest/components/CacheAdvancedTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/CacheAdvancedTests.tsx index e8809e07ee..657a152358 100644 --- a/src/renderer/src/windows/dataRefactorTest/components/CacheAdvancedTests.tsx +++ b/src/renderer/src/windows/dataRefactorTest/components/CacheAdvancedTests.tsx @@ -3,12 +3,12 @@ import { useCache, useSharedCache } from '@renderer/data/hooks/useCache' import { usePreference } from '@renderer/data/hooks/usePreference' import { loggerService } from '@renderer/services/LoggerService' import { ThemeMode } from '@shared/data/preference/preferenceTypes' -import { Button, Input, message, Space, Typography, Card, Row, Col, Divider, Progress, Badge, Tag } from 'antd' -import { Clock, Shield, Zap, Activity, AlertTriangle, CheckCircle, XCircle, Timer } from 'lucide-react' -import React, { useState, useEffect, useRef, useCallback } from 'react' +import { Badge, Button, Card, Col, Divider, message, Progress, Row, Space, Tag, Typography } from 'antd' +import { Activity, AlertTriangle, CheckCircle, Clock, Shield, Timer, XCircle, Zap } from 'lucide-react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import styled from 'styled-components' -const { Text, Title } = Typography +const { Text } = Typography const logger = loggerService.withContext('CacheAdvancedTests') @@ -21,37 +21,37 @@ const CacheAdvancedTests: React.FC = () => { const isDarkTheme = currentTheme === ThemeMode.dark // TTL Testing - const [ttlKey] = useState('test-ttl-cache') - const [ttlValue, setTtlValue] = useCache(ttlKey) + const [ttlKey] = useState('test-ttl-cache' as const) + const [ttlValue, setTtlValue] = useCache(ttlKey as any, 'test-ttl-cache') const [ttlExpireTime, setTtlExpireTime] = useState(null) const [ttlProgress, setTtlProgress] = useState(0) // Hook Reference Tracking - const [protectedKey] = useState('test-protected-cache') - const [protectedValue, setProtectedValue] = useCache(protectedKey, 'protected-value') + const [protectedKey] = useState('test-protected-cache' as const) + const [protectedValue] = useCache(protectedKey as any, 'protected-value') const [deleteAttemptResult, setDeleteAttemptResult] = useState('') // Deep Equality Testing - const [deepEqualKey] = useState('test-deep-equal') - const [objectValue, setObjectValue] = useCache(deepEqualKey, { nested: { count: 0 }, tags: ['initial'] }) - const [updateSkipCount, setUpdateSkipCount] = useState(0) + const [deepEqualKey] = useState('test-deep-equal' as const) + const [objectValue, setObjectValue] = useCache(deepEqualKey as any, { nested: { count: 0 }, tags: ['initial'] }) + const [updateSkipCount] = useState(0) // Performance Testing - const [perfKey] = useState('test-performance') - const [perfValue, setPerfValue] = useCache(perfKey, 0) + const [perfKey] = useState('test-performance' as const) + const [perfValue, setPerfValue] = useCache(perfKey as any, 0) const [rapidUpdateCount, setRapidUpdateCount] = useState(0) const [subscriptionTriggers, setSubscriptionTriggers] = useState(0) const renderCountRef = useRef(0) const [displayRenderCount, setDisplayRenderCount] = useState(0) // Multi-hook testing - const [multiKey] = useState('test-multi-hook') - const [value1] = useCache(multiKey, 'hook-1-default') - const [value2] = useCache(multiKey, 'hook-2-default') - const [value3] = useSharedCache(multiKey, 'hook-3-shared') + const [multiKey] = useState('test-multi-hook' as const) + const [value1] = useCache(multiKey as any, 'hook-1-default') + const [value2] = useCache(multiKey as any, 'hook-2-default') + const [value3] = useSharedCache(multiKey as any, 'hook-3-shared') - const intervalRef = useRef() - const performanceTestRef = useRef() + const intervalRef = useRef(null) + const performanceTestRef = useRef(null) // Update render count without causing re-renders renderCountRef.current += 1 @@ -59,53 +59,56 @@ const CacheAdvancedTests: React.FC = () => { // Track subscription changes useEffect(() => { const unsubscribe = cacheService.subscribe(perfKey, () => { - setSubscriptionTriggers(prev => prev + 1) + setSubscriptionTriggers((prev) => prev + 1) }) return unsubscribe }, [perfKey]) // TTL Testing Functions - const startTTLTest = useCallback((ttlMs: number) => { - const testValue = { message: 'TTL Test', timestamp: Date.now() } - cacheService.set(ttlKey, testValue, ttlMs) - setTtlValue(testValue) + const startTTLTest = useCallback( + (ttlMs: number) => { + const testValue = { message: 'TTL Test', timestamp: Date.now() } + cacheService.set(ttlKey, testValue, ttlMs) + setTtlValue(testValue.message) - const expireAt = Date.now() + ttlMs - setTtlExpireTime(expireAt) + const expireAt = Date.now() + ttlMs + setTtlExpireTime(expireAt) - // Clear previous interval - if (intervalRef.current) { - clearInterval(intervalRef.current) - } - - // Update progress every 100ms - intervalRef.current = setInterval(() => { - const now = Date.now() - const remaining = Math.max(0, expireAt - now) - const progress = Math.max(0, 100 - (remaining / ttlMs) * 100) - - setTtlProgress(progress) - - if (remaining <= 0) { - clearInterval(intervalRef.current!) - setTtlExpireTime(null) - message.info('TTL expired, checking value...') - - // Check if value is actually expired - setTimeout(() => { - const currentValue = cacheService.get(ttlKey) - if (currentValue === undefined) { - message.success('TTL expiration working correctly!') - } else { - message.warning('TTL expiration may have failed') - } - }, 100) + // Clear previous interval + if (intervalRef.current) { + clearInterval(intervalRef.current) } - }, 100) - message.info(`TTL test started: ${ttlMs}ms`) - logger.info('TTL test started', { key: ttlKey, ttl: ttlMs, expireAt }) - }, [ttlKey, setTtlValue]) + // Update progress every 100ms + intervalRef.current = setInterval(() => { + const now = Date.now() + const remaining = Math.max(0, expireAt - now) + const progress = Math.max(0, 100 - (remaining / ttlMs) * 100) + + setTtlProgress(progress) + + if (remaining <= 0) { + clearInterval(intervalRef.current!) + setTtlExpireTime(null) + message.info('TTL expired, checking value...') + + // Check if value is actually expired + setTimeout(() => { + const currentValue = cacheService.get(ttlKey) + if (currentValue === undefined) { + message.success('TTL expiration working correctly!') + } else { + message.warning('TTL expiration may have failed') + } + }, 100) + } + }, 100) + + message.info(`TTL test started: ${ttlMs}ms`) + logger.info('TTL test started', { key: ttlKey, ttl: ttlMs, expireAt }) + }, + [ttlKey, setTtlValue] + ) // Hook Reference Tracking Test const testDeleteProtection = () => { @@ -126,7 +129,7 @@ const CacheAdvancedTests: React.FC = () => { switch (operation) { case 'same-reference': // Set same reference - should skip - setObjectValue(objectValue) + setObjectValue(objectValue as { nested: { count: number }; tags: string[] }) break case 'same-content': @@ -198,12 +201,19 @@ const CacheAdvancedTests: React.FC = () => {
- Advanced Features • Renders: {displayRenderCount || renderCountRef.current} • Subscriptions: {subscriptionTriggers} - + + Advanced Features • Renders: {displayRenderCount || renderCountRef.current} • Subscriptions:{' '} + {subscriptionTriggers} + +
@@ -221,10 +231,11 @@ const CacheAdvancedTests: React.FC = () => { style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }} - > + }}> - Key: {ttlKey} + + Key: {ttlKey} + {deleteAttemptResult && ( - - {deleteAttemptResult} - + {deleteAttemptResult} )} @@ -316,17 +319,23 @@ const CacheAdvancedTests: React.FC = () => { style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }} - > + }}> - Key: {deepEqualKey} - Skip Count: + + Key: {deepEqualKey} + + + Skip Count: + - @@ -448,12 +458,12 @@ const CacheAdvancedTests: React.FC = () => { } const TestContainer = styled.div<{ $isDark: boolean }>` - color: ${props => props.$isDark ? '#fff' : '#000'}; + color: ${(props) => (props.$isDark ? '#fff' : '#000')}; ` const ResultDisplay = styled.div<{ $isDark: boolean }>` - background: ${props => props.$isDark ? '#0d1117' : '#f6f8fa'}; - border: 1px solid ${props => props.$isDark ? '#30363d' : '#d0d7de'}; + background: ${(props) => (props.$isDark ? '#0d1117' : '#f6f8fa')}; + border: 1px solid ${(props) => (props.$isDark ? '#30363d' : '#d0d7de')}; border-radius: 6px; padding: 8px; font-size: 11px; @@ -464,9 +474,9 @@ const ResultDisplay = styled.div<{ $isDark: boolean }>` margin: 0; white-space: pre-wrap; word-break: break-all; - color: ${props => props.$isDark ? '#e6edf3' : '#1f2328'}; + color: ${(props) => (props.$isDark ? '#e6edf3' : '#1f2328')}; font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; } ` -export default CacheAdvancedTests \ No newline at end of file +export default CacheAdvancedTests diff --git a/src/renderer/src/windows/dataRefactorTest/components/CacheBasicTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/CacheBasicTests.tsx index d44e6e84a5..ad69fa365d 100644 --- a/src/renderer/src/windows/dataRefactorTest/components/CacheBasicTests.tsx +++ b/src/renderer/src/windows/dataRefactorTest/components/CacheBasicTests.tsx @@ -1,16 +1,15 @@ -import { useCache, useSharedCache, usePersistCache } from '@renderer/data/hooks/useCache' +import { useCache, usePersistCache, useSharedCache } from '@renderer/data/hooks/useCache' import { usePreference } from '@renderer/data/hooks/usePreference' import { loggerService } from '@renderer/services/LoggerService' -import type { PersistCacheKey } from '@shared/data/cache/cacheSchemas' +import type { RendererPersistCacheKey } from '@shared/data/cache/cacheSchemas' import { ThemeMode } from '@shared/data/preference/preferenceTypes' -import { Button, Input, message, Select, Space, Typography, Card, Row, Col, Divider, Slider } from 'antd' -import { Zap, Database, Eye, Edit, RefreshCw, Users, HardDrive } from 'lucide-react' -import React, { useState, useEffect, useRef } from 'react' +import { Button, Card, Col, Divider, Input, message, Row, Select, Slider, Space, Typography } from 'antd' +import { Database, Edit, Eye, HardDrive, RefreshCw, Users, Zap } from 'lucide-react' +import React, { useRef, useState } from 'react' import styled from 'styled-components' -const { Text, Title } = Typography +const { Text } = Typography const { Option } = Select -const { TextArea } = Input const logger = loggerService.withContext('CacheBasicTests') @@ -26,25 +25,25 @@ const CacheBasicTests: React.FC = () => { const [memoryCacheKey, setMemoryCacheKey] = useState('test-hook-memory-1') const [memoryCacheDefault, setMemoryCacheDefault] = useState('default-memory-value') const [newMemoryValue, setNewMemoryValue] = useState('') - const [memoryValue, setMemoryValue] = useCache(memoryCacheKey, memoryCacheDefault) + const [memoryValue, setMemoryValue] = useCache(memoryCacheKey as any, memoryCacheDefault) // useSharedCache testing const [sharedCacheKey, setSharedCacheKey] = useState('test-hook-shared-1') const [sharedCacheDefault, setSharedCacheDefault] = useState('default-shared-value') const [newSharedValue, setNewSharedValue] = useState('') - const [sharedValue, setSharedValue] = useSharedCache(sharedCacheKey, sharedCacheDefault) + const [sharedValue, setSharedValue] = useSharedCache(sharedCacheKey as any, sharedCacheDefault) // usePersistCache testing - const [persistCacheKey, setPersistCacheKey] = useState('example-1') + const [persistCacheKey, setPersistCacheKey] = useState('example-1') const [newPersistValue, setNewPersistValue] = useState('') const [persistValue, setPersistValue] = usePersistCache(persistCacheKey) // Testing different data types - const [numberKey] = useState('test-number-cache') - const [numberValue, setNumberValue] = useCache(numberKey, 42) + const [numberKey] = useState('test-number-cache' as const) + const [numberValue, setNumberValue] = useCache(numberKey as any, 42) - const [objectKey] = useState('test-object-cache') - const [objectValue, setObjectValue] = useCache(objectKey, { name: 'test', count: 0, active: true }) + const [objectKey] = useState('test-object-cache' as const) + const [objectValue, setObjectValue] = useCache(objectKey as any, { name: 'test', count: 0, active: true }) // Stats const renderCountRef = useRef(0) @@ -52,7 +51,7 @@ const CacheBasicTests: React.FC = () => { const [updateCount, setUpdateCount] = useState(0) // Available persist keys - const persistKeys: PersistCacheKey[] = ['example-1', 'example-2', 'example-3', 'example-4'] + const persistKeys: RendererPersistCacheKey[] = ['example-1', 'example-2', 'example-3', 'example-4'] // Update render count without causing re-renders renderCountRef.current += 1 @@ -79,7 +78,7 @@ const CacheBasicTests: React.FC = () => { const parsed = parseValue(newMemoryValue) setMemoryValue(parsed) setNewMemoryValue('') - setUpdateCount(prev => prev + 1) + setUpdateCount((prev) => prev + 1) message.success(`Memory cache updated: ${memoryCacheKey}`) logger.info('Memory cache updated via hook', { key: memoryCacheKey, value: parsed }) } catch (error) { @@ -93,7 +92,7 @@ const CacheBasicTests: React.FC = () => { const parsed = parseValue(newSharedValue) setSharedValue(parsed) setNewSharedValue('') - setUpdateCount(prev => prev + 1) + setUpdateCount((prev) => prev + 1) message.success(`Shared cache updated: ${sharedCacheKey} (broadcasted to other windows)`) logger.info('Shared cache updated via hook', { key: sharedCacheKey, value: parsed }) } catch (error) { @@ -118,7 +117,7 @@ const CacheBasicTests: React.FC = () => { setPersistValue(parsed as any) setNewPersistValue('') - setUpdateCount(prev => prev + 1) + setUpdateCount((prev) => prev + 1) message.success(`Persist cache updated: ${persistCacheKey} (saved + broadcasted)`) logger.info('Persist cache updated via hook', { key: persistCacheKey, value: parsed }) } catch (error) { @@ -129,13 +128,14 @@ const CacheBasicTests: React.FC = () => { // Test different data types const handleNumberUpdate = (newValue: number) => { setNumberValue(newValue) - setUpdateCount(prev => prev + 1) + setUpdateCount((prev) => prev + 1) logger.info('Number cache updated', { value: newValue }) } const handleObjectUpdate = (field: string, value: any) => { - setObjectValue(prev => ({ ...prev, [field]: value })) - setUpdateCount(prev => prev + 1) + const currentValue = objectValue || { name: 'test', count: 0, active: true } + setObjectValue({ ...currentValue, [field]: value }) + setUpdateCount((prev) => prev + 1) logger.info('Object cache updated', { field, value }) } @@ -144,12 +144,18 @@ const CacheBasicTests: React.FC = () => {
- React Hook Tests • Renders: {displayRenderCount || renderCountRef.current} • Updates: {updateCount} - + + React Hook Tests • Renders: {displayRenderCount || renderCountRef.current} • Updates: {updateCount} + +
@@ -167,8 +173,7 @@ const CacheBasicTests: React.FC = () => { style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }} - > + }}> { prefix={} /> - @@ -222,8 +222,7 @@ const CacheBasicTests: React.FC = () => { style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }} - > + }}> { prefix={} /> - @@ -277,17 +271,17 @@ const CacheBasicTests: React.FC = () => { style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }} - > + }}> @@ -299,12 +293,7 @@ const CacheBasicTests: React.FC = () => { prefix={} /> - @@ -333,11 +322,14 @@ const CacheBasicTests: React.FC = () => { style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }} - > + }}> - Key: {numberKey} - Current Value: {numberValue} + + Key: {numberKey} + + + Current Value: {numberValue} + { /> - + @@ -368,10 +362,11 @@ const CacheBasicTests: React.FC = () => { style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }} - > + }}> - Key: {objectKey} + + Key: {objectKey} + { /> @@ -414,12 +408,12 @@ const CacheBasicTests: React.FC = () => { } const TestContainer = styled.div<{ $isDark: boolean }>` - color: ${props => props.$isDark ? '#fff' : '#000'}; + color: ${(props) => (props.$isDark ? '#fff' : '#000')}; ` const ResultDisplay = styled.div<{ $isDark: boolean }>` - background: ${props => props.$isDark ? '#0d1117' : '#f6f8fa'}; - border: 1px solid ${props => props.$isDark ? '#30363d' : '#d0d7de'}; + background: ${(props) => (props.$isDark ? '#0d1117' : '#f6f8fa')}; + border: 1px solid ${(props) => (props.$isDark ? '#30363d' : '#d0d7de')}; border-radius: 6px; padding: 8px; font-size: 11px; @@ -430,9 +424,9 @@ const ResultDisplay = styled.div<{ $isDark: boolean }>` margin: 0; white-space: pre-wrap; word-break: break-all; - color: ${props => props.$isDark ? '#e6edf3' : '#1f2328'}; + color: ${(props) => (props.$isDark ? '#e6edf3' : '#1f2328')}; font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; } ` -export default CacheBasicTests \ No newline at end of file +export default CacheBasicTests diff --git a/src/renderer/src/windows/dataRefactorTest/components/CacheServiceTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/CacheServiceTests.tsx index d9e00a192a..ab4f25e34c 100644 --- a/src/renderer/src/windows/dataRefactorTest/components/CacheServiceTests.tsx +++ b/src/renderer/src/windows/dataRefactorTest/components/CacheServiceTests.tsx @@ -1,14 +1,14 @@ import { cacheService } from '@renderer/data/CacheService' -import { loggerService } from '@renderer/services/LoggerService' -import type { PersistCacheKey, PersistCacheSchema } from '@shared/data/cache/cacheSchemas' import { usePreference } from '@renderer/data/hooks/usePreference' +import { loggerService } from '@renderer/services/LoggerService' +import type { RendererPersistCacheKey, RendererPersistCacheSchema } from '@shared/data/cache/cacheSchemas' import { ThemeMode } from '@shared/data/preference/preferenceTypes' -import { Button, Input, message, Select, Space, Typography, Card, Row, Col, Divider } from 'antd' -import { Database, Clock, Trash2, Eye, Edit, Zap } from 'lucide-react' -import React, { useState, useEffect } from 'react' +import { Button, Card, Col, Divider, Input, message, Row, Select, Space, Typography } from 'antd' +import { Clock, Database, Edit, Eye, Trash2, Zap } from 'lucide-react' +import React, { useEffect, useState } from 'react' import styled from 'styled-components' -const { Text, Title } = Typography +const { Text } = Typography const { Option } = Select const { TextArea } = Input @@ -31,7 +31,7 @@ const CacheServiceTests: React.FC = () => { const [sharedValue, setSharedValue] = useState('{"type": "shared", "data": "cross-window"}') const [sharedTTL, setSharedTTL] = useState('10000') - const [persistKey, setPersistKey] = useState('example-1') + const [persistKey, setPersistKey] = useState('example-1') const [persistValue, setPersistValue] = useState('updated-example-value') // Display states @@ -42,7 +42,7 @@ const CacheServiceTests: React.FC = () => { const [updateCount, setUpdateCount] = useState(0) // Available persist keys from schema - const persistKeys: PersistCacheKey[] = ['example-1', 'example-2', 'example-3', 'example-4'] + const persistKeys: RendererPersistCacheKey[] = ['example-1', 'example-2', 'example-3', 'example-4'] const parseValue = (value: string): any => { if (!value) return undefined @@ -67,7 +67,7 @@ const CacheServiceTests: React.FC = () => { const ttl = memoryTTL ? parseInt(memoryTTL) : undefined cacheService.set(memoryKey, parsed, ttl) message.success(`Memory cache set: ${memoryKey}`) - setUpdateCount(prev => prev + 1) + setUpdateCount((prev) => prev + 1) logger.info('Memory cache set', { key: memoryKey, value: parsed, ttl }) } catch (error) { message.error(`Memory cache set failed: ${(error as Error).message}`) @@ -115,7 +115,7 @@ const CacheServiceTests: React.FC = () => { const ttl = sharedTTL ? parseInt(sharedTTL) : undefined cacheService.setShared(sharedKey, parsed, ttl) message.success(`Shared cache set: ${sharedKey} (broadcasted to other windows)`) - setUpdateCount(prev => prev + 1) + setUpdateCount((prev) => prev + 1) logger.info('Shared cache set', { key: sharedKey, value: parsed, ttl }) } catch (error) { message.error(`Shared cache set failed: ${(error as Error).message}`) @@ -171,9 +171,9 @@ const CacheServiceTests: React.FC = () => { parsed = parseValue(persistValue) // object } - cacheService.setPersist(persistKey, parsed as PersistCacheSchema[typeof persistKey]) + cacheService.setPersist(persistKey, parsed as RendererPersistCacheSchema[typeof persistKey]) message.success(`Persist cache set: ${persistKey} (saved to localStorage + broadcasted)`) - setUpdateCount(prev => prev + 1) + setUpdateCount((prev) => prev + 1) logger.info('Persist cache set', { key: persistKey, value: parsed }) } catch (error) { message.error(`Persist cache set failed: ${(error as Error).message}`) @@ -227,9 +227,7 @@ const CacheServiceTests: React.FC = () => {
- - 直接测试 CacheService API • Updates: {updateCount} • Auto-refresh: 1s - + 直接测试 CacheService API • Updates: {updateCount} • Auto-refresh: 1s
@@ -246,8 +244,7 @@ const CacheServiceTests: React.FC = () => { style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }} - > + }}> { />