diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index ef2d578e7a..25c1f0c4a6 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -157,5 +157,10 @@ export enum IpcChannel { // Memory File Storage Memory_LoadData = 'memory:load-data', - Memory_SaveData = 'memory:save-data' + Memory_SaveData = 'memory:save-data', + Memory_DeleteShortMemoryById = 'memory:delete-short-memory-by-id', + + // Long-term Memory File Storage + LongTermMemory_LoadData = 'long-term-memory:load-data', + LongTermMemory_SaveData = 'long-term-memory:save-data' } diff --git a/src/main/ipc.ts b/src/main/ipc.ts index b59d1bfafa..b16cfa3faa 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -26,6 +26,7 @@ import { searchService } from './services/SearchService' import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService' import { TrayService } from './services/TrayService' import { windowService } from './services/WindowService' +import { memoryFileService } from './services/MemoryFileService' import { getResourcePath } from './utils' import { decrypt, encrypt } from './utils/aes' import { getConfigDir, getFilesDir } from './utils/file' @@ -306,4 +307,21 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.SearchWindow_OpenUrl, async (_, uid: string, url: string) => { return await searchService.openUrlInSearchWindow(uid, url) }) + + // memory + ipcMain.handle(IpcChannel.Memory_LoadData, async () => { + return await memoryFileService.loadData() + }) + ipcMain.handle(IpcChannel.Memory_SaveData, async (_, data, forceOverwrite = false) => { + return await memoryFileService.saveData(data, forceOverwrite) + }) + ipcMain.handle(IpcChannel.Memory_DeleteShortMemoryById, async (_, id) => { + return await memoryFileService.deleteShortMemoryById(id) + }) + ipcMain.handle(IpcChannel.LongTermMemory_LoadData, async () => { + return await memoryFileService.loadLongTermData() + }) + ipcMain.handle(IpcChannel.LongTermMemory_SaveData, async (_, data, forceOverwrite = false) => { + return await memoryFileService.saveLongTermData(data, forceOverwrite) + }) } diff --git a/src/main/services/MemoryFileService.ts b/src/main/services/MemoryFileService.ts index 5c01294c31..41fd33691b 100644 --- a/src/main/services/MemoryFileService.ts +++ b/src/main/services/MemoryFileService.ts @@ -1,126 +1,305 @@ import { promises as fs } from 'fs' import path from 'path' import { getConfigDir } from '../utils/file' -import { IpcChannel } from '@shared/IpcChannel' -import { ipcMain } from 'electron' import log from 'electron-log' // 定义记忆文件路径 const memoryDataPath = path.join(getConfigDir(), 'memory-data.json') +// 定义长期记忆文件路径 +const longTermMemoryDataPath = path.join(getConfigDir(), 'long-term-memory-data.json') export class MemoryFileService { constructor() { this.registerIpcHandlers() } - private registerIpcHandlers() { - // 读取记忆数据 - ipcMain.handle(IpcChannel.Memory_LoadData, async () => { + async loadData() { + try { + // 确保配置目录存在 + const configDir = path.dirname(memoryDataPath) try { - // 确保配置目录存在 - const configDir = path.dirname(memoryDataPath) - try { - await fs.mkdir(configDir, { recursive: true }) - } catch (mkdirError) { - log.warn('Failed to create config directory, it may already exist:', mkdirError) - } - - // 检查文件是否存在 - try { - await fs.access(memoryDataPath) - } catch (accessError) { - // 文件不存在,创建默认文件 - log.info('Memory data file does not exist, creating default file') - const defaultData = { - memoryLists: [{ - id: 'default', - name: '默认列表', - isActive: true - }], - memories: [], - shortMemories: [], - analyzeModel: 'gpt-3.5-turbo', - shortMemoryAnalyzeModel: 'gpt-3.5-turbo', - vectorizeModel: 'gpt-3.5-turbo' - } - await fs.writeFile(memoryDataPath, JSON.stringify(defaultData, null, 2)) - return defaultData - } - - // 读取文件 - const data = await fs.readFile(memoryDataPath, 'utf-8') - const parsedData = JSON.parse(data) - log.info('Memory data loaded successfully') - return parsedData - } catch (error) { - log.error('Failed to load memory data:', error) - return null + await fs.mkdir(configDir, { recursive: true }) + } catch (mkdirError) { + log.warn('Failed to create config directory, it may already exist:', mkdirError) } - }) - // 保存记忆数据 - ipcMain.handle(IpcChannel.Memory_SaveData, async (_, data) => { + // 检查文件是否存在 try { - // 确保配置目录存在 - const configDir = path.dirname(memoryDataPath) - try { - await fs.mkdir(configDir, { recursive: true }) - } catch (mkdirError) { - log.warn('Failed to create config directory, it may already exist:', mkdirError) + await fs.access(memoryDataPath) + } catch (accessError) { + // 文件不存在,创建默认文件 + log.info('Memory data file does not exist, creating default file') + const defaultData = { + memoryLists: [{ + id: 'default', + name: '默认列表', + isActive: true + }], + shortMemories: [], + analyzeModel: 'gpt-3.5-turbo', + shortMemoryAnalyzeModel: 'gpt-3.5-turbo', + historicalContextAnalyzeModel: 'gpt-3.5-turbo', + vectorizeModel: 'gpt-3.5-turbo' + } + await fs.writeFile(memoryDataPath, JSON.stringify(defaultData, null, 2)) + return defaultData + } + + // 读取文件 + const data = await fs.readFile(memoryDataPath, 'utf-8') + const parsedData = JSON.parse(data) + log.info('Memory data loaded successfully') + return parsedData + } catch (error) { + log.error('Failed to load memory data:', error) + return null + } + } + + async saveData(data: any, forceOverwrite: boolean = false) { + try { + // 确保配置目录存在 + const configDir = path.dirname(memoryDataPath) + try { + await fs.mkdir(configDir, { recursive: true }) + } catch (mkdirError) { + log.warn('Failed to create config directory, it may already exist:', mkdirError) + } + + // 如果强制覆盖,直接使用传入的数据 + if (forceOverwrite) { + log.info('Force overwrite enabled for short memory data, using provided data directly') + + // 确保数据包含必要的字段 + const defaultData = { + memoryLists: [], + shortMemories: [], + analyzeModel: '', + shortMemoryAnalyzeModel: '', + historicalContextAnalyzeModel: '', + vectorizeModel: '' } - // 尝试读取现有数据并合并 - let existingData = {} - try { - await fs.access(memoryDataPath) - const fileContent = await fs.readFile(memoryDataPath, 'utf-8') - existingData = JSON.parse(fileContent) - log.info('Existing memory data loaded for merging') - } catch (readError) { - log.warn('No existing memory data found or failed to read:', readError) - // 如果文件不存在或读取失败,使用空对象 - } + // 合并默认数据和传入的数据,确保数据结构完整 + const completeData = { ...defaultData, ...data } - // 合并数据,注意数组的处理 - const mergedData = { ...existingData } + // 保存数据 + await fs.writeFile(memoryDataPath, JSON.stringify(completeData, null, 2)) + log.info('Memory data saved successfully (force overwrite)') + return true + } - // 处理每个属性 - Object.entries(data).forEach(([key, value]) => { - // 如果是数组属性,需要特殊处理 - if (Array.isArray(value) && Array.isArray(mergedData[key])) { - // 对于 memories 和 shortMemories,需要合并而不是覆盖 - if (key === 'memories' || key === 'shortMemories') { - // 创建一个集合来跟踪已存在的记忆ID - const existingIds = new Set(mergedData[key].map(item => item.id)) + // 尝试读取现有数据并合并 + let existingData = {} + try { + await fs.access(memoryDataPath) + const fileContent = await fs.readFile(memoryDataPath, 'utf-8') + existingData = JSON.parse(fileContent) + log.info('Existing memory data loaded for merging') + } catch (readError) { + log.warn('No existing memory data found or failed to read:', readError) + // 如果文件不存在或读取失败,使用空对象 + } - // 将新记忆添加到现有记忆中,避免重复 - value.forEach(item => { - if (item.id && !existingIds.has(item.id)) { - mergedData[key].push(item) - existingIds.add(item.id) - } - }) - } else { - // 其他数组属性,使用新值 - mergedData[key] = value - } + // 合并数据,注意数组的处理 + const mergedData = { ...existingData } + + // 处理每个属性 + Object.entries(data).forEach(([key, value]) => { + // 如果是数组属性,需要特殊处理 + if (Array.isArray(value) && Array.isArray(mergedData[key])) { + // 对于 shortMemories 和 memories,直接使用传入的数组,完全替换现有的记忆 + if (key === 'shortMemories' || key === 'memories') { + mergedData[key] = value + log.info(`Replacing ${key} array with provided data`) } else { - // 非数组属性,直接使用新值 + // 其他数组属性,使用新值 mergedData[key] = value } - }) + } else { + // 非数组属性,直接使用新值 + mergedData[key] = value + } + }) - // 保存合并后的数据 - await fs.writeFile(memoryDataPath, JSON.stringify(mergedData, null, 2)) - log.info('Memory data saved successfully') + // 保存合并后的数据 + await fs.writeFile(memoryDataPath, JSON.stringify(mergedData, null, 2)) + log.info('Memory data saved successfully') + return true + } catch (error) { + log.error('Failed to save memory data:', error) + return false + } + } + + async loadLongTermData() { + try { + // 确保配置目录存在 + const configDir = path.dirname(longTermMemoryDataPath) + try { + await fs.mkdir(configDir, { recursive: true }) + } catch (mkdirError) { + log.warn('Failed to create config directory, it may already exist:', mkdirError) + } + + // 检查文件是否存在 + try { + await fs.access(longTermMemoryDataPath) + } catch (accessError) { + // 文件不存在,创建默认文件 + log.info('Long-term memory data file does not exist, creating default file') + const now = new Date().toISOString() + const defaultData = { + memoryLists: [{ + id: 'default', + name: '默认列表', + isActive: true, + createdAt: now, + updatedAt: now + }], + memories: [], + currentListId: 'default', + analyzeModel: 'gpt-3.5-turbo' + } + await fs.writeFile(longTermMemoryDataPath, JSON.stringify(defaultData, null, 2)) + return defaultData + } + + // 读取文件 + const data = await fs.readFile(longTermMemoryDataPath, 'utf-8') + const parsedData = JSON.parse(data) + log.info('Long-term memory data loaded successfully') + return parsedData + } catch (error) { + log.error('Failed to load long-term memory data:', error) + return null + } + } + + async saveLongTermData(data: any, forceOverwrite: boolean = false) { + try { + // 确保配置目录存在 + const configDir = path.dirname(longTermMemoryDataPath) + try { + await fs.mkdir(configDir, { recursive: true }) + } catch (mkdirError) { + log.warn('Failed to create config directory, it may already exist:', mkdirError) + } + + // 如果强制覆盖,直接使用传入的数据 + if (forceOverwrite) { + log.info('Force overwrite enabled, using provided data directly') + + // 确保数据包含必要的字段 + const defaultData = { + memoryLists: [], + memories: [], + currentListId: '', + analyzeModel: '' + } + + // 合并默认数据和传入的数据,确保数据结构完整 + const completeData = { ...defaultData, ...data } + + // 保存数据 + await fs.writeFile(longTermMemoryDataPath, JSON.stringify(completeData, null, 2)) + log.info('Long-term memory data saved successfully (force overwrite)') return true - } catch (error) { - log.error('Failed to save memory data:', error) + } + + // 尝试读取现有数据并合并 + let existingData = {} + try { + await fs.access(longTermMemoryDataPath) + const fileContent = await fs.readFile(longTermMemoryDataPath, 'utf-8') + existingData = JSON.parse(fileContent) + log.info('Existing long-term memory data loaded for merging') + } catch (readError) { + log.warn('No existing long-term memory data found or failed to read:', readError) + // 如果文件不存在或读取失败,使用空对象 + } + + // 合并数据,注意数组的处理 + const mergedData = { ...existingData } + + // 处理每个属性 + Object.entries(data).forEach(([key, value]) => { + // 如果是数组属性,需要特殊处理 + if (Array.isArray(value) && Array.isArray(mergedData[key])) { + // 对于 memories 和 shortMemories,直接使用传入的数组,完全替换现有的记忆 + if (key === 'memories' || key === 'shortMemories') { + mergedData[key] = value + log.info(`Replacing ${key} array with provided data`) + } else { + // 其他数组属性,使用新值 + mergedData[key] = value + } + } else { + // 非数组属性,直接使用新值 + mergedData[key] = value + } + }) + + // 保存合并后的数据 + await fs.writeFile(longTermMemoryDataPath, JSON.stringify(mergedData, null, 2)) + log.info('Long-term memory data saved successfully') + return true + } catch (error) { + log.error('Failed to save long-term memory data:', error) + return false + } + } + + /** + * 删除指定ID的短期记忆 + * @param id 要删除的短期记忆ID + * @returns 是否成功删除 + */ + async deleteShortMemoryById(id: string) { + try { + // 检查文件是否存在 + try { + await fs.access(memoryDataPath) + } catch (accessError) { + log.error('Memory data file does not exist, cannot delete memory') return false } - }) + + // 读取文件 + const fileContent = await fs.readFile(memoryDataPath, 'utf-8') + const data = JSON.parse(fileContent) + + // 检查shortMemories数组是否存在 + if (!data.shortMemories || !Array.isArray(data.shortMemories)) { + log.error('No shortMemories array found in memory data file') + return false + } + + // 过滤掉要删除的记忆 + const originalLength = data.shortMemories.length + data.shortMemories = data.shortMemories.filter((memory: any) => memory.id !== id) + + // 如果长度没变,说明没有找到要删除的记忆 + if (data.shortMemories.length === originalLength) { + log.warn(`Short memory with ID ${id} not found, nothing to delete`) + return false + } + + // 写回文件 + await fs.writeFile(memoryDataPath, JSON.stringify(data, null, 2)) + log.info(`Successfully deleted short memory with ID ${id}`) + return true + } catch (error) { + log.error('Failed to delete short memory:', error) + return false + } + } + + private registerIpcHandlers() { + // 注册处理函数已移至ipc.ts文件中 + // 这里不需要重复注册 } } -// 创建单例实例 +// 创建并导出MemoryFileService实例 export const memoryFileService = new MemoryFileService() diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 12c78b4f8f..70b9d6e391 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -192,7 +192,10 @@ declare global { } memory: { loadData: () => Promise - saveData: (data: any) => Promise + saveData: (data: any, forceOverwrite?: boolean) => Promise + deleteShortMemoryById: (id: string) => Promise + loadLongTermData: () => Promise + saveLongTermData: (data: any, forceOverwrite?: boolean) => Promise } } } diff --git a/src/preload/index.ts b/src/preload/index.ts index dc69e6af59..1310c458ee 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -180,7 +180,10 @@ const api = { }, memory: { loadData: () => ipcRenderer.invoke(IpcChannel.Memory_LoadData), - saveData: (data: any) => ipcRenderer.invoke(IpcChannel.Memory_SaveData, data) + saveData: (data: any) => ipcRenderer.invoke(IpcChannel.Memory_SaveData, data), + deleteShortMemoryById: (id: string) => ipcRenderer.invoke(IpcChannel.Memory_DeleteShortMemoryById, id), + loadLongTermData: () => ipcRenderer.invoke(IpcChannel.LongTermMemory_LoadData), + saveLongTermData: (data: any, forceOverwrite: boolean = false) => ipcRenderer.invoke(IpcChannel.LongTermMemory_SaveData, data, forceOverwrite) } } diff --git a/src/renderer/src/components/MemoryProvider.tsx b/src/renderer/src/components/MemoryProvider.tsx index 6d782911d1..34ebaf8d25 100644 --- a/src/renderer/src/components/MemoryProvider.tsx +++ b/src/renderer/src/components/MemoryProvider.tsx @@ -1,7 +1,27 @@ import { useMemoryService } from '@renderer/services/MemoryService' import { useAppDispatch, useAppSelector } from '@renderer/store' import store from '@renderer/store' -import { clearShortMemories, loadMemoryData } from '@renderer/store/memory' +import { + clearShortMemories, + loadMemoryData, + loadLongTermMemoryData, + setCurrentMemoryList, + setMemoryActive, + setShortMemoryActive, + setAutoAnalyze, + setAdaptiveAnalysisEnabled, + setAnalysisFrequency, + setAnalysisDepth, + setInterestTrackingEnabled, + setMonitoringEnabled, + setPriorityManagementEnabled, + setDecayEnabled, + setFreshnessEnabled, + setDecayRate, + setContextualRecommendationEnabled, + setAutoRecommendMemories, + setRecommendationThreshold +} from '@renderer/store/memory' import { FC, ReactNode, useEffect, useRef } from 'react' interface MemoryProviderProps { @@ -38,20 +58,82 @@ const MemoryProvider: FC = ({ children }) => { // 添加一个 ref 来存储上次分析时的消息数量 const lastAnalyzedCountRef = useRef(0) - // 在组件挂载时加载记忆数据 + // 在组件挂载时加载记忆数据和设置 useEffect(() => { console.log('[MemoryProvider] Loading memory data from file') - // 使用Redux Thunk加载记忆数据 + // 使用Redux Thunk加载短期记忆数据 dispatch(loadMemoryData()) .then((result) => { if (result.payload) { - console.log('[MemoryProvider] Memory data loaded successfully via Redux Thunk') + console.log('[MemoryProvider] Short-term memory data loaded successfully via Redux Thunk') + + // 更新所有设置 + const data = result.payload + + // 基本设置 + if (data.isActive !== undefined) dispatch(setMemoryActive(data.isActive)) + if (data.shortMemoryActive !== undefined) dispatch(setShortMemoryActive(data.shortMemoryActive)) + if (data.autoAnalyze !== undefined) dispatch(setAutoAnalyze(data.autoAnalyze)) + + // 自适应分析相关 + if (data.adaptiveAnalysisEnabled !== undefined) dispatch(setAdaptiveAnalysisEnabled(data.adaptiveAnalysisEnabled)) + if (data.analysisFrequency !== undefined) dispatch(setAnalysisFrequency(data.analysisFrequency)) + if (data.analysisDepth !== undefined) dispatch(setAnalysisDepth(data.analysisDepth)) + + // 用户关注点相关 + if (data.interestTrackingEnabled !== undefined) dispatch(setInterestTrackingEnabled(data.interestTrackingEnabled)) + + // 性能监控相关 + if (data.monitoringEnabled !== undefined) dispatch(setMonitoringEnabled(data.monitoringEnabled)) + + // 智能优先级与时效性管理相关 + if (data.priorityManagementEnabled !== undefined) dispatch(setPriorityManagementEnabled(data.priorityManagementEnabled)) + if (data.decayEnabled !== undefined) dispatch(setDecayEnabled(data.decayEnabled)) + if (data.freshnessEnabled !== undefined) dispatch(setFreshnessEnabled(data.freshnessEnabled)) + if (data.decayRate !== undefined) dispatch(setDecayRate(data.decayRate)) + + // 上下文感知记忆推荐相关 + if (data.contextualRecommendationEnabled !== undefined) dispatch(setContextualRecommendationEnabled(data.contextualRecommendationEnabled)) + if (data.autoRecommendMemories !== undefined) dispatch(setAutoRecommendMemories(data.autoRecommendMemories)) + if (data.recommendationThreshold !== undefined) dispatch(setRecommendationThreshold(data.recommendationThreshold)) + + console.log('[MemoryProvider] Memory settings loaded successfully') } else { - console.log('[MemoryProvider] No memory data loaded or loading failed') + console.log('[MemoryProvider] No short-term memory data loaded or loading failed') } }) .catch(error => { - console.error('[MemoryProvider] Error loading memory data:', error) + console.error('[MemoryProvider] Error loading short-term memory data:', error) + }) + + // 使用Redux Thunk加载长期记忆数据 + dispatch(loadLongTermMemoryData()) + .then((result) => { + if (result.payload) { + console.log('[MemoryProvider] Long-term memory data loaded successfully via Redux Thunk') + + // 确保在长期记忆数据加载后,检查并设置当前记忆列表 + setTimeout(() => { + const state = store.getState().memory + if (!state.currentListId && state.memoryLists && state.memoryLists.length > 0) { + // 先尝试找到一个isActive为true的列表 + const activeList = state.memoryLists.find(list => list.isActive) + if (activeList) { + console.log('[MemoryProvider] Auto-selecting active memory list:', activeList.name) + dispatch(setCurrentMemoryList(activeList.id)) + } else { + // 如果没有激活的列表,使用第一个列表 + console.log('[MemoryProvider] Auto-selecting first memory list:', state.memoryLists[0].name) + dispatch(setCurrentMemoryList(state.memoryLists[0].id)) + } + } + }, 500) // 添加一个小延迟,确保状态已更新 + } else { + console.log('[MemoryProvider] No long-term memory data loaded or loading failed') + } + }) + .catch(error => { + console.error('[MemoryProvider] Error loading long-term memory data:', error) }) }, [dispatch]) @@ -100,6 +182,43 @@ const MemoryProvider: FC = ({ children }) => { previousTopicRef.current = currentTopic || null }, [currentTopic, shortMemoryActive, dispatch]) + // 监控记忆列表变化,确保总是有一个选中的记忆列表 + useEffect(() => { + // 立即检查一次 + const checkAndSetMemoryList = () => { + const state = store.getState().memory + if (state.memoryLists && state.memoryLists.length > 0) { + // 如果没有选中的记忆列表,或者选中的列表不存在 + if (!state.currentListId || !state.memoryLists.some(list => list.id === state.currentListId)) { + // 先尝试找到一个isActive为true的列表 + const activeList = state.memoryLists.find(list => list.isActive) + if (activeList) { + console.log('[MemoryProvider] Setting active memory list:', activeList.name) + dispatch(setCurrentMemoryList(activeList.id)) + } else if (state.memoryLists.length > 0) { + // 如果没有激活的列表,使用第一个列表 + console.log('[MemoryProvider] Setting first memory list:', state.memoryLists[0].name) + dispatch(setCurrentMemoryList(state.memoryLists[0].id)) + } + } + } + } + + // 立即检查一次 + checkAndSetMemoryList() + + // 设置定时器,每秒检查一次,持续5秒 + const intervalId = setInterval(checkAndSetMemoryList, 1000) + const timeoutId = setTimeout(() => { + clearInterval(intervalId) + }, 5000) + + return () => { + clearInterval(intervalId) + clearTimeout(timeoutId) + } + }, [dispatch]) + return <>{children} } diff --git a/src/renderer/src/components/Popups/ShortMemoryPopup.tsx b/src/renderer/src/components/Popups/ShortMemoryPopup.tsx index daf35710f8..a32687fdce 100644 --- a/src/renderer/src/components/Popups/ShortMemoryPopup.tsx +++ b/src/renderer/src/components/Popups/ShortMemoryPopup.tsx @@ -5,8 +5,9 @@ import { addShortMemoryItem, analyzeAndAddShortMemories } from '@renderer/servic import { useAppDispatch, useAppSelector } from '@renderer/store' import store from '@renderer/store' import { deleteShortMemory } from '@renderer/store/memory' -import { Button, Card, Col, Empty, Input, List, Modal, Row, Statistic, Tooltip } from 'antd' -import { useState } from 'react' +import { Button, Card, Col, Empty, Input, List, Modal, Row, Statistic, Tooltip, message } from 'antd' +import { useState, useCallback } from 'react' +import _ from 'lodash' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -47,16 +48,16 @@ const PopupContainer: React.FC = ({ topicId, resolve }) => { const [newMemoryContent, setNewMemoryContent] = useState('') const [isAnalyzing, setIsAnalyzing] = useState(false) - // 添加新的短记忆 - const handleAddMemory = () => { + // 添加新的短记忆 - 使用防抖减少频繁更新 + const handleAddMemory = useCallback(_.debounce(() => { if (newMemoryContent.trim() && topicId) { addShortMemoryItem(newMemoryContent.trim(), topicId) setNewMemoryContent('') // 清空输入框 } - } + }, 300), [newMemoryContent, topicId]) - // 手动分析对话内容 - const handleAnalyzeConversation = async () => { + // 手动分析对话内容 - 使用节流避免频繁分析操作 + const handleAnalyzeConversation = useCallback(_.throttle(async () => { if (!topicId || !shortMemoryActive) return setIsAnalyzing(true) @@ -84,13 +85,43 @@ const PopupContainer: React.FC = ({ topicId, resolve }) => { } finally { setIsAnalyzing(false) } - } + }, 1000), [topicId, shortMemoryActive, t]) - // 删除短记忆 - 直接删除无需确认 - const handleDeleteMemory = (id: string) => { - // 直接删除记忆,无需确认对话框 + // 删除短记忆 - 直接删除无需确认,使用节流避免频繁删除操作 + const handleDeleteMemory = useCallback(_.throttle(async (id: string) => { + // 先从当前状态中获取要删除的记忆之外的所有记忆 + const state = store.getState().memory + const filteredShortMemories = state.shortMemories.filter(memory => memory.id !== id) + + // 执行删除操作 dispatch(deleteShortMemory(id)) - } + + // 直接使用 window.api.memory.saveData 方法保存过滤后的列表 + try { + // 加载当前文件数据 + const currentData = await window.api.memory.loadData() + + // 替换 shortMemories 数组 + const newData = { + ...currentData, + shortMemories: filteredShortMemories + } + + // 使用 true 参数强制覆盖文件 + const result = await window.api.memory.saveData(newData, true) + + if (result) { + console.log(`[ShortMemoryPopup] Successfully deleted short memory with ID ${id}`) + message.success(t('settings.memory.deleteSuccess') || '删除成功') + } else { + console.error(`[ShortMemoryPopup] Failed to delete short memory with ID ${id}`) + message.error(t('settings.memory.deleteError') || '删除失败') + } + } catch (error) { + console.error('[ShortMemoryPopup] Failed to delete short memory:', error) + message.error(t('settings.memory.deleteError') || '删除失败') + } + }, 500), [dispatch, t]) const onClose = () => { setOpen(false) @@ -122,11 +153,11 @@ const PopupContainer: React.FC = ({ topicId, resolve }) => { - diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 23471fadbe..86d60b08c5 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1025,6 +1025,13 @@ "launch.title": "Launch", "launch.totray": "Minimize to Tray on Launch", "memory": { + "historicalContext": { + "title": "Historical Dialog Context", + "description": "Allow AI to automatically reference historical dialogs when needed, to provide more coherent answers.", + "enable": "Enable Historical Dialog Context", + "enableTip": "When enabled, AI will automatically analyze and reference historical dialogs when needed, to provide more coherent answers", + "analyzeModelTip": "Select the model used for historical dialog context analysis, it's recommended to choose a model with faster response" + }, "title": "Memory Function", "description": "Manage AI assistant's long-term memory, automatically analyze conversations and extract important information", "enableMemory": "Enable Memory Function", @@ -1048,6 +1055,14 @@ "startingAnalysis": "Starting analysis...", "cannotAnalyze": "Cannot analyze, please check settings", "resetAnalyzingState": "Reset Analysis State", + "resetLongTermMemory": "Reset Analysis Markers", + "resetLongTermMemorySuccess": "Long-term memory analysis markers reset", + "resetLongTermMemoryNoChange": "No analysis markers to reset", + "resetLongTermMemoryError": "Failed to reset long-term memory analysis markers", + "saveAllSettings": "Save All Settings", + "saveAllSettingsDescription": "Save all memory function settings to file to ensure they persist after application restart.", + "saveAllSettingsSuccess": "All settings saved successfully", + "saveAllSettingsError": "Failed to save settings", "analyzeConversation": "Analyze Conversation", "shortMemoryAnalysisSuccess": "Analysis Successful", "shortMemoryAnalysisSuccessContent": "Successfully extracted and added important information to short-term memory", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index c49627a549..5476f63f6f 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1025,6 +1025,13 @@ "launch.title": "启动", "launch.totray": "启动时最小化到托盘", "memory": { + "historicalContext": { + "title": "历史对话上下文", + "description": "允许AI在需要时自动引用历史对话,以提供更连贯的回答。", + "enable": "启用历史对话上下文", + "enableTip": "启用后,AI会在需要时自动分析并引用历史对话,以提供更连贯的回答", + "analyzeModelTip": "选择用于历史对话上下文分析的模型,建议选择响应较快的模型" + }, "title": "记忆功能", "description": "管理AI助手的长期记忆,自动分析对话并提取重要信息", "enableMemory": "启用记忆功能", @@ -1053,6 +1060,14 @@ "startingAnalysis": "开始分析...", "cannotAnalyze": "无法分析,请检查设置", "resetAnalyzingState": "重置分析状态", + "resetLongTermMemory": "重置分析标记", + "resetLongTermMemorySuccess": "长期记忆分析标记已重置", + "resetLongTermMemoryNoChange": "没有需要重置的分析标记", + "resetLongTermMemoryError": "重置长期记忆分析标记失败", + "saveAllSettings": "保存所有设置", + "saveAllSettingsDescription": "将所有记忆功能的设置保存到文件中,确保应用重启后设置仍然生效。", + "saveAllSettingsSuccess": "所有设置已成功保存", + "saveAllSettingsError": "保存设置失败", "analyzeConversation": "分析对话", "shortMemoryAnalysisSuccess": "分析成功", "shortMemoryAnalysisSuccessContent": "已成功提取并添加重要信息到短期记忆", @@ -1181,9 +1196,13 @@ "noCurrentTopic": "请先选择一个对话话题", "confirmDelete": "确认删除", "confirmDeleteContent": "确定要删除这条短期记忆吗?", + "confirmDeleteAll": "确认删除全部", + "confirmDeleteAllContent": "确定要删除该话题下的所有短期记忆吗?", "delete": "删除", + "cancel": "取消", "allTopics": "所有话题", - "noTopics": "没有话题" + "noTopics": "没有话题", + "shortMemoriesByTopic": "按话题分组的短期记忆" }, "mcp": { "actions": "操作", diff --git a/src/renderer/src/pages/settings/MemorySettings/CollapsibleShortMemoryManager.tsx b/src/renderer/src/pages/settings/MemorySettings/CollapsibleShortMemoryManager.tsx index 9cfa368b02..7c31660e1b 100644 --- a/src/renderer/src/pages/settings/MemorySettings/CollapsibleShortMemoryManager.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/CollapsibleShortMemoryManager.tsx @@ -1,61 +1,210 @@ -import { DeleteOutlined } from '@ant-design/icons' +import { DeleteOutlined, ClearOutlined } from '@ant-design/icons' import { TopicManager } from '@renderer/hooks/useTopic' -import { addShortMemoryItem } from '@renderer/services/MemoryService' import { useAppDispatch, useAppSelector } from '@renderer/store' import store from '@renderer/store' -import { deleteShortMemory, setShortMemoryActive, ShortMemory } from '@renderer/store/memory' // Import ShortMemory from here -import { Topic } from '@renderer/types' // Remove ShortMemory import from here -import { Button, Collapse, Empty, Input, List, Switch, Tooltip, Typography } from 'antd' -import { useEffect, useState } from 'react' +import { deleteShortMemory } from '@renderer/store/memory' +import { Button, Collapse, Empty, List, Modal, Pagination, Tooltip, Typography } from 'antd' +import { useEffect, useState, useCallback, memo } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -const { Title } = Typography -// 不再需要确认对话框 -// const { Panel } = Collapse // Panel is no longer used +// 定义话题和记忆的接口 +interface TopicWithMemories { + topic: { + id: string + name: string + assistantId: string + createdAt: string + updatedAt: string + messages: any[] + } + memories: ShortMemory[] + currentPage?: number // 当前页码 +} -const HeaderContainer = styled.div` +// 短期记忆接口 +interface ShortMemory { + id: string + content: string + topicId: string + createdAt: string + updatedAt?: string // 可选属性 +} + +// 记忆项组件的属性 +interface MemoryItemProps { + memory: ShortMemory + onDelete: (id: string) => void + t: any + index: number // 添加索引属性,用于显示序号 +} + +// 样式组件 +const LoadingContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 200px; + width: 100%; + color: var(--color-text-2); +` + +const StyledCollapse = styled(Collapse)` + width: 100%; + background-color: transparent; + border: none; + + .ant-collapse-item { + margin-bottom: 8px; + border: 1px solid var(--color-border); + border-radius: 4px; + overflow: hidden; + } + + .ant-collapse-header { + background-color: var(--color-bg-2); + padding: 8px 16px !important; + position: relative; + } + + /* 确保折叠图标不会遮挡内容 */ + .ant-collapse-expand-icon { + margin-right: 8px; + } + + .ant-collapse-content { + border-top: 1px solid var(--color-border); + } + + .ant-collapse-content-box { + padding: 4px 0 !important; /* 减少上下内边距,保持左右为0 */ + } +` + +const CollapseHeader = styled.div` display: flex; justify-content: space-between; align-items: center; - margin-bottom: 16px; + width: 100%; + padding-right: 24px; /* 为删除按钮留出空间 */ + + /* 左侧内容区域,包含话题名称和记忆数量 */ + > span { + margin-right: auto; + display: flex; + align-items: center; + } + + /* 删除按钮样式 */ + .ant-btn { + margin-left: 8px; + } ` -const InputContainer = styled.div` - margin-bottom: 16px; -` - -const LoadingContainer = styled.div` +const MemoryCount = styled.span` + background-color: var(--color-primary); + color: white; + border-radius: 10px; + padding: 0 8px; + font-size: 12px; + margin-left: 8px; + min-width: 24px; text-align: center; - padding: 20px 0; + display: inline-block; + z-index: 1; /* 确保计数显示在最上层 */ ` -const AddButton = styled(Button)` - margin-top: 8px; +const MemoryContent = styled.div` + word-break: break-word; + font-size: 14px; + line-height: 1.6; + margin-bottom: 4px; + padding: 4px 0; ` -interface TopicWithMemories { - topic: Topic - memories: ShortMemory[] -} +const PaginationContainer = styled.div` + display: flex; + justify-content: center; + padding: 12px 0; + border-top: 1px solid var(--color-border); +` +const AnimatedListItem = styled(List.Item)` + transition: all 0.3s ease; + padding: 8px 24px; /* 增加左右内边距,减少上下内边距 */ + margin: 4px 0; /* 减少上下外边距 */ + border-bottom: 1px solid var(--color-border); + + &:last-child { + border-bottom: none; + } + + &.deleting { + opacity: 0; + transform: translateX(100%); + } + + /* 增加内容区域的内边距 */ + .ant-list-item-meta { + padding-left: 24px; + } + + /* 调整内容区域的标题和描述文字间距 */ + .ant-list-item-meta-title { + margin-bottom: 4px; /* 减少标题和描述之间的间距 */ + } + + .ant-list-item-meta-description { + padding-left: 4px; + } +` + +// 记忆项组件 +const MemoryItem = memo(({ memory, onDelete, t, index }: MemoryItemProps) => { + const [isDeleting, setIsDeleting] = useState(false); + + const handleDelete = async () => { + setIsDeleting(true); + // 添加小延迟,让动画有时间播放 + setTimeout(() => { + onDelete(memory.id); + }, 300); + }; + + return ( + + + + + ) +} + +export default HistoricalContextSettings diff --git a/src/renderer/src/pages/settings/MemorySettings/MemoryDeduplicationPanel.tsx b/src/renderer/src/pages/settings/MemorySettings/MemoryDeduplicationPanel.tsx index 7a88f06d55..f966ca8802 100644 --- a/src/renderer/src/pages/settings/MemorySettings/MemoryDeduplicationPanel.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/MemoryDeduplicationPanel.tsx @@ -144,19 +144,27 @@ const MemoryDeduplicationPanel: React.FC = ({ Modal.confirm({ title: t(`${translationPrefix}.confirmApply`), content: t(`${translationPrefix}.confirmApplyContent`), - onOk: () => { - if (applyResults) { - // 使用自定义的应用函数 - applyResults(deduplicationResult) - } else { - // 使用默认的应用函数 - applyDeduplicationResult(deduplicationResult, true, isShortMemory) + onOk: async () => { + try { + if (applyResults) { + // 使用自定义的应用函数 + applyResults(deduplicationResult) + } else { + // 使用默认的应用函数 + await applyDeduplicationResult(deduplicationResult, true, isShortMemory) + } + setDeduplicationResult(null) + Modal.success({ + title: t(`${translationPrefix}.applySuccess`), + content: t(`${translationPrefix}.applySuccessContent`) + }) + } catch (error) { + console.error('[Memory Deduplication Panel] Error applying deduplication result:', error) + Modal.error({ + title: t(`${translationPrefix}.applyError`) || '应用失败', + content: t(`${translationPrefix}.applyErrorContent`) || '应用去重结果时发生错误,请重试' + }) } - setDeduplicationResult(null) - Modal.success({ - title: t(`${translationPrefix}.applySuccess`), - content: t(`${translationPrefix}.applySuccessContent`) - }) } }) } diff --git a/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx b/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx index 3a85481ba3..c68f5bb137 100644 --- a/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx @@ -1,12 +1,14 @@ import { DeleteOutlined, EditOutlined, ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons' import { useAppDispatch, useAppSelector } from '@renderer/store' +import store from '@renderer/store' import { addMemoryList, deleteMemoryList, editMemoryList, MemoryList, setCurrentMemoryList, - toggleMemoryListActive + toggleMemoryListActive, + saveLongTermMemoryData } from '@renderer/store/memory' import { Button, Empty, Input, List, Modal, Switch, Tooltip, Typography } from 'antd' import React, { useState } from 'react' @@ -46,14 +48,14 @@ const MemoryListManager: React.FC = ({ onSelectList }) = } // 处理模态框确认 - const handleOk = () => { + const handleOk = async () => { if (!newListName.trim()) { return // 名称不能为空 } if (editingList) { // 编辑现有列表 - dispatch( + await dispatch( editMemoryList({ id: editingList.id, name: newListName, @@ -62,7 +64,7 @@ const MemoryListManager: React.FC = ({ onSelectList }) = ) } else { // 添加新列表 - dispatch( + await dispatch( addMemoryList({ name: newListName, description: newListDescription, @@ -71,6 +73,18 @@ const MemoryListManager: React.FC = ({ onSelectList }) = ) } + // 保存到长期记忆文件 + try { + const state = store.getState().memory + await dispatch(saveLongTermMemoryData({ + memoryLists: state.memoryLists, + currentListId: state.currentListId + })).unwrap() + console.log('[MemoryListManager] Memory lists saved to file after edit') + } catch (error) { + console.error('[MemoryListManager] Failed to save memory lists after edit:', error) + } + setIsModalVisible(false) setNewListName('') setNewListDescription('') @@ -94,23 +108,59 @@ const MemoryListManager: React.FC = ({ onSelectList }) = okText: t('common.delete'), okType: 'danger', cancelText: t('common.cancel'), - onOk() { + async onOk() { dispatch(deleteMemoryList(list.id)) + + // 保存到长期记忆文件 + try { + const state = store.getState().memory + await dispatch(saveLongTermMemoryData({ + memoryLists: state.memoryLists, + currentListId: state.currentListId + })).unwrap() + console.log('[MemoryListManager] Memory lists saved to file after delete') + } catch (error) { + console.error('[MemoryListManager] Failed to save memory lists after delete:', error) + } } }) } // 切换列表激活状态 - const handleToggleActive = (list: MemoryList, checked: boolean) => { + const handleToggleActive = async (list: MemoryList, checked: boolean) => { dispatch(toggleMemoryListActive({ id: list.id, isActive: checked })) + + // 保存到长期记忆文件 + try { + const state = store.getState().memory + await dispatch(saveLongTermMemoryData({ + memoryLists: state.memoryLists, + currentListId: state.currentListId + })).unwrap() + console.log('[MemoryListManager] Memory lists saved to file after toggle active') + } catch (error) { + console.error('[MemoryListManager] Failed to save memory lists after toggle active:', error) + } } // 选择列表 - const handleSelectList = (listId: string) => { + const handleSelectList = async (listId: string) => { dispatch(setCurrentMemoryList(listId)) if (onSelectList) { onSelectList(listId) } + + // 保存到长期记忆文件 + try { + const state = store.getState().memory + await dispatch(saveLongTermMemoryData({ + memoryLists: state.memoryLists, + currentListId: state.currentListId + })).unwrap() + console.log('[MemoryListManager] Memory lists saved to file after select list') + } catch (error) { + console.error('[MemoryListManager] Failed to save memory lists after select list:', error) + } } return ( diff --git a/src/renderer/src/pages/settings/MemorySettings/PriorityManagementSettings.tsx b/src/renderer/src/pages/settings/MemorySettings/PriorityManagementSettings.tsx index 1b09e355b5..d2d2807117 100644 --- a/src/renderer/src/pages/settings/MemorySettings/PriorityManagementSettings.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/PriorityManagementSettings.tsx @@ -1,6 +1,7 @@ import { InfoCircleOutlined } from '@ant-design/icons' import { useAppDispatch, useAppSelector } from '@renderer/store' import { + saveMemoryData, setDecayEnabled, setDecayRate, setFreshnessEnabled, @@ -25,78 +26,110 @@ const SliderContainer = styled.div` const PriorityManagementSettings: FC = () => { const { t } = useTranslation() const dispatch = useAppDispatch() - + // 获取相关状态 const priorityManagementEnabled = useAppSelector((state) => state.memory.priorityManagementEnabled) const decayEnabled = useAppSelector((state) => state.memory.decayEnabled) const freshnessEnabled = useAppSelector((state) => state.memory.freshnessEnabled) const decayRate = useAppSelector((state) => state.memory.decayRate) - + // 处理开关状态变化 - const handlePriorityManagementToggle = (checked: boolean) => { + const handlePriorityManagementToggle = async (checked: boolean) => { dispatch(setPriorityManagementEnabled(checked)) - } - - const handleDecayToggle = (checked: boolean) => { - dispatch(setDecayEnabled(checked)) - } - - const handleFreshnessToggle = (checked: boolean) => { - dispatch(setFreshnessEnabled(checked)) - } - - // 处理衰减率变化 - const handleDecayRateChange = (value: number | null) => { - if (value !== null) { - dispatch(setDecayRate(value)) + + // 保存设置 + try { + await dispatch(saveMemoryData({ priorityManagementEnabled: checked })).unwrap() + console.log('[PriorityManagementSettings] Priority management enabled setting saved:', checked) + } catch (error) { + console.error('[PriorityManagementSettings] Failed to save priority management enabled setting:', error) } } - + + const handleDecayToggle = async (checked: boolean) => { + dispatch(setDecayEnabled(checked)) + + // 保存设置 + try { + await dispatch(saveMemoryData({ decayEnabled: checked })).unwrap() + console.log('[PriorityManagementSettings] Decay enabled setting saved:', checked) + } catch (error) { + console.error('[PriorityManagementSettings] Failed to save decay enabled setting:', error) + } + } + + const handleFreshnessToggle = async (checked: boolean) => { + dispatch(setFreshnessEnabled(checked)) + + // 保存设置 + try { + await dispatch(saveMemoryData({ freshnessEnabled: checked })).unwrap() + console.log('[PriorityManagementSettings] Freshness enabled setting saved:', checked) + } catch (error) { + console.error('[PriorityManagementSettings] Failed to save freshness enabled setting:', error) + } + } + + // 处理衰减率变化 + const handleDecayRateChange = async (value: number | null) => { + if (value !== null) { + dispatch(setDecayRate(value)) + + // 保存设置 + try { + await dispatch(saveMemoryData({ decayRate: value })).unwrap() + console.log('[PriorityManagementSettings] Decay rate setting saved:', value) + } catch (error) { + console.error('[PriorityManagementSettings] Failed to save decay rate setting:', error) + } + } + } + // 手动更新记忆优先级 const handleUpdatePriorities = () => { dispatch(updateMemoryPriorities()) } - + return ( {t('settings.memory.priorityManagement.title') || '智能优先级与时效性管理'} - {t('settings.memory.priorityManagement.description') || + {t('settings.memory.priorityManagement.description') || '智能管理记忆的优先级、衰减和鲜度,确保最重要和最相关的记忆优先显示。'} - + {t('settings.memory.priorityManagement.enable') || '启用智能优先级管理'} - - + - + {t('settings.memory.priorityManagement.decay') || '记忆衰减'} - - - + {t('settings.memory.priorityManagement.decayRate') || '衰减速率'} - @@ -124,32 +157,32 @@ const PriorityManagementSettings: FC = () => { /> - + {t('settings.memory.priorityManagement.freshness') || '记忆鲜度'} - - - + {t('settings.memory.priorityManagement.updateNow') || '立即更新优先级'} - - + {/* 话题选择 */} {isActive && ( @@ -676,6 +770,12 @@ const MemorySettings: FC = () => { icon={}> {t('settings.memory.analyzeNow') || '立即分析'} + {isAnalyzing && (