diff --git a/dist-electron/main/main.js b/dist-electron/main/main.js index cfe8d25..1b9d75f 100644 --- a/dist-electron/main/main.js +++ b/dist-electron/main/main.js @@ -1,6 +1,6 @@ "use strict"; require("electron"); -require("./main-3jJaZPgE.js"); +require("./main-BekteP6H.js"); require("electron-squirrel-startup"); require("electron-log"); require("bytenode"); diff --git a/electron/main.ts b/electron/main.ts index d36f20b..6bbe221 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -3,6 +3,7 @@ import { CONFIG_KEYS } from '@lib/constants' import { setupMainWindow } from './wins'; import started from 'electron-squirrel-startup' import configManager from '@electron/service/config-service' +import themeManager from '@electron/service/theme-service' import { runTaskOperationService } from '@electron/process/runTaskOperationService' import { initScriptStoreService } from '@electron/service/script-store-service' import log from 'electron-log'; @@ -154,6 +155,7 @@ if (started) { app.whenReady().then(async () => { await configManager.init(); + await themeManager.init(); gatewayManager.init(); diff --git a/electron/service/config-service/index.ts b/electron/service/config-service/index.ts index 1f1ed4f..a4f61d9 100644 --- a/electron/service/config-service/index.ts +++ b/electron/service/config-service/index.ts @@ -14,6 +14,7 @@ const DEFAULT_CONFIG: IConfig = { [CONFIG_KEYS.PROVIDER]: '', [CONFIG_KEYS.DEFAULT_MODEL]: null, [CONFIG_KEYS.SELECTED_CHANNELS]: [], + [CONFIG_KEYS.IMAGE_CACHE]: [], } export class ConfigService { diff --git a/electron/service/theme-service/index.ts b/electron/service/theme-service/index.ts index 4945e33..dcbec3d 100644 --- a/electron/service/theme-service/index.ts +++ b/electron/service/theme-service/index.ts @@ -8,15 +8,17 @@ class ThemeService { private _isDark: boolean = nativeTheme.shouldUseDarkColors; constructor() { - const themeMode = configManager.get(CONFIG_KEYS.THEME_MODE); - if (themeMode) { - nativeTheme.themeSource = themeMode; - this._isDark = nativeTheme.shouldUseDarkColors; - } this._setupIpcEvent(); logManager.info('ThemeService initialized successfully.'); } + async init() { + const themeMode = configManager.get(CONFIG_KEYS.THEME_MODE); + nativeTheme.themeSource = themeMode; + this._isDark = nativeTheme.shouldUseDarkColors; + logManager.info('ThemeService async init completed.'); + } + private _setupIpcEvent() { ipcMain.handle(IPC_EVENTS.SET_THEME_MODE, (_e, mode: ThemeMode) => { nativeTheme.themeSource = mode; @@ -25,7 +27,7 @@ class ThemeService { }); ipcMain.handle(IPC_EVENTS.GET_THEME_MODE, () => { - return nativeTheme.themeSource; + return configManager.get(CONFIG_KEYS.THEME_MODE); }); ipcMain.handle(IPC_EVENTS.IS_DARK_THEME, () => { diff --git a/electron/service/window-service/index.ts b/electron/service/window-service/index.ts index 83beeec..c902f6f 100644 --- a/electron/service/window-service/index.ts +++ b/electron/service/window-service/index.ts @@ -29,24 +29,26 @@ const isMac = process.platform === 'darwin'; const isWindows = process.platform === 'win32'; const useCustomTitleBar = isWindows; -const SHARED_WINDOW_OPTIONS = { - frame: isMac || !useCustomTitleBar, - titleBarStyle: isMac ? 'hiddenInset' : useCustomTitleBar ? 'hidden' : 'default', - trafficLightPosition: isMac ? { x: 16, y: 16 } : undefined, - show: false, - title: 'NIANXX', - darkTheme: themeManager.isDark, - backgroundColor: themeManager.isDark ? '#2C2C2C' : '#FFFFFF', - webPreferences: { - nodeIntegration: false, // 禁用 Node.js 集成,提高安全性 - contextIsolation: true, // 启用上下文隔离,防止渲染进程访问主进程 API - sandbox: true, // 启用沙箱模式,进一步增强安全性 - backgroundThrottling: false, - preload: MAIN_WINDOW_VITE_DEV_SERVER_URL - ? path.join(process.cwd(), 'dist-electron/preload/preload.js') - : path.join(__dirname, 'preload.js'), - }, -} as BrowserWindowConstructorOptions; +function getSharedWindowOptions(): BrowserWindowConstructorOptions { + return { + frame: isMac || !useCustomTitleBar, + titleBarStyle: isMac ? 'hiddenInset' : useCustomTitleBar ? 'hidden' : 'default', + trafficLightPosition: isMac ? { x: 16, y: 16 } : undefined, + show: false, + title: 'NIANXX', + darkTheme: themeManager.isDark, + backgroundColor: themeManager.isDark ? '#2C2C2C' : '#FFFFFF', + webPreferences: { + nodeIntegration: false, // 禁用 Node.js 集成,提高安全性 + contextIsolation: true, // 启用上下文隔离,防止渲染进程访问主进程 API + sandbox: true, // 启用沙箱模式,进一步增强安全性 + backgroundThrottling: false, + preload: MAIN_WINDOW_VITE_DEV_SERVER_URL + ? path.join(process.cwd(), 'dist-electron/preload/preload.js') + : path.join(__dirname, 'preload.js'), + }, + }; +} class WindowService { private static _instance: WindowService; @@ -218,7 +220,7 @@ class WindowService { return this._isHiddenWin(name) ? this._winStates[name].instance as BrowserWindow : new BrowserWindow({ - ...SHARED_WINDOW_OPTIONS, + ...getSharedWindowOptions(), icon: this._logo, ...opts, }); diff --git a/src/i18n/index.ts b/src/i18n/index.ts index d0566d6..25bbb90 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -1,6 +1,6 @@ import { createI18n, type I18n, type I18nOptions } from 'vue-i18n'; import { SUPPORTED_LANGUAGE_CODES, SUPPORTED_LANGUAGES, NAMESPACES, type LanguageCode } from './constants'; -import { resolveSupportedLanguage, detectSystemLanguage } from './resolver'; +import { resolveSupportedLanguage } from './resolver'; // 使用 import.meta.glob 动态加载所有语言文件 // 文件路径模式:./locales/{语言}/{命名空间}.json @@ -34,32 +34,12 @@ function buildResources() { return resources; } -// 获取持久化的语言设置(稍后由 Pinia store 提供) -function getPersistedLanguage(): LanguageCode | null { - try { - const saved = localStorage.getItem('zn-language'); - return saved && SUPPORTED_LANGUAGE_CODES.includes(saved as LanguageCode) ? saved as LanguageCode : null; - } catch { - return null; - } -} - -// 确定初始语言:持久化设置 > 系统语言 > 默认中文 -function determineInitialLocale(): LanguageCode { - const persisted = getPersistedLanguage(); - if (persisted) return persisted; - - const systemLang = detectSystemLanguage(); - return systemLang; -} - async function createI18nInstance() { const resources = buildResources(); - const initialLocale = determineInitialLocale(); const options: I18nOptions = { legacy: false, - locale: initialLocale, + locale: 'zh', fallbackLocale: 'zh', messages: resources, // 使用构建的资源对象 availableLocales: SUPPORTED_LANGUAGE_CODES, @@ -85,11 +65,6 @@ export async function setLanguage(lang: LanguageCode, _i18n?: I18n) { } (__i18n.global.locale as unknown as { value: LanguageCode }).value = lang; - - // 持久化到 localStorage(稍后由 Pinia store 处理) - try { - localStorage.setItem('zn-language', lang); - } catch {} } export function getLanguage() { diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 3268d59..ab0402d 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -103,6 +103,7 @@ export enum CONFIG_KEYS { AUTO_CHECK_UPDATE = 'autoCheckUpdate', AUTO_DOWNLOAD_UPDATE = 'autoDownloadUpdate', SELECTED_CHANNELS = 'selectedChannels', + IMAGE_CACHE = 'imageCache', } export enum MENU_IDS { diff --git a/src/lib/types.ts b/src/lib/types.ts index 9f03fcb..9df5037 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -20,6 +20,8 @@ export interface IConfig { [CONFIG_KEYS.DEFAULT_MODEL]?: string | null; // 选中的渠道 [CONFIG_KEYS.SELECTED_CHANNELS]: Array<{ id: string; channelName: string; channelUrl: string }>; + // 图片缓存 + [CONFIG_KEYS.IMAGE_CACHE]: Array<[string, any]>; } export interface Provider { diff --git a/src/stores/chat.ts b/src/stores/chat.ts index af978d4..de77607 100644 --- a/src/stores/chat.ts +++ b/src/stores/chat.ts @@ -9,6 +9,7 @@ import { extractText, isToolOnlyMessage, isToolResultRole, isInternalMessage } f import { hostApiFetch } from '@lib/host-api' import { gatewayRpc, onGatewayEvent } from '@lib/gateway-client' import { useProviderStore } from '@stores/providers' +import { IPC_EVENTS, CONFIG_KEYS } from '@lib/constants' // ── Constants ─────────────────────────────────────────────────── const DEFAULT_SESSION_KEY = 'agent:main:main' @@ -32,7 +33,6 @@ let _lastLoadSessionsAt = 0 const _historyLoadInFlight = new Map>() const _lastHistoryLoadAtBySession = new Map() const _chatEventDedupe = new Map() -const IMAGE_CACHE_KEY = 'zn-ai:image-cache' const IMAGE_CACHE_MAX = 100 // ── Helpers: Timers ───────────────────────────────────────────── @@ -90,29 +90,35 @@ function isDuplicateChatEvent(eventState: string, event: Record } // ── Helpers: Image Cache ──────────────────────────────────────── -function loadImageCache(): Map { +let _imageCache: Map = new Map() +let _imageCacheInitialized = false + +async function initImageCache(): Promise { + if (_imageCacheInitialized) return + _imageCache = await loadImageCache() + _imageCacheInitialized = true +} + +async function loadImageCache(): Promise> { try { - const raw = localStorage.getItem(IMAGE_CACHE_KEY) - if (raw) { - const entries = JSON.parse(raw) as Array<[string, AttachedFileMeta]> - return new Map(entries) + const raw = await window.api.invoke(IPC_EVENTS.GET_CONFIG, CONFIG_KEYS.IMAGE_CACHE) + if (Array.isArray(raw)) { + return new Map(raw as Array<[string, AttachedFileMeta]>) } } catch { /* ignore */ } return new Map() } -function saveImageCache(cache: Map): void { +async function saveImageCache(cache: Map): Promise { try { const entries = Array.from(cache.entries()) const trimmed = entries.length > IMAGE_CACHE_MAX ? entries.slice(entries.length - IMAGE_CACHE_MAX) : entries - localStorage.setItem(IMAGE_CACHE_KEY, JSON.stringify(trimmed)) + await window.api.invoke(IPC_EVENTS.SET_CONFIG, CONFIG_KEYS.IMAGE_CACHE, trimmed) } catch { /* ignore */ } } -const _imageCache = loadImageCache() - // ── Helpers: Timestamp ────────────────────────────────────────── function toMs(ts: number): number { return ts < 1e12 ? ts * 1000 : ts @@ -918,6 +924,7 @@ export const useChatStore = defineStore('chat', { try { // Cache image attachments if (attachments && attachments.length > 0) { + await initImageCache() for (const a of attachments) { _imageCache.set(a.stagedPath, { fileName: a.fileName, @@ -926,7 +933,7 @@ export const useChatStore = defineStore('chat', { preview: a.preview, }) } - saveImageCache(_imageCache) + await saveImageCache(_imageCache) } let messageContent = trimmed diff --git a/src/stores/locale.ts b/src/stores/locale.ts index 7475c89..2403f50 100644 --- a/src/stores/locale.ts +++ b/src/stores/locale.ts @@ -2,9 +2,7 @@ import { defineStore } from 'pinia'; import { i18n, setLanguage, getLanguage, type LanguageCode } from '@src/i18n'; import { SUPPORTED_LANGUAGES, SUPPORTED_LANGUAGE_CODES } from '@src/i18n/constants'; import { resolveSupportedLanguage, detectSystemLanguage } from '@src/i18n/resolver'; - -// 持久化键 -const STORAGE_KEY = 'zn-language'; +import { IPC_EVENTS, CONFIG_KEYS } from '@lib/constants'; interface LocaleState { language: LanguageCode; @@ -37,8 +35,8 @@ export const useLocaleStore = defineStore('locale', { if (this.initialized) return; try { - // 1. 尝试从 localStorage 读取持久化设置 - const saved = localStorage.getItem(STORAGE_KEY); + // 1. 尝试从 electron-store 读取持久化设置 + const saved = await window.api.invoke(IPC_EVENTS.GET_CONFIG, CONFIG_KEYS.LANGUAGE); let lang: LanguageCode = 'zh'; if (saved && SUPPORTED_LANGUAGE_CODES.includes(saved as LanguageCode)) { @@ -63,12 +61,12 @@ export const useLocaleStore = defineStore('locale', { /** * 设置语言 * @param language 目标语言代码 - * @param persist 是否持久化到 localStorage(默认为 true) + * @param persist 是否持久化到 electron-store(默认为 true) */ async setLanguage(language: LanguageCode, persist: boolean = true) { // 验证语言代码有效性 const resolvedLang = resolveSupportedLanguage(language); - + if (resolvedLang === this.language) { return; // 语言未变化 } @@ -80,14 +78,14 @@ export const useLocaleStore = defineStore('locale', { // 2. 更新 store 状态 this.language = resolvedLang; - // 3. 持久化到 localStorage + // 3. 持久化到 electron-store if (persist) { - localStorage.setItem(STORAGE_KEY, resolvedLang); + await window.api.invoke(IPC_EVENTS.SET_CONFIG, CONFIG_KEYS.LANGUAGE, resolvedLang); } // 4. 触发语言变化事件(供其他组件监听) - window.dispatchEvent(new CustomEvent('language-changed', { - detail: { language: resolvedLang } + window.dispatchEvent(new CustomEvent('language-changed', { + detail: { language: resolvedLang } })); } catch (error) { console.error('Failed to set language:', error); diff --git a/src/stores/theme.ts b/src/stores/theme.ts index 39c75b4..b202425 100644 --- a/src/stores/theme.ts +++ b/src/stores/theme.ts @@ -42,18 +42,12 @@ const applyThemeToDom = (theme: 'light' | 'dark') => { }; export const useThemeStore = defineStore('zn-ai-theme', { - state: (): ThemeState => { - // 从 localStorage 恢复缓存的主题,确保在初始化前 UI 不会闪烁 - const cachedTheme = typeof localStorage !== 'undefined' ? localStorage.getItem('zn-ai-theme-cache') as Theme : null; - const initialTheme = (cachedTheme === 'light' || cachedTheme === 'dark' || cachedTheme === 'system') ? cachedTheme : 'system'; - - return { - theme: initialTheme, - isDark: false, - systemTheme: 'light', - initialized: false, - }; - }, + state: (): ThemeState => ({ + theme: 'system', + isDark: false, + systemTheme: 'light', + initialized: false, + }), actions: { async init() { @@ -83,12 +77,7 @@ export const useThemeStore = defineStore('zn-ai-theme', { this.isDark = isDark; this.systemTheme = systemTheme; this.initialized = true; - - // 缓存到 localStorage - if (typeof localStorage !== 'undefined') { - localStorage.setItem('zn-ai-theme-cache', savedTheme); - } - + // 6. 监听系统主题变化 if (typeof window !== 'undefined') { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); @@ -159,12 +148,7 @@ export const useThemeStore = defineStore('zn-ai-theme', { // 4. 更新 store 状态 this.theme = theme; this.isDark = isDark; - - // 5. 缓存到 localStorage - if (typeof localStorage !== 'undefined') { - localStorage.setItem('zn-ai-theme-cache', theme); - } - + console.log('Theme changed:', { theme, appliedTheme, isDark }); } catch (error) { console.error('Failed to set theme:', error); diff --git a/src/utils/cache.ts b/src/utils/cache.ts index a1edc46..8f469b3 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -37,52 +37,9 @@ const sessionCache = { } } -const localCache = { - set (key: string, value: string) { - if (!localStorage) { - return - } - if (key != null && value != null) { - localStorage.setItem(key, value) - } - }, - - get (key: string) { - if (!localStorage) { - return null - } - if (key == null) { - return null - } - return localStorage.getItem(key) - }, - - setJSON (key: string, jsonValue: any) { - if (jsonValue != null) { - this.set(key, JSON.stringify(jsonValue)) - } - }, - - getJSON (key: string) { - const value = this.get(key) - if (value != null) { - return JSON.parse(value) - } - return null - }, - - remove (key: string) { - localStorage.removeItem(key) - } -} - export default { /** * 会话级缓存 */ - session: sessionCache, - /** - * 本地缓存 - */ - local: localCache + session: sessionCache } diff --git a/src/utils/storage.ts b/src/utils/storage.ts index 5bab515..1f010fd 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -1,41 +1,5 @@ import Cookies from 'js-cookie'; -/** - * window.localStorage 浏览器永久缓存 - * @method set 设置永久缓存 - * @method get 获取永久缓存 - * @method remove 移除永久缓存 - * @method clear 移除全部永久缓存 - */ -export const Local = { - // 查看 v2.4.3版本更新日志 - setKey(key: string) { - // @ts-ignore - return `${__NEXT_NAME__}:${key}`; - }, - - // 设置永久缓存 - set(key: string, val: T) { - window.localStorage.setItem(Local.setKey(key), JSON.stringify(val)); - }, - - // 获取永久缓存 - get(key: string) { - let json = window.localStorage.getItem(Local.setKey(key)); - return JSON.parse(json); - }, - - // 移除永久缓存 - remove(key: string) { - window.localStorage.removeItem(Local.setKey(key)); - }, - - // 移除全部永久缓存 - clear() { - window.localStorage.clear(); - }, -}; - /** * window.sessionStorage 浏览器临时缓存 * @method set 设置临时缓存 @@ -81,8 +45,4 @@ export const Session = { return this.get('token'); }, - // 获取当前的租户 - getTenant() { - return Local.get('tenantId') ? Local.get('tenantId') : 1; - }, };