From 54a8f31422f37f0f4ce642c89662a399816273e3 Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Sat, 12 Apr 2025 22:03:13 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E8=AE=B0=E5=BF=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/mcpServers/simpleremember.ts | 303 ++++++++++++++++++ .../providers/AiProvider/AnthropicProvider.ts | 4 +- .../providers/AiProvider/GeminiProvider.ts | 4 +- .../providers/AiProvider/OpenAIProvider.ts | 3 +- src/renderer/src/store/mcp.ts | 7 + src/renderer/src/utils/prompt.ts | 22 +- src/renderer/src/utils/remember-utils.ts | 67 ++++ 7 files changed, 404 insertions(+), 6 deletions(-) create mode 100644 src/main/mcpServers/simpleremember.ts create mode 100644 src/renderer/src/utils/remember-utils.ts diff --git a/src/main/mcpServers/simpleremember.ts b/src/main/mcpServers/simpleremember.ts new file mode 100644 index 0000000000..1da17fe2f4 --- /dev/null +++ b/src/main/mcpServers/simpleremember.ts @@ -0,0 +1,303 @@ +// src/main/mcpServers/simpleremember.ts +import { getConfigDir } from '@main/utils/file' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js' +import { promises as fs } from 'fs' +import path from 'path' +import { Mutex } from 'async-mutex' + +// 定义记忆文件路径 +const defaultMemoryPath = path.join(getConfigDir(), 'simpleremember.json') + +// 记忆项接口 +interface Memory { + content: string; + createdAt: string; +} + +// 记忆存储结构 +interface MemoryStorage { + memories: Memory[]; +} + +class SimpleRememberManager { + private memoryPath: string; + private memories: Memory[] = []; + private fileMutex: Mutex = new Mutex(); + + constructor(memoryPath: string) { + this.memoryPath = memoryPath; + } + + // 静态工厂方法用于初始化 + public static async create(memoryPath: string): Promise { + const manager = new SimpleRememberManager(memoryPath); + await manager._ensureMemoryPathExists(); + await manager._loadMemoriesFromDisk(); + return manager; + } + + // 确保记忆文件存在 + private async _ensureMemoryPathExists(): Promise { + try { + const directory = path.dirname(this.memoryPath); + await fs.mkdir(directory, { recursive: true }); + try { + await fs.access(this.memoryPath); + } catch (error) { + // 文件不存在,创建一个空文件 + await fs.writeFile(this.memoryPath, JSON.stringify({ memories: [] }, null, 2)); + } + } catch (error) { + console.error('Failed to ensure memory path exists:', error); + throw new McpError(ErrorCode.InternalError, `Failed to ensure memory path: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 从磁盘加载记忆 + private async _loadMemoriesFromDisk(): Promise { + try { + const data = await fs.readFile(this.memoryPath, 'utf-8'); + // 处理空文件情况 + if (data.trim() === '') { + this.memories = []; + await this._persistMemories(); + return; + } + const storage: MemoryStorage = JSON.parse(data); + this.memories = storage.memories || []; + } catch (error) { + if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') { + this.memories = []; + await this._persistMemories(); + } else if (error instanceof SyntaxError) { + console.error('Failed to parse simpleremember.json, initializing with empty memories:', error); + this.memories = []; + await this._persistMemories(); + } else { + console.error('Unexpected error loading memories:', error); + throw new McpError(ErrorCode.InternalError, `Failed to load memories: ${error instanceof Error ? error.message : String(error)}`); + } + } + } + + // 将记忆持久化到磁盘 + private async _persistMemories(): Promise { + const release = await this.fileMutex.acquire(); + try { + const storage: MemoryStorage = { + memories: this.memories + }; + await fs.writeFile(this.memoryPath, JSON.stringify(storage, null, 2)); + } catch (error) { + console.error('Failed to save memories:', error); + throw new McpError(ErrorCode.InternalError, `Failed to save memories: ${error instanceof Error ? error.message : String(error)}`); + } finally { + release(); + } + } + + // 添加新记忆 + async remember(memory: string): Promise { + const newMemory: Memory = { + content: memory, + createdAt: new Date().toISOString() + }; + this.memories.push(newMemory); + await this._persistMemories(); + return newMemory; + } + + // 获取所有记忆 + async getAllMemories(): Promise { + return [...this.memories]; + } + + // 获取记忆 - 这个方法会被get_memories工具调用 + async get_memories(): Promise { + return this.getAllMemories(); + } +} + +// 定义工具 - 按照MCP规范定义工具 +const REMEMBER_TOOL = { + name: 'remember', + description: '用于记忆长期有用信息的工具。这个工具会自动应用记忆,无需显式调用。只用于存储长期有用的信息,不适合临时信息。', + inputSchema: { + type: 'object', + properties: { + memory: { + type: 'string', + description: '要记住的简洁(1句话)记忆内容' + } + }, + required: ['memory'] + } +}; + +const GET_MEMORIES_TOOL = { + name: 'get_memories', + description: '获取所有已存储的记忆', + inputSchema: { + type: 'object', + properties: {} + } +}; + +// 添加日志以便调试 +console.log("[SimpleRemember] Defined tools:", { REMEMBER_TOOL, GET_MEMORIES_TOOL }); + +class SimpleRememberServer { + public server: Server; + private simpleRememberManager: SimpleRememberManager | null = null; + private initializationPromise: Promise; + + constructor(envPath: string = '') { + const memoryPath = envPath + ? path.isAbsolute(envPath) + ? envPath + : path.resolve(envPath) + : defaultMemoryPath; + + console.log("[SimpleRemember] Creating server with memory path:", memoryPath); + + // 初始化服务器 + this.server = new Server( + { + name: 'simple-remember-server', + version: '1.0.0' + }, + { + capabilities: { + tools: { + // 按照MCP规范声明工具能力 + listChanged: true + }, + // 添加空的prompts能力,表示支持提示词功能但没有实际的提示词 + prompts: {} + } + } + ); + + console.log("[SimpleRemember] Server initialized with tools capability"); + + // 手动添加工具到服务器的工具列表中 + console.log("[SimpleRemember] Adding tools to server"); + + // 先设置请求处理程序,再初始化管理器 + this.setupRequestHandlers(); + this.initializationPromise = this._initializeManager(memoryPath); + + console.log("[SimpleRemember] Server initialization complete"); + // 打印工具信息以确认它们已注册 + console.log("[SimpleRemember] Tools registered:", [REMEMBER_TOOL.name, GET_MEMORIES_TOOL.name]); + } + + private async _initializeManager(memoryPath: string): Promise { + try { + this.simpleRememberManager = await SimpleRememberManager.create(memoryPath); + console.log("SimpleRememberManager initialized successfully."); + } catch (error) { + console.error("Failed to initialize SimpleRememberManager:", error); + this.simpleRememberManager = null; + } + } + + private async _getManager(): Promise { + if (!this.simpleRememberManager) { + await this.initializationPromise; + if (!this.simpleRememberManager) { + throw new McpError(ErrorCode.InternalError, "SimpleRememberManager is not initialized"); + } + } + return this.simpleRememberManager; + } + + setupRequestHandlers() { + // 添加对prompts/list请求的处理 + this.server.setRequestHandler(ListPromptsRequestSchema, async (request) => { + console.log("[SimpleRemember] Listing prompts request received", request); + + // 返回空的提示词列表 + return { + prompts: [] + }; + }); + + this.server.setRequestHandler(ListToolsRequestSchema, async (request) => { + // 直接返回工具列表,不需要等待管理器初始化 + console.log("[SimpleRemember] Listing tools request received", request); + + // 打印工具定义以确保它们存在 + console.log("[SimpleRemember] REMEMBER_TOOL:", JSON.stringify(REMEMBER_TOOL)); + console.log("[SimpleRemember] GET_MEMORIES_TOOL:", JSON.stringify(GET_MEMORIES_TOOL)); + + const toolsList = [REMEMBER_TOOL, GET_MEMORIES_TOOL]; + console.log("[SimpleRemember] Returning tools:", JSON.stringify(toolsList)); + + // 按照MCP规范返回工具列表 + return { + tools: toolsList, + // 如果有分页,可以添加nextCursor + // nextCursor: "next-page-cursor" + }; + }); + + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + console.log(`[SimpleRemember] Received tool call: ${name}`, args); + + try { + const manager = await this._getManager(); + + if (name === 'remember') { + if (!args || typeof args.memory !== 'string') { + console.error(`[SimpleRemember] Invalid arguments for ${name}:`, args); + throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'memory' string is required.`); + } + console.log(`[SimpleRemember] Remembering: "${args.memory}"`); + const result = await manager.remember(args.memory); + console.log(`[SimpleRemember] Memory saved successfully:`, result); + // 按照MCP规范返回工具调用结果 + return { + content: [{ + type: 'text', + text: `记忆已保存: "${args.memory}"` + }], + isError: false + }; + } + + if (name === 'get_memories') { + console.log(`[SimpleRemember] Getting all memories`); + const memories = await manager.get_memories(); + console.log(`[SimpleRemember] Retrieved ${memories.length} memories`); + // 按照MCP规范返回工具调用结果 + return { + content: [{ + type: 'text', + text: JSON.stringify(memories, null, 2) + }], + isError: false + }; + } + + console.error(`[SimpleRemember] Unknown tool: ${name}`); + throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); + } catch (error) { + console.error(`[SimpleRemember] Error handling tool call ${name}:`, error); + // 按照MCP规范返回工具调用错误 + return { + content: [{ + type: 'text', + text: error instanceof Error ? error.message : String(error) + }], + isError: true + }; + } + }); + } +} + +export default SimpleRememberServer; diff --git a/src/renderer/src/providers/AiProvider/AnthropicProvider.ts b/src/renderer/src/providers/AiProvider/AnthropicProvider.ts index d9f72f7175..e39ecdd3a1 100644 --- a/src/renderer/src/providers/AiProvider/AnthropicProvider.ts +++ b/src/renderer/src/providers/AiProvider/AnthropicProvider.ts @@ -14,6 +14,8 @@ import { Assistant, FileTypes, MCPToolResponse, Message, Model, Provider, Sugges import { removeSpecialCharactersForTopicName } from '@renderer/utils' import { parseAndCallTools } from '@renderer/utils/mcp-tools' import { buildSystemPrompt } from '@renderer/utils/prompt' +import store from '@renderer/store' +import { getActiveServers } from '@renderer/store/mcp' import { first, flatten, sum, takeRight } from 'lodash' import OpenAI from 'openai' @@ -177,7 +179,7 @@ export default class AnthropicProvider extends BaseProvider { let systemPrompt = assistant.prompt if (mcpTools && mcpTools.length > 0) { - systemPrompt = buildSystemPrompt(systemPrompt, mcpTools) + systemPrompt = await buildSystemPrompt(systemPrompt, mcpTools, getActiveServers(store.getState())) } const body: MessageCreateParamsNonStreaming = { diff --git a/src/renderer/src/providers/AiProvider/GeminiProvider.ts b/src/renderer/src/providers/AiProvider/GeminiProvider.ts index be40686f9c..8cd74dc8d2 100644 --- a/src/renderer/src/providers/AiProvider/GeminiProvider.ts +++ b/src/renderer/src/providers/AiProvider/GeminiProvider.ts @@ -24,6 +24,8 @@ import { getStoreSetting } from '@renderer/hooks/useSettings' import i18n from '@renderer/i18n' import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService' import { EVENT_NAMES } from '@renderer/services/EventService' +import store from '@renderer/store' +import { getActiveServers } from '@renderer/store/mcp' import { filterContextMessages, filterEmptyMessages, @@ -228,7 +230,7 @@ export default class GeminiProvider extends BaseProvider { let systemInstruction = assistant.prompt if (mcpTools && mcpTools.length > 0) { - systemInstruction = buildSystemPrompt(assistant.prompt || '', mcpTools) + systemInstruction = await buildSystemPrompt(assistant.prompt || '', mcpTools, getActiveServers(store.getState())) } // const tools = mcpToolsToGeminiTools(mcpTools) diff --git a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts index 964694703e..359be4041a 100644 --- a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts +++ b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts @@ -20,6 +20,7 @@ import { filterUserRoleStartMessages } from '@renderer/services/MessagesService' import store from '@renderer/store' +import { getActiveServers } from '@renderer/store/mcp' import { Assistant, FileTypes, @@ -318,7 +319,7 @@ export default class OpenAIProvider extends BaseProvider { } } if (mcpTools && mcpTools.length > 0) { - systemMessage.content = buildSystemPrompt(systemMessage.content || '', mcpTools) + systemMessage.content = await buildSystemPrompt(systemMessage.content || '', mcpTools, getActiveServers(store.getState())) } const userMessages: ChatCompletionMessageParam[] = [] diff --git a/src/renderer/src/store/mcp.ts b/src/renderer/src/store/mcp.ts index 1d075e70e2..71509482fa 100644 --- a/src/renderer/src/store/mcp.ts +++ b/src/renderer/src/store/mcp.ts @@ -97,6 +97,13 @@ export const builtinMCPServers: MCPServer[] = [ type: 'inMemory', description: '实现文件系统操作的模型上下文协议(MCP)的 Node.js 服务器', isActive: false + }, + { + id: nanoid(), + name: '@cherry/simpleremember', + type: 'inMemory', + description: '自动记忆工具,功能跟上面的记忆工具差不多。这个记忆会自动应用到对话中,无需显式调用。适合记住用户偏好、项目背景等长期有用信息.可以跨对话。', + isActive: true } ] diff --git a/src/renderer/src/utils/prompt.ts b/src/renderer/src/utils/prompt.ts index 698fdb1186..94649ff873 100644 --- a/src/renderer/src/utils/prompt.ts +++ b/src/renderer/src/utils/prompt.ts @@ -147,12 +147,28 @@ ${availableTools} ` } -export const buildSystemPrompt = (userSystemPrompt: string, tools: MCPTool[]): string => { +import { MCPServer } from '@renderer/types' +import { getRememberedMemories } from './remember-utils' + +export const buildSystemPrompt = async (userSystemPrompt: string, tools: MCPTool[], mcpServers: MCPServer[] = []): Promise => { + // 获取记忆 + let memoriesPrompt = ''; + try { + memoriesPrompt = await getRememberedMemories(mcpServers); + } catch (error) { + console.error('Error getting memories:', error); + } + + // 添加记忆工具的使用说明 + const rememberInstructions = '\n\n您可以使用remember工具记住用户的长期偏好和重要信息。当用户说"请记住..."或"记住..."时,使用remember工具存储这些信息。记忆会自动应用到所有对话中,无需显式调用。'; + + const enhancedPrompt = userSystemPrompt + rememberInstructions + memoriesPrompt; + if (tools && tools.length > 0) { - return SYSTEM_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', userSystemPrompt) + return SYSTEM_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', enhancedPrompt) .replace('{{ TOOL_USE_EXAMPLES }}', ToolUseExamples) .replace('{{ AVAILABLE_TOOLS }}', AvailableTools(tools)) } - return userSystemPrompt + return enhancedPrompt } diff --git a/src/renderer/src/utils/remember-utils.ts b/src/renderer/src/utils/remember-utils.ts new file mode 100644 index 0000000000..0ce61ae1f5 --- /dev/null +++ b/src/renderer/src/utils/remember-utils.ts @@ -0,0 +1,67 @@ +// src/renderer/src/utils/remember-utils.ts +import { MCPServer } from '@renderer/types' + +export async function getRememberedMemories(mcpServers: MCPServer[]): Promise { + try { + // 查找simpleremember服务器 + const rememberServer = mcpServers.find(server => server.name === '@cherry/simpleremember' && server.isActive); + + if (!rememberServer) { + console.log('[SimpleRemember] Server not found or not active'); + return ''; + } + + console.log('[SimpleRemember] Found server:', rememberServer.name, 'isActive:', rememberServer.isActive); + + // 调用get_memories工具 + try { + console.log('[SimpleRemember] Calling get_memories tool...'); + const response = await window.api.mcp.callTool({ + server: rememberServer, + name: 'get_memories', + args: {} + }); + + console.log('[SimpleRemember] get_memories response:', response); + + if (response.isError) { + console.error('[SimpleRemember] Error getting memories:', response); + return ''; + } + + // 解析记忆 + // 根据MCP规范,工具返回的是content数组,而不是data + let memories = []; + if (response.content && response.content.length > 0 && response.content[0].text) { + try { + memories = JSON.parse(response.content[0].text); + } catch (parseError) { + console.error('[SimpleRemember] Failed to parse memories JSON:', parseError); + return ''; + } + } else if (response.data) { + // 兼容旧版本的返回格式 + memories = response.data; + } + + console.log('[SimpleRemember] Parsed memories:', memories); + + if (!Array.isArray(memories) || memories.length === 0) { + console.log('[SimpleRemember] No memories found or invalid format'); + return ''; + } + + // 构建记忆提示词 + const memoryPrompt = memories.map(memory => `- ${memory.content}`).join('\n'); + console.log('[SimpleRemember] Generated memory prompt:', memoryPrompt); + + return `\n\n用户的记忆:\n${memoryPrompt}`; + } catch (toolError) { + console.error('[SimpleRemember] Error calling get_memories tool:', toolError); + return ''; + } + } catch (error) { + console.error('[SimpleRemember] Error in getRememberedMemories:', error); + return ''; + } +} From 8aea052bd60c11b16548bed3a899ac0643f15586 Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Sun, 13 Apr 2025 03:51:11 +0800 Subject: [PATCH 02/11] =?UTF-8?q?=E8=AE=B0=E5=BF=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/mcpServers/factory.ts | 5 + src/main/mcpServers/memory.ts | 602 ++++++++++-------- src/main/mcpServers/simpleremember.ts | 244 +++---- src/renderer/src/App.tsx | 35 +- .../src/components/MemoryProvider.tsx | 72 +++ src/renderer/src/i18n/index.ts | 10 +- src/renderer/src/i18n/locales/en-us.json | 38 ++ src/renderer/src/i18n/locales/zh-cn.json | 53 ++ .../src/pages/home/Messages/MessageError.tsx | 33 +- .../home/Messages/MessageErrorBoundary.tsx | 43 +- .../pages/home/Messages/MessageMenubar.tsx | 45 +- .../settings/MemorySettings/CenterNode.tsx | 30 + .../MemorySettings/MemoryListManager.tsx | 261 ++++++++ .../settings/MemorySettings/MemoryMindMap.tsx | 220 +++++++ .../settings/MemorySettings/MemoryNode.tsx | 77 +++ .../MemorySettings/ShortMemoryManager.tsx | 115 ++++ .../pages/settings/MemorySettings/index.tsx | 535 ++++++++++++++++ .../src/pages/settings/SettingsPage.tsx | 9 + src/renderer/src/pages/settings/index.tsx | 15 +- .../providers/AiProvider/AnthropicProvider.ts | 38 +- .../src/providers/AiProvider/BaseProvider.ts | 2 +- .../providers/AiProvider/GeminiProvider.ts | 47 +- .../providers/AiProvider/OpenAIProvider.ts | 81 ++- .../src/providers/AiProvider/index.ts | 4 +- src/renderer/src/services/ApiService.ts | 27 +- src/renderer/src/services/MemoryService.ts | 382 +++++++++++ src/renderer/src/store/index.ts | 2 + src/renderer/src/store/mcp.ts | 3 +- src/renderer/src/store/memory.ts | 363 +++++++++++ src/renderer/src/store/messages.ts | 24 +- src/renderer/src/utils/error.ts | 48 +- src/renderer/src/utils/prompt.ts | 65 +- src/renderer/src/utils/remember-utils.ts | 49 +- 33 files changed, 3067 insertions(+), 510 deletions(-) create mode 100644 src/renderer/src/components/MemoryProvider.tsx create mode 100644 src/renderer/src/pages/settings/MemorySettings/CenterNode.tsx create mode 100644 src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx create mode 100644 src/renderer/src/pages/settings/MemorySettings/MemoryMindMap.tsx create mode 100644 src/renderer/src/pages/settings/MemorySettings/MemoryNode.tsx create mode 100644 src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx create mode 100644 src/renderer/src/pages/settings/MemorySettings/index.tsx create mode 100644 src/renderer/src/services/MemoryService.ts create mode 100644 src/renderer/src/store/memory.ts diff --git a/src/main/mcpServers/factory.ts b/src/main/mcpServers/factory.ts index 479ec23c1f..d35770d48d 100644 --- a/src/main/mcpServers/factory.ts +++ b/src/main/mcpServers/factory.ts @@ -6,6 +6,7 @@ import FetchServer from './fetch' import FileSystemServer from './filesystem' import MemoryServer from './memory' import ThinkingServer from './sequentialthinking' +import SimpleRememberServer from './simpleremember' export function createInMemoryMCPServer(name: string, args: string[] = [], envs: Record = {}): Server { Logger.info(`[MCP] Creating in-memory MCP server: ${name} with args: ${args} and envs: ${JSON.stringify(envs)}`) @@ -26,6 +27,10 @@ export function createInMemoryMCPServer(name: string, args: string[] = [], envs: case '@cherry/filesystem': { return new FileSystemServer(args).server } + case '@cherry/simpleremember': { + const envPath = envs.SIMPLEREMEMBER_FILE_PATH + return new SimpleRememberServer(envPath).server + } default: throw new Error(`Unknown in-memory MCP server: ${name}`) } diff --git a/src/main/mcpServers/memory.ts b/src/main/mcpServers/memory.ts index 9b4d2d4c8d..211b5f2238 100644 --- a/src/main/mcpServers/memory.ts +++ b/src/main/mcpServers/memory.ts @@ -1,9 +1,9 @@ import { getConfigDir } from '@main/utils/file' import { Server } from '@modelcontextprotocol/sdk/server/index.js' -import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js' +import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js' +import { Mutex } from 'async-mutex' // 引入 Mutex import { promises as fs } from 'fs' import path from 'path' -import { Mutex } from 'async-mutex' // 引入 Mutex // Define memory file path const defaultMemoryPath = path.join(getConfigDir(), 'memory.json') @@ -62,7 +62,10 @@ class KnowledgeGraphManager { } catch (error) { console.error('Failed to ensure memory path exists:', error) // Propagate the error or handle it more gracefully depending on requirements - throw new McpError(ErrorCode.InternalError, `Failed to ensure memory path: ${error instanceof Error ? error.message : String(error)}`) + throw new McpError( + ErrorCode.InternalError, + `Failed to ensure memory path: ${error instanceof Error ? error.message : String(error)}` + ) } } @@ -81,8 +84,8 @@ class KnowledgeGraphManager { const graph: KnowledgeGraph = JSON.parse(data) this.entities.clear() this.relations.clear() - graph.entities.forEach(entity => this.entities.set(entity.name, entity)) - graph.relations.forEach(relation => this.relations.add(this._serializeRelation(relation))) + graph.entities.forEach((entity) => this.entities.set(entity.name, entity)) + graph.relations.forEach((relation) => this.relations.add(this._serializeRelation(relation))) } catch (error) { if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') { // File doesn't exist (should have been created by _ensureMemoryPathExists, but handle defensively) @@ -90,14 +93,17 @@ class KnowledgeGraphManager { this.relations = new Set() await this._persistGraph() // Create the file with empty structure } else if (error instanceof SyntaxError) { - console.error('Failed to parse memory.json, initializing with empty graph:', error) - // If JSON is invalid, start fresh and overwrite the corrupted file - this.entities = new Map() - this.relations = new Set() - await this._persistGraph() + console.error('Failed to parse memory.json, initializing with empty graph:', error) + // If JSON is invalid, start fresh and overwrite the corrupted file + this.entities = new Map() + this.relations = new Set() + await this._persistGraph() } else { console.error('Failed to load knowledge graph from disk:', error) - throw new McpError(ErrorCode.InternalError, `Failed to load graph: ${error instanceof Error ? error.message : String(error)}`) + throw new McpError( + ErrorCode.InternalError, + `Failed to load graph: ${error instanceof Error ? error.message : String(error)}` + ) } } } @@ -108,13 +114,16 @@ class KnowledgeGraphManager { try { const graphData: KnowledgeGraph = { entities: Array.from(this.entities.values()), - relations: Array.from(this.relations).map(rStr => this._deserializeRelation(rStr)) + relations: Array.from(this.relations).map((rStr) => this._deserializeRelation(rStr)) } await fs.writeFile(this.memoryPath, JSON.stringify(graphData, null, 2)) } catch (error) { console.error('Failed to save knowledge graph:', error) // Decide how to handle write errors - potentially retry or notify - throw new McpError(ErrorCode.InternalError, `Failed to save graph: ${error instanceof Error ? error.message : String(error)}`) + throw new McpError( + ErrorCode.InternalError, + `Failed to save graph: ${error instanceof Error ? error.message : String(error)}` + ) } finally { release() } @@ -133,10 +142,10 @@ class KnowledgeGraphManager { async createEntities(entities: Entity[]): Promise { const newEntities: Entity[] = [] - entities.forEach(entity => { + entities.forEach((entity) => { if (!this.entities.has(entity.name)) { // Ensure observations is always an array - const newEntity = { ...entity, observations: Array.isArray(entity.observations) ? entity.observations : [] }; + const newEntity = { ...entity, observations: Array.isArray(entity.observations) ? entity.observations : [] } this.entities.set(entity.name, newEntity) newEntities.push(newEntity) } @@ -149,11 +158,11 @@ class KnowledgeGraphManager { async createRelations(relations: Relation[]): Promise { const newRelations: Relation[] = [] - relations.forEach(relation => { + relations.forEach((relation) => { // Ensure related entities exist before creating a relation if (!this.entities.has(relation.from) || !this.entities.has(relation.to)) { - console.warn(`Skipping relation creation: Entity not found for relation ${relation.from} -> ${relation.to}`) - return; // Skip this relation + console.warn(`Skipping relation creation: Entity not found for relation ${relation.from} -> ${relation.to}`) + return // Skip this relation } const relationStr = this._serializeRelation(relation) if (!this.relations.has(relationStr)) { @@ -172,20 +181,20 @@ class KnowledgeGraphManager { ): Promise<{ entityName: string; addedObservations: string[] }[]> { const results: { entityName: string; addedObservations: string[] }[] = [] let changed = false - observations.forEach(o => { + observations.forEach((o) => { const entity = this.entities.get(o.entityName) if (!entity) { // Option 1: Throw error - throw new McpError(ErrorCode.InvalidParams, `Entity with name ${o.entityName} not found`) + throw new McpError(ErrorCode.InvalidParams, `Entity with name ${o.entityName} not found`) // Option 2: Skip and warn // console.warn(`Entity with name ${o.entityName} not found when adding observations. Skipping.`); // return; } // Ensure observations array exists if (!Array.isArray(entity.observations)) { - entity.observations = []; + entity.observations = [] } - const newObservations = o.contents.filter(content => !entity.observations.includes(content)) + const newObservations = o.contents.filter((content) => !entity.observations.includes(content)) if (newObservations.length > 0) { entity.observations.push(...newObservations) results.push({ entityName: o.entityName, addedObservations: newObservations }) @@ -206,7 +215,7 @@ class KnowledgeGraphManager { const namesToDelete = new Set(entityNames) // Delete entities - namesToDelete.forEach(name => { + namesToDelete.forEach((name) => { if (this.entities.delete(name)) { changed = true } @@ -214,14 +223,14 @@ class KnowledgeGraphManager { // Delete relations involving deleted entities const relationsToDelete = new Set() - this.relations.forEach(relStr => { + this.relations.forEach((relStr) => { const rel = this._deserializeRelation(relStr) if (namesToDelete.has(rel.from) || namesToDelete.has(rel.to)) { relationsToDelete.add(relStr) } }) - relationsToDelete.forEach(relStr => { + relationsToDelete.forEach((relStr) => { if (this.relations.delete(relStr)) { changed = true } @@ -234,12 +243,12 @@ class KnowledgeGraphManager { async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise { let changed = false - deletions.forEach(d => { + deletions.forEach((d) => { const entity = this.entities.get(d.entityName) if (entity && Array.isArray(entity.observations)) { const initialLength = entity.observations.length const observationsToDelete = new Set(d.observations) - entity.observations = entity.observations.filter(o => !observationsToDelete.has(o)) + entity.observations = entity.observations.filter((o) => !observationsToDelete.has(o)) if (entity.observations.length !== initialLength) { changed = true } @@ -252,7 +261,7 @@ class KnowledgeGraphManager { async deleteRelations(relations: Relation[]): Promise { let changed = false - relations.forEach(rel => { + relations.forEach((rel) => { const relStr = this._serializeRelation(rel) if (this.relations.delete(relStr)) { changed = true @@ -266,27 +275,29 @@ class KnowledgeGraphManager { // Read the current state from memory async readGraph(): Promise { // Return a deep copy to prevent external modification of the internal state - return JSON.parse(JSON.stringify({ + return JSON.parse( + JSON.stringify({ entities: Array.from(this.entities.values()), - relations: Array.from(this.relations).map(rStr => this._deserializeRelation(rStr)) - })); + relations: Array.from(this.relations).map((rStr) => this._deserializeRelation(rStr)) + }) + ) } // Search operates on the in-memory graph async searchNodes(query: string): Promise { const lowerCaseQuery = query.toLowerCase() const filteredEntities = Array.from(this.entities.values()).filter( - e => + (e) => e.name.toLowerCase().includes(lowerCaseQuery) || e.entityType.toLowerCase().includes(lowerCaseQuery) || - (Array.isArray(e.observations) && e.observations.some(o => o.toLowerCase().includes(lowerCaseQuery))) + (Array.isArray(e.observations) && e.observations.some((o) => o.toLowerCase().includes(lowerCaseQuery))) ) - const filteredEntityNames = new Set(filteredEntities.map(e => e.name)) + const filteredEntityNames = new Set(filteredEntities.map((e) => e.name)) const filteredRelations = Array.from(this.relations) - .map(rStr => this._deserializeRelation(rStr)) - .filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)) + .map((rStr) => this._deserializeRelation(rStr)) + .filter((r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)) return { entities: filteredEntities, @@ -296,26 +307,26 @@ class KnowledgeGraphManager { // Open operates on the in-memory graph async openNodes(names: string[]): Promise { - const nameSet = new Set(names); - const filteredEntities = Array.from(this.entities.values()).filter(e => nameSet.has(e.name)); - const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); + const nameSet = new Set(names) + const filteredEntities = Array.from(this.entities.values()).filter((e) => nameSet.has(e.name)) + const filteredEntityNames = new Set(filteredEntities.map((e) => e.name)) - const filteredRelations = Array.from(this.relations) - .map(rStr => this._deserializeRelation(rStr)) - .filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)); + const filteredRelations = Array.from(this.relations) + .map((rStr) => this._deserializeRelation(rStr)) + .filter((r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)) - return { - entities: filteredEntities, - relations: filteredRelations - }; + return { + entities: filteredEntities, + relations: filteredRelations + } } } class MemoryServer { public server: Server // Hold the manager instance, initialized asynchronously - private knowledgeGraphManager: KnowledgeGraphManager | null = null; - private initializationPromise: Promise; // To track initialization + private knowledgeGraphManager: KnowledgeGraphManager | null = null + private initializationPromise: Promise // To track initialization constructor(envPath: string = '') { const memoryPath = envPath @@ -336,33 +347,32 @@ class MemoryServer { } ) // Start initialization, but don't block constructor - this.initializationPromise = this._initializeManager(memoryPath); - this.setupRequestHandlers(); // Setup handlers immediately + this.initializationPromise = this._initializeManager(memoryPath) + this.setupRequestHandlers() // Setup handlers immediately } // Private async method to handle manager initialization private async _initializeManager(memoryPath: string): Promise { - try { - this.knowledgeGraphManager = await KnowledgeGraphManager.create(memoryPath); - console.log("KnowledgeGraphManager initialized successfully."); - } catch (error) { - console.error("Failed to initialize KnowledgeGraphManager:", error); - // Server might be unusable, consider how to handle this state - // Maybe set a flag and return errors for all tool calls? - this.knowledgeGraphManager = null; // Ensure it's null if init fails - } + try { + this.knowledgeGraphManager = await KnowledgeGraphManager.create(memoryPath) + console.log('KnowledgeGraphManager initialized successfully.') + } catch (error) { + console.error('Failed to initialize KnowledgeGraphManager:', error) + // Server might be unusable, consider how to handle this state + // Maybe set a flag and return errors for all tool calls? + this.knowledgeGraphManager = null // Ensure it's null if init fails + } } // Ensures the manager is initialized before handling tool calls private async _getManager(): Promise { - await this.initializationPromise; // Wait for initialization to complete - if (!this.knowledgeGraphManager) { - throw new McpError(ErrorCode.InternalError, "Memory server failed to initialize. Cannot process requests."); - } - return this.knowledgeGraphManager; + await this.initializationPromise // Wait for initialization to complete + if (!this.knowledgeGraphManager) { + throw new McpError(ErrorCode.InternalError, 'Memory server failed to initialize. Cannot process requests.') + } + return this.knowledgeGraphManager } - // Setup handlers (can be called from constructor) setupRequestHandlers() { // ListTools remains largely the same, descriptions might be updated if needed @@ -371,196 +381,197 @@ class MemoryServer { // Although ListTools itself doesn't *call* the manager, it implies the // manager is ready to handle calls for those tools. try { - await this._getManager(); // Wait for initialization before confirming tools are available + await this._getManager() // Wait for initialization before confirming tools are available } catch (error) { - // If manager failed to init, maybe return an empty tool list or throw? - console.error("Cannot list tools, manager initialization failed:", error); - return { tools: [] }; // Return empty list if server is not ready + // If manager failed to init, maybe return an empty tool list or throw? + console.error('Cannot list tools, manager initialization failed:', error) + return { tools: [] } // Return empty list if server is not ready } return { tools: [ - { - name: 'create_entities', - description: 'Create multiple new entities in the knowledge graph. Skips existing entities.', - inputSchema: { - type: 'object', - properties: { - entities: { - type: 'array', - items: { - type: 'object', - properties: { - name: { type: 'string', description: 'The name of the entity' }, - entityType: { type: 'string', description: 'The type of the entity' }, - observations: { - type: 'array', - items: { type: 'string' }, - description: 'An array of observation contents associated with the entity', - default: [] // Add default empty array - } - }, - required: ['name', 'entityType'] // Observations are optional now on creation - } - } - }, - required: ['entities'] - } - }, - { - name: 'create_relations', - description: 'Create multiple new relations between EXISTING entities. Skips existing relations or relations with non-existent entities.', - inputSchema: { - type: 'object', - properties: { - relations: { - type: 'array', - items: { - type: 'object', - properties: { - from: { type: 'string', description: 'The name of the entity where the relation starts' }, - to: { type: 'string', description: 'The name of the entity where the relation ends' }, - relationType: { type: 'string', description: 'The type of the relation' } - }, - required: ['from', 'to', 'relationType'] - } - } - }, - required: ['relations'] - } - }, - { - name: 'add_observations', - description: 'Add new observations to existing entities. Skips duplicate observations.', - inputSchema: { - type: 'object', - properties: { - observations: { - type: 'array', - items: { - type: 'object', - properties: { - entityName: { type: 'string', description: 'The name of the entity to add the observations to' }, - contents: { - type: 'array', - items: { type: 'string' }, - description: 'An array of observation contents to add' - } - }, - required: ['entityName', 'contents'] - } - } - }, - required: ['observations'] - } - }, - { - name: 'delete_entities', - description: 'Delete multiple entities and their associated relations.', - inputSchema: { - type: 'object', - properties: { - entityNames: { - type: 'array', - items: { type: 'string' }, - description: 'An array of entity names to delete' - } - }, - required: ['entityNames'] - } - }, - { - name: 'delete_observations', - description: 'Delete specific observations from entities.', - inputSchema: { - type: 'object', - properties: { - deletions: { - type: 'array', - items: { - type: 'object', - properties: { - entityName: { type: 'string', description: 'The name of the entity containing the observations' }, - observations: { - type: 'array', - items: { type: 'string' }, - description: 'An array of observations to delete' - } - }, - required: ['entityName', 'observations'] - } - } - }, - required: ['deletions'] - } - }, - { - name: 'delete_relations', - description: 'Delete multiple specific relations.', - inputSchema: { - type: 'object', - properties: { - relations: { - type: 'array', - items: { - type: 'object', - properties: { - from: { type: 'string', description: 'The name of the entity where the relation starts' }, - to: { type: 'string', description: 'The name of the entity where the relation ends' }, - relationType: { type: 'string', description: 'The type of the relation' } - }, - required: ['from', 'to', 'relationType'] + { + name: 'create_entities', + description: 'Create multiple new entities in the knowledge graph. Skips existing entities.', + inputSchema: { + type: 'object', + properties: { + entities: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'The name of the entity' }, + entityType: { type: 'string', description: 'The type of the entity' }, + observations: { + type: 'array', + items: { type: 'string' }, + description: 'An array of observation contents associated with the entity', + default: [] // Add default empty array + } }, - description: 'An array of relations to delete' + required: ['name', 'entityType'] // Observations are optional now on creation } - }, - required: ['relations'] - } - }, - { - name: 'read_graph', - description: 'Read the entire knowledge graph from memory.', - inputSchema: { - type: 'object', - properties: {} - } - }, - { - name: 'search_nodes', - description: 'Search nodes (entities and relations) in memory based on a query.', - inputSchema: { - type: 'object', - properties: { - query: { - type: 'string', - description: 'The search query to match against entity names, types, and observation content' + } + }, + required: ['entities'] + } + }, + { + name: 'create_relations', + description: + 'Create multiple new relations between EXISTING entities. Skips existing relations or relations with non-existent entities.', + inputSchema: { + type: 'object', + properties: { + relations: { + type: 'array', + items: { + type: 'object', + properties: { + from: { type: 'string', description: 'The name of the entity where the relation starts' }, + to: { type: 'string', description: 'The name of the entity where the relation ends' }, + relationType: { type: 'string', description: 'The type of the relation' } + }, + required: ['from', 'to', 'relationType'] } - }, - required: ['query'] - } - }, - { - name: 'open_nodes', - description: 'Retrieve specific entities and their connecting relations from memory by name.', - inputSchema: { - type: 'object', - properties: { - names: { - type: 'array', - items: { type: 'string' }, - description: 'An array of entity names to retrieve' + } + }, + required: ['relations'] + } + }, + { + name: 'add_observations', + description: 'Add new observations to existing entities. Skips duplicate observations.', + inputSchema: { + type: 'object', + properties: { + observations: { + type: 'array', + items: { + type: 'object', + properties: { + entityName: { type: 'string', description: 'The name of the entity to add the observations to' }, + contents: { + type: 'array', + items: { type: 'string' }, + description: 'An array of observation contents to add' + } + }, + required: ['entityName', 'contents'] } - }, - required: ['names'] - } - } - ] + } + }, + required: ['observations'] + } + }, + { + name: 'delete_entities', + description: 'Delete multiple entities and their associated relations.', + inputSchema: { + type: 'object', + properties: { + entityNames: { + type: 'array', + items: { type: 'string' }, + description: 'An array of entity names to delete' + } + }, + required: ['entityNames'] + } + }, + { + name: 'delete_observations', + description: 'Delete specific observations from entities.', + inputSchema: { + type: 'object', + properties: { + deletions: { + type: 'array', + items: { + type: 'object', + properties: { + entityName: { type: 'string', description: 'The name of the entity containing the observations' }, + observations: { + type: 'array', + items: { type: 'string' }, + description: 'An array of observations to delete' + } + }, + required: ['entityName', 'observations'] + } + } + }, + required: ['deletions'] + } + }, + { + name: 'delete_relations', + description: 'Delete multiple specific relations.', + inputSchema: { + type: 'object', + properties: { + relations: { + type: 'array', + items: { + type: 'object', + properties: { + from: { type: 'string', description: 'The name of the entity where the relation starts' }, + to: { type: 'string', description: 'The name of the entity where the relation ends' }, + relationType: { type: 'string', description: 'The type of the relation' } + }, + required: ['from', 'to', 'relationType'] + }, + description: 'An array of relations to delete' + } + }, + required: ['relations'] + } + }, + { + name: 'read_graph', + description: 'Read the entire knowledge graph from memory.', + inputSchema: { + type: 'object', + properties: {} + } + }, + { + name: 'search_nodes', + description: 'Search nodes (entities and relations) in memory based on a query.', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'The search query to match against entity names, types, and observation content' + } + }, + required: ['query'] + } + }, + { + name: 'open_nodes', + description: 'Retrieve specific entities and their connecting relations from memory by name.', + inputSchema: { + type: 'object', + properties: { + names: { + type: 'array', + items: { type: 'string' }, + description: 'An array of entity names to retrieve' + } + }, + required: ['names'] + } + } + ] } }) // CallTool handler needs to await the manager and the async methods this.server.setRequestHandler(CallToolRequestSchema, async (request) => { - const manager = await this._getManager(); // Ensure manager is ready + const manager = await this._getManager() // Ensure manager is ready const { name, arguments: args } = request.params if (!args) { @@ -573,41 +584,75 @@ class MemoryServer { case 'create_entities': // Validate args structure if necessary, though SDK might do basic validation if (!args.entities || !Array.isArray(args.entities)) { - throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'entities' array is required.`); + throw new McpError( + ErrorCode.InvalidParams, + `Invalid arguments for ${name}: 'entities' array is required.` + ) } return { - content: [{ type: 'text', text: JSON.stringify(await manager.createEntities(args.entities as Entity[]), null, 2) }] + content: [ + { type: 'text', text: JSON.stringify(await manager.createEntities(args.entities as Entity[]), null, 2) } + ] } case 'create_relations': - if (!args.relations || !Array.isArray(args.relations)) { - throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'relations' array is required.`); - } + if (!args.relations || !Array.isArray(args.relations)) { + throw new McpError( + ErrorCode.InvalidParams, + `Invalid arguments for ${name}: 'relations' array is required.` + ) + } return { - content: [{ type: 'text', text: JSON.stringify(await manager.createRelations(args.relations as Relation[]), null, 2) }] + content: [ + { + type: 'text', + text: JSON.stringify(await manager.createRelations(args.relations as Relation[]), null, 2) + } + ] } case 'add_observations': - if (!args.observations || !Array.isArray(args.observations)) { - throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'observations' array is required.`); - } + if (!args.observations || !Array.isArray(args.observations)) { + throw new McpError( + ErrorCode.InvalidParams, + `Invalid arguments for ${name}: 'observations' array is required.` + ) + } return { - content: [{ type: 'text', text: JSON.stringify(await manager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] + content: [ + { + type: 'text', + text: JSON.stringify( + await manager.addObservations(args.observations as { entityName: string; contents: string[] }[]), + null, + 2 + ) + } + ] } case 'delete_entities': - if (!args.entityNames || !Array.isArray(args.entityNames)) { - throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'entityNames' array is required.`); - } + if (!args.entityNames || !Array.isArray(args.entityNames)) { + throw new McpError( + ErrorCode.InvalidParams, + `Invalid arguments for ${name}: 'entityNames' array is required.` + ) + } await manager.deleteEntities(args.entityNames as string[]) return { content: [{ type: 'text', text: 'Entities deleted successfully' }] } case 'delete_observations': - if (!args.deletions || !Array.isArray(args.deletions)) { - throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'deletions' array is required.`); - } + if (!args.deletions || !Array.isArray(args.deletions)) { + throw new McpError( + ErrorCode.InvalidParams, + `Invalid arguments for ${name}: 'deletions' array is required.` + ) + } await manager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]) return { content: [{ type: 'text', text: 'Observations deleted successfully' }] } case 'delete_relations': - if (!args.relations || !Array.isArray(args.relations)) { - throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'relations' array is required.`); - } + if (!args.relations || !Array.isArray(args.relations)) { + throw new McpError( + ErrorCode.InvalidParams, + `Invalid arguments for ${name}: 'relations' array is required.` + ) + } await manager.deleteRelations(args.relations as Relation[]) return { content: [{ type: 'text', text: 'Relations deleted successfully' }] } case 'read_graph': @@ -616,30 +661,37 @@ class MemoryServer { content: [{ type: 'text', text: JSON.stringify(await manager.readGraph(), null, 2) }] } case 'search_nodes': - if (typeof args.query !== 'string') { - throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'query' string is required.`); - } + if (typeof args.query !== 'string') { + throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'query' string is required.`) + } return { - content: [{ type: 'text', text: JSON.stringify(await manager.searchNodes(args.query as string), null, 2) }] + content: [ + { type: 'text', text: JSON.stringify(await manager.searchNodes(args.query as string), null, 2) } + ] } case 'open_nodes': - if (!args.names || !Array.isArray(args.names)) { - throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'names' array is required.`); - } + if (!args.names || !Array.isArray(args.names)) { + throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'names' array is required.`) + } return { - content: [{ type: 'text', text: JSON.stringify(await manager.openNodes(args.names as string[]), null, 2) }] + content: [ + { type: 'text', text: JSON.stringify(await manager.openNodes(args.names as string[]), null, 2) } + ] } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`) } } catch (error) { - // Catch errors from manager methods (like entity not found) or other issues - if (error instanceof McpError) { - throw error; // Re-throw McpErrors directly - } - console.error(`Error executing tool ${name}:`, error); - // Throw a generic internal error for unexpected issues - throw new McpError(ErrorCode.InternalError, `Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}`); + // Catch errors from manager methods (like entity not found) or other issues + if (error instanceof McpError) { + throw error // Re-throw McpErrors directly + } + console.error(`Error executing tool ${name}:`, error) + // Throw a generic internal error for unexpected issues + throw new McpError( + ErrorCode.InternalError, + `Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}` + ) } }) } diff --git a/src/main/mcpServers/simpleremember.ts b/src/main/mcpServers/simpleremember.ts index 1da17fe2f4..cf692eb7b3 100644 --- a/src/main/mcpServers/simpleremember.ts +++ b/src/main/mcpServers/simpleremember.ts @@ -1,99 +1,114 @@ // src/main/mcpServers/simpleremember.ts import { getConfigDir } from '@main/utils/file' import { Server } from '@modelcontextprotocol/sdk/server/index.js' -import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js' +import { + CallToolRequestSchema, + ErrorCode, + ListPromptsRequestSchema, + ListToolsRequestSchema, + McpError +} from '@modelcontextprotocol/sdk/types.js' +import { Mutex } from 'async-mutex' import { promises as fs } from 'fs' import path from 'path' -import { Mutex } from 'async-mutex' // 定义记忆文件路径 const defaultMemoryPath = path.join(getConfigDir(), 'simpleremember.json') // 记忆项接口 interface Memory { - content: string; - createdAt: string; + content: string + createdAt: string } // 记忆存储结构 interface MemoryStorage { - memories: Memory[]; + memories: Memory[] } class SimpleRememberManager { - private memoryPath: string; - private memories: Memory[] = []; - private fileMutex: Mutex = new Mutex(); + private memoryPath: string + private memories: Memory[] = [] + private fileMutex: Mutex = new Mutex() constructor(memoryPath: string) { - this.memoryPath = memoryPath; + this.memoryPath = memoryPath } // 静态工厂方法用于初始化 public static async create(memoryPath: string): Promise { - const manager = new SimpleRememberManager(memoryPath); - await manager._ensureMemoryPathExists(); - await manager._loadMemoriesFromDisk(); - return manager; + const manager = new SimpleRememberManager(memoryPath) + await manager._ensureMemoryPathExists() + await manager._loadMemoriesFromDisk() + return manager } // 确保记忆文件存在 private async _ensureMemoryPathExists(): Promise { try { - const directory = path.dirname(this.memoryPath); - await fs.mkdir(directory, { recursive: true }); + const directory = path.dirname(this.memoryPath) + await fs.mkdir(directory, { recursive: true }) try { - await fs.access(this.memoryPath); + await fs.access(this.memoryPath) } catch (error) { // 文件不存在,创建一个空文件 - await fs.writeFile(this.memoryPath, JSON.stringify({ memories: [] }, null, 2)); + await fs.writeFile(this.memoryPath, JSON.stringify({ memories: [] }, null, 2)) } } catch (error) { - console.error('Failed to ensure memory path exists:', error); - throw new McpError(ErrorCode.InternalError, `Failed to ensure memory path: ${error instanceof Error ? error.message : String(error)}`); + console.error('Failed to ensure memory path exists:', error) + throw new McpError( + ErrorCode.InternalError, + `Failed to ensure memory path: ${error instanceof Error ? error.message : String(error)}` + ) } } // 从磁盘加载记忆 private async _loadMemoriesFromDisk(): Promise { try { - const data = await fs.readFile(this.memoryPath, 'utf-8'); + const data = await fs.readFile(this.memoryPath, 'utf-8') // 处理空文件情况 if (data.trim() === '') { - this.memories = []; - await this._persistMemories(); - return; + this.memories = [] + await this._persistMemories() + return } - const storage: MemoryStorage = JSON.parse(data); - this.memories = storage.memories || []; + const storage: MemoryStorage = JSON.parse(data) + this.memories = storage.memories || [] } catch (error) { if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') { - this.memories = []; - await this._persistMemories(); + this.memories = [] + await this._persistMemories() } else if (error instanceof SyntaxError) { - console.error('Failed to parse simpleremember.json, initializing with empty memories:', error); - this.memories = []; - await this._persistMemories(); + console.error('Failed to parse simpleremember.json, initializing with empty memories:', error) + this.memories = [] + await this._persistMemories() } else { - console.error('Unexpected error loading memories:', error); - throw new McpError(ErrorCode.InternalError, `Failed to load memories: ${error instanceof Error ? error.message : String(error)}`); + console.error('Unexpected error loading memories:', error) + throw new McpError( + ErrorCode.InternalError, + `Failed to load memories: ${error instanceof Error ? error.message : String(error)}` + ) } } } // 将记忆持久化到磁盘 private async _persistMemories(): Promise { - const release = await this.fileMutex.acquire(); + const release = await this.fileMutex.acquire() try { const storage: MemoryStorage = { memories: this.memories - }; - await fs.writeFile(this.memoryPath, JSON.stringify(storage, null, 2)); + } + await fs.writeFile(this.memoryPath, JSON.stringify(storage, null, 2)) } catch (error) { - console.error('Failed to save memories:', error); - throw new McpError(ErrorCode.InternalError, `Failed to save memories: ${error instanceof Error ? error.message : String(error)}`); + console.error('Failed to save memories:', error) + throw new McpError( + ErrorCode.InternalError, + `Failed to save memories: ${error instanceof Error ? error.message : String(error)}` + ) } finally { - release(); + release() } } @@ -102,27 +117,28 @@ class SimpleRememberManager { const newMemory: Memory = { content: memory, createdAt: new Date().toISOString() - }; - this.memories.push(newMemory); - await this._persistMemories(); - return newMemory; + } + this.memories.push(newMemory) + await this._persistMemories() + return newMemory } // 获取所有记忆 async getAllMemories(): Promise { - return [...this.memories]; + return [...this.memories] } // 获取记忆 - 这个方法会被get_memories工具调用 async get_memories(): Promise { - return this.getAllMemories(); + return this.getAllMemories() } } // 定义工具 - 按照MCP规范定义工具 const REMEMBER_TOOL = { name: 'remember', - description: '用于记忆长期有用信息的工具。这个工具会自动应用记忆,无需显式调用。只用于存储长期有用的信息,不适合临时信息。', + description: + '用于记忆长期有用信息的工具。这个工具会自动应用记忆,无需显式调用。只用于存储长期有用的信息,不适合临时信息。', inputSchema: { type: 'object', properties: { @@ -133,7 +149,7 @@ const REMEMBER_TOOL = { }, required: ['memory'] } -}; +} const GET_MEMORIES_TOOL = { name: 'get_memories', @@ -142,24 +158,20 @@ const GET_MEMORIES_TOOL = { type: 'object', properties: {} } -}; +} // 添加日志以便调试 -console.log("[SimpleRemember] Defined tools:", { REMEMBER_TOOL, GET_MEMORIES_TOOL }); +console.log('[SimpleRemember] Defined tools:', { REMEMBER_TOOL, GET_MEMORIES_TOOL }) class SimpleRememberServer { - public server: Server; - private simpleRememberManager: SimpleRememberManager | null = null; - private initializationPromise: Promise; + public server: Server + private simpleRememberManager: SimpleRememberManager | null = null + private initializationPromise: Promise constructor(envPath: string = '') { - const memoryPath = envPath - ? path.isAbsolute(envPath) - ? envPath - : path.resolve(envPath) - : defaultMemoryPath; + const memoryPath = envPath ? (path.isAbsolute(envPath) ? envPath : path.resolve(envPath)) : defaultMemoryPath - console.log("[SimpleRemember] Creating server with memory path:", memoryPath); + console.log('[SimpleRemember] Creating server with memory path:', memoryPath) // 初始化服务器 this.server = new Server( @@ -177,127 +189,133 @@ class SimpleRememberServer { prompts: {} } } - ); + ) - console.log("[SimpleRemember] Server initialized with tools capability"); + console.log('[SimpleRemember] Server initialized with tools capability') // 手动添加工具到服务器的工具列表中 - console.log("[SimpleRemember] Adding tools to server"); + console.log('[SimpleRemember] Adding tools to server') // 先设置请求处理程序,再初始化管理器 - this.setupRequestHandlers(); - this.initializationPromise = this._initializeManager(memoryPath); + this.setupRequestHandlers() + this.initializationPromise = this._initializeManager(memoryPath) - console.log("[SimpleRemember] Server initialization complete"); + console.log('[SimpleRemember] Server initialization complete') // 打印工具信息以确认它们已注册 - console.log("[SimpleRemember] Tools registered:", [REMEMBER_TOOL.name, GET_MEMORIES_TOOL.name]); + console.log('[SimpleRemember] Tools registered:', [REMEMBER_TOOL.name, GET_MEMORIES_TOOL.name]) } private async _initializeManager(memoryPath: string): Promise { try { - this.simpleRememberManager = await SimpleRememberManager.create(memoryPath); - console.log("SimpleRememberManager initialized successfully."); + this.simpleRememberManager = await SimpleRememberManager.create(memoryPath) + console.log('SimpleRememberManager initialized successfully.') } catch (error) { - console.error("Failed to initialize SimpleRememberManager:", error); - this.simpleRememberManager = null; + console.error('Failed to initialize SimpleRememberManager:', error) + this.simpleRememberManager = null } } private async _getManager(): Promise { if (!this.simpleRememberManager) { - await this.initializationPromise; + await this.initializationPromise if (!this.simpleRememberManager) { - throw new McpError(ErrorCode.InternalError, "SimpleRememberManager is not initialized"); + throw new McpError(ErrorCode.InternalError, 'SimpleRememberManager is not initialized') } } - return this.simpleRememberManager; + return this.simpleRememberManager } setupRequestHandlers() { // 添加对prompts/list请求的处理 this.server.setRequestHandler(ListPromptsRequestSchema, async (request) => { - console.log("[SimpleRemember] Listing prompts request received", request); + console.log('[SimpleRemember] Listing prompts request received', request) // 返回空的提示词列表 return { prompts: [] - }; - }); + } + }) this.server.setRequestHandler(ListToolsRequestSchema, async (request) => { // 直接返回工具列表,不需要等待管理器初始化 - console.log("[SimpleRemember] Listing tools request received", request); + console.log('[SimpleRemember] Listing tools request received', request) // 打印工具定义以确保它们存在 - console.log("[SimpleRemember] REMEMBER_TOOL:", JSON.stringify(REMEMBER_TOOL)); - console.log("[SimpleRemember] GET_MEMORIES_TOOL:", JSON.stringify(GET_MEMORIES_TOOL)); + console.log('[SimpleRemember] REMEMBER_TOOL:', JSON.stringify(REMEMBER_TOOL)) + console.log('[SimpleRemember] GET_MEMORIES_TOOL:', JSON.stringify(GET_MEMORIES_TOOL)) - const toolsList = [REMEMBER_TOOL, GET_MEMORIES_TOOL]; - console.log("[SimpleRemember] Returning tools:", JSON.stringify(toolsList)); + const toolsList = [REMEMBER_TOOL, GET_MEMORIES_TOOL] + console.log('[SimpleRemember] Returning tools:', JSON.stringify(toolsList)) // 按照MCP规范返回工具列表 return { - tools: toolsList, + tools: toolsList // 如果有分页,可以添加nextCursor // nextCursor: "next-page-cursor" - }; - }); + } + }) this.server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; + const { name, arguments: args } = request.params - console.log(`[SimpleRemember] Received tool call: ${name}`, args); + console.log(`[SimpleRemember] Received tool call: ${name}`, args) try { - const manager = await this._getManager(); + const manager = await this._getManager() if (name === 'remember') { if (!args || typeof args.memory !== 'string') { - console.error(`[SimpleRemember] Invalid arguments for ${name}:`, args); - throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'memory' string is required.`); + console.error(`[SimpleRemember] Invalid arguments for ${name}:`, args) + throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'memory' string is required.`) } - console.log(`[SimpleRemember] Remembering: "${args.memory}"`); - const result = await manager.remember(args.memory); - console.log(`[SimpleRemember] Memory saved successfully:`, result); + console.log(`[SimpleRemember] Remembering: "${args.memory}"`) + const result = await manager.remember(args.memory) + console.log(`[SimpleRemember] Memory saved successfully:`, result) // 按照MCP规范返回工具调用结果 return { - content: [{ - type: 'text', - text: `记忆已保存: "${args.memory}"` - }], + content: [ + { + type: 'text', + text: `记忆已保存: "${args.memory}"` + } + ], isError: false - }; + } } if (name === 'get_memories') { - console.log(`[SimpleRemember] Getting all memories`); - const memories = await manager.get_memories(); - console.log(`[SimpleRemember] Retrieved ${memories.length} memories`); + console.log(`[SimpleRemember] Getting all memories`) + const memories = await manager.get_memories() + console.log(`[SimpleRemember] Retrieved ${memories.length} memories`) // 按照MCP规范返回工具调用结果 return { - content: [{ - type: 'text', - text: JSON.stringify(memories, null, 2) - }], + content: [ + { + type: 'text', + text: JSON.stringify(memories, null, 2) + } + ], isError: false - }; + } } - console.error(`[SimpleRemember] Unknown tool: ${name}`); - throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); + console.error(`[SimpleRemember] Unknown tool: ${name}`) + throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`) } catch (error) { - console.error(`[SimpleRemember] Error handling tool call ${name}:`, error); + console.error(`[SimpleRemember] Error handling tool call ${name}:`, error) // 按照MCP规范返回工具调用错误 return { - content: [{ - type: 'text', - text: error instanceof Error ? error.message : String(error) - }], + content: [ + { + type: 'text', + text: error instanceof Error ? error.message : String(error) + } + ], isError: true - }; + } } - }); + }) } } -export default SimpleRememberServer; +export default SimpleRememberServer diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index c5e70a920e..6a54e54951 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -6,6 +6,7 @@ import { HashRouter, Route, Routes } from 'react-router-dom' import { PersistGate } from 'redux-persist/integration/react' import Sidebar from './components/app/Sidebar' +import MemoryProvider from './components/MemoryProvider' import TopViewContainer from './components/TopView' import AntdProvider from './context/AntdProvider' import StyleSheetManager from './context/StyleSheetManager' @@ -29,22 +30,24 @@ function App(): React.ReactElement { - - - - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - + + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + diff --git a/src/renderer/src/components/MemoryProvider.tsx b/src/renderer/src/components/MemoryProvider.tsx new file mode 100644 index 0000000000..defc213e4b --- /dev/null +++ b/src/renderer/src/components/MemoryProvider.tsx @@ -0,0 +1,72 @@ +import { useMemoryService } from '@renderer/services/MemoryService' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import { clearShortMemories } from '@renderer/store/memory' +import { FC, ReactNode, useEffect, useRef } from 'react' + +interface MemoryProviderProps { + children: ReactNode +} + +/** + * 记忆功能提供者组件 + * 这个组件负责初始化记忆功能并在适当的时候触发记忆分析 + */ +const MemoryProvider: FC = ({ children }) => { + console.log('[MemoryProvider] Initializing memory provider') + const { analyzeAndAddMemories } = useMemoryService() + const dispatch = useAppDispatch() + + // 从 Redux 获取记忆状态 + const isActive = useAppSelector((state) => state.memory?.isActive || false) + const autoAnalyze = useAppSelector((state) => state.memory?.autoAnalyze || false) + const analyzeModel = useAppSelector((state) => state.memory?.analyzeModel || null) + const shortMemoryActive = useAppSelector((state) => state.memory?.shortMemoryActive || false) + + // 获取当前对话 + const currentTopic = useAppSelector((state) => state.messages?.currentTopic?.id) + const messages = useAppSelector((state) => { + if (!currentTopic || !state.messages?.messagesByTopic) { + return [] + } + return state.messages.messagesByTopic[currentTopic] || [] + }) + + // 存储上一次的话题ID + const previousTopicRef = useRef(null) + + // 添加一个 ref 来存储上次分析时的消息数量 + const lastAnalyzedCountRef = useRef(0) + + // 当对话更新时,触发记忆分析 + useEffect(() => { + if (isActive && autoAnalyze && analyzeModel && messages.length > 0) { + // 检查是否有新消息需要分析 + const newMessagesCount = messages.length - lastAnalyzedCountRef.current + + // 当有 5 条或更多新消息,或者消息数量是 5 的倍数且从未分析过时触发分析 + if (newMessagesCount >= 5 || (messages.length % 5 === 0 && lastAnalyzedCountRef.current === 0)) { + console.log(`[Memory Analysis] Triggering analysis with ${newMessagesCount} new messages`) + // 将当前话题ID传递给分析函数 + analyzeAndAddMemories(currentTopic) + lastAnalyzedCountRef.current = messages.length + } + } + }, [isActive, autoAnalyze, analyzeModel, messages.length, analyzeAndAddMemories, currentTopic]) + + // 当对话话题切换时,清除上一个话题的短记忆 + useEffect(() => { + // 如果短记忆功能激活且当前话题发生变化 + if (shortMemoryActive && currentTopic !== previousTopicRef.current && previousTopicRef.current) { + console.log(`[Memory] Topic changed from ${previousTopicRef.current} to ${currentTopic}, clearing short memories`) + // 清除上一个话题的短记忆 + dispatch(clearShortMemories(previousTopicRef.current)) + } + + // 更新上一次的话题ID + previousTopicRef.current = currentTopic + }, [currentTopic, shortMemoryActive, dispatch]) + + return <>{children} +} + +export default MemoryProvider diff --git a/src/renderer/src/i18n/index.ts b/src/renderer/src/i18n/index.ts index 9ecc0581cd..04979102aa 100644 --- a/src/renderer/src/i18n/index.ts +++ b/src/renderer/src/i18n/index.ts @@ -3,11 +3,11 @@ import i18n from 'i18next' import { initReactI18next } from 'react-i18next' // Original translation -import enUS from './locales/en-us.json' -import jaJP from './locales/ja-jp.json' -import ruRU from './locales/ru-ru.json' -import zhCN from './locales/zh-cn.json' -import zhTW from './locales/zh-tw.json' +import enUS from './locales/en-US.json' +import jaJP from './locales/ja-JP.json' +import ruRU from './locales/ru-RU.json' +import zhCN from './locales/zh-CN.json' +import zhTW from './locales/zh-TW.json' // Machine translation import elGR from './translate/el-gr.json' import esES from './translate/es-es.json' diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 322d5d26c4..24717916ba 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1024,6 +1024,44 @@ "launch.onboot": "Start Automatically on Boot", "launch.title": "Launch", "launch.totray": "Minimize to Tray on Launch", + "memory": { + "title": "Memory Function", + "description": "Manage AI assistant's long-term memory, automatically analyze conversations and extract important information", + "enableMemory": "Enable Memory Function", + "enableAutoAnalyze": "Enable Auto Analysis", + "analyzeModel": "Analysis Model", + "selectModel": "Select Model", + "memoriesList": "Memory List", + "addMemory": "Add Memory", + "editMemory": "Edit Memory", + "clearAll": "Clear All", + "noMemories": "No memories yet", + "memoryPlaceholder": "Enter content to remember", + "addSuccess": "Memory added successfully", + "editSuccess": "Memory edited successfully", + "deleteSuccess": "Memory deleted successfully", + "clearSuccess": "Memories cleared successfully", + "clearConfirmTitle": "Confirm Clear", + "clearConfirmContent": "Are you sure you want to clear all memories? This action cannot be undone.", + "manualAnalyze": "Manual Analysis", + "analyzeNow": "Analyze Now", + "startingAnalysis": "Starting analysis...", + "cannotAnalyze": "Cannot analyze, please check settings", + "selectTopic": "Select Topic", + "selectTopicPlaceholder": "Select a topic to analyze", + "filterByCategory": "Filter by Category", + "allCategories": "All", + "uncategorized": "Uncategorized", + "shortMemory": "Short-term Memory", + "toggleShortMemoryActive": "Toggle Short-term Memory", + "addShortMemory": "Add Short-term Memory", + "addShortMemoryPlaceholder": "Enter short-term memory content, only valid in current conversation", + "noShortMemories": "No short-term memories", + "noCurrentTopic": "Please select a conversation topic first", + "confirmDelete": "Confirm Delete", + "confirmDeleteContent": "Are you sure you want to delete this short-term memory?", + "delete": "Delete" + }, "mcp": { "actions": "Actions", "active": "Active", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 1b7c54dfbe..fb0311a64c 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1024,6 +1024,59 @@ "launch.onboot": "开机自动启动", "launch.title": "启动", "launch.totray": "启动时最小化到托盘", + "memory": { + "title": "记忆功能", + "description": "管理AI助手的长期记忆,自动分析对话并提取重要信息", + "enableMemory": "启用记忆功能", + "enableAutoAnalyze": "启用自动分析", + "analyzeModel": "分析模型", + "selectModel": "选择模型", + "memoriesList": "记忆列表", + "memoryLists": "记忆角色", + "addMemory": "添加记忆", + "editMemory": "编辑记忆", + "clearAll": "清空全部", + "noMemories": "暂无记忆", + "memoryPlaceholder": "输入要记住的内容", + "addSuccess": "记忆添加成功", + "editSuccess": "记忆编辑成功", + "deleteSuccess": "记忆删除成功", + "clearSuccess": "记忆清空成功", + "clearConfirmTitle": "确认清空", + "clearConfirmContent": "确定要清空所有记忆吗?此操作无法撤销。", + "listView": "列表视图", + "mindmapView": "思维导图", + "centerNodeLabel": "用户记忆", + "manualAnalyze": "手动分析", + "analyzeNow": "立即分析", + "startingAnalysis": "开始分析...", + "cannotAnalyze": "无法分析,请检查设置", + "selectTopic": "选择话题", + "selectTopicPlaceholder": "选择要分析的话题", + "filterByCategory": "按分类筛选", + "allCategories": "全部", + "uncategorized": "未分类", + "addList": "添加记忆列表", + "editList": "编辑记忆列表", + "listName": "列表名称", + "listNamePlaceholder": "输入列表名称", + "listDescription": "列表描述", + "listDescriptionPlaceholder": "输入列表描述(可选)", + "noLists": "暂无记忆列表", + "confirmDeleteList": "确认删除列表", + "confirmDeleteListContent": "确定要删除 {{name}} 列表吗?此操作将同时删除列表中的所有记忆,且不可恢复。", + "toggleActive": "切换激活状态", + "clearConfirmContentList": "确定要清空 {{name}} 中的所有记忆吗?此操作不可恢复。", + "shortMemory": "短期记忆", + "toggleShortMemoryActive": "切换短期记忆功能", + "addShortMemory": "添加短期记忆", + "addShortMemoryPlaceholder": "输入短期记忆内容,只在当前对话中有效", + "noShortMemories": "暂无短期记忆", + "noCurrentTopic": "请先选择一个对话话题", + "confirmDelete": "确认删除", + "confirmDeleteContent": "确定要删除这条短期记忆吗?", + "delete": "删除" + }, "mcp": { "actions": "操作", "active": "启用", diff --git a/src/renderer/src/pages/home/Messages/MessageError.tsx b/src/renderer/src/pages/home/Messages/MessageError.tsx index 9b2da544e7..9fd2763188 100644 --- a/src/renderer/src/pages/home/Messages/MessageError.tsx +++ b/src/renderer/src/pages/home/Messages/MessageError.tsx @@ -7,6 +7,31 @@ import styled from 'styled-components' import Markdown from '../Markdown/Markdown' const MessageError: FC<{ message: Message }> = ({ message }) => { + const { t } = useTranslation() + + // 首先检查是否存在已知的问题错误 + if (message.error && typeof message.error === 'object') { + // 处理 rememberInstructions 错误 + if (message.error.message === 'rememberInstructions is not defined') { + return ( + <> + + + + ) + } + + // 处理网络错误 + if (message.error.message === 'network error') { + return ( + <> + + + + ) + } + } + return ( <> @@ -28,7 +53,13 @@ const MessageErrorInfo: FC<{ message: Message }> = ({ message }) => { const HTTP_ERROR_CODES = [400, 401, 403, 404, 429, 500, 502, 503, 504] - if (message.error && HTTP_ERROR_CODES.includes(message.error?.status)) { + // Add more robust checks: ensure error is an object and status is a number before accessing/including + if ( + message.error && + typeof message.error === 'object' && // Check if error is an object + typeof message.error.status === 'number' && // Check if status is a number + HTTP_ERROR_CODES.includes(message.error.status) // Now safe to access status + ) { return } diff --git a/src/renderer/src/pages/home/Messages/MessageErrorBoundary.tsx b/src/renderer/src/pages/home/Messages/MessageErrorBoundary.tsx index bc6a692776..79129b4989 100644 --- a/src/renderer/src/pages/home/Messages/MessageErrorBoundary.tsx +++ b/src/renderer/src/pages/home/Messages/MessageErrorBoundary.tsx @@ -9,6 +9,7 @@ interface Props { interface State { hasError: boolean + errorMessage?: string } const ErrorFallback = ({ fallback }: { fallback?: React.ReactNode }) => { @@ -26,16 +27,52 @@ class MessageErrorBoundary extends React.Component { this.state = { hasError: false } } - static getDerivedStateFromError() { - return { hasError: true } + static getDerivedStateFromError(error: Error) { + // 检查是否是特定错误 + let errorMessage: string | undefined = undefined + + if (error.message === 'rememberInstructions is not defined') { + errorMessage = '消息加载时发生错误' + } else if (error.message === 'network error') { + errorMessage = '网络连接错误,请检查您的网络连接并重试' + } else if ( + typeof error.message === 'string' && + (error.message.includes('network') || error.message.includes('timeout') || error.message.includes('connection')) + ) { + errorMessage = '网络连接问题' + } + + return { hasError: true, errorMessage } + } + // 正确缩进 componentDidCatch + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + // Log the detailed error information to the console + console.error('MessageErrorBoundary caught an error:', error, errorInfo) + + // 如果是特定错误,记录更多信息 + if (error.message === 'rememberInstructions is not defined') { + console.warn('Known issue with rememberInstructions detected in MessageErrorBoundary') + } else if (error.message === 'network error') { + console.warn('Network error detected in MessageErrorBoundary') + } else if ( + typeof error.message === 'string' && + (error.message.includes('network') || error.message.includes('timeout') || error.message.includes('connection')) + ) { + console.warn('Network-related error detected in MessageErrorBoundary:', error.message) + } } + // 正确缩进 render render() { if (this.state.hasError) { + // 如果有特定错误消息,显示自定义错误 + if (this.state.errorMessage) { + return + } return } return this.props.children } -} +} // MessageErrorBoundary 类的结束括号,已删除多余的括号 export default MessageErrorBoundary diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index 6a0d8d6dd3..584aeaeb35 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -148,35 +148,42 @@ const MessageMenubar: FC = (props) => { const imageUrls: string[] = [] let match let content = editedText - + while ((match = imageRegex.exec(editedText)) !== null) { imageUrls.push(match[1]) content = content.replace(match[0], '') } - + // 更新消息内容,保留图片信息 - await editMessage(message.id, { + await editMessage(message.id, { content: content.trim(), metadata: { ...message.metadata, - generateImage: imageUrls.length > 0 ? { - type: 'url', - images: imageUrls - } : undefined - } - }) - - resendMessage && handleResendUserMessage({ - ...message, - content: content.trim(), - metadata: { - ...message.metadata, - generateImage: imageUrls.length > 0 ? { - type: 'url', - images: imageUrls - } : undefined + generateImage: + imageUrls.length > 0 + ? { + type: 'url', + images: imageUrls + } + : undefined } }) + + resendMessage && + handleResendUserMessage({ + ...message, + content: content.trim(), + metadata: { + ...message.metadata, + generateImage: + imageUrls.length > 0 + ? { + type: 'url', + images: imageUrls + } + : undefined + } + }) } }, [message, editMessage, handleResendUserMessage, t]) diff --git a/src/renderer/src/pages/settings/MemorySettings/CenterNode.tsx b/src/renderer/src/pages/settings/MemorySettings/CenterNode.tsx new file mode 100644 index 0000000000..b17b4892c7 --- /dev/null +++ b/src/renderer/src/pages/settings/MemorySettings/CenterNode.tsx @@ -0,0 +1,30 @@ +import { Handle, Position } from '@xyflow/react'; +import { Card, Typography } from 'antd'; +import styled from 'styled-components'; + +interface CenterNodeProps { + data: { + label: string; + }; +} + +const CenterNode: React.FC = ({ data }) => { + return ( + + + {data.label} + + + + + + + ); +}; + +const NodeContainer = styled.div` + width: 150px; + text-align: center; +`; + +export default CenterNode; diff --git a/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx b/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx new file mode 100644 index 0000000000..3a85481ba3 --- /dev/null +++ b/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx @@ -0,0 +1,261 @@ +import { DeleteOutlined, EditOutlined, ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import { + addMemoryList, + deleteMemoryList, + editMemoryList, + MemoryList, + setCurrentMemoryList, + toggleMemoryListActive +} from '@renderer/store/memory' +import { Button, Empty, Input, List, Modal, Switch, Tooltip, Typography } from 'antd' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +const { Title } = Typography +const { confirm } = Modal + +interface MemoryListManagerProps { + onSelectList?: (listId: string) => void +} + +const MemoryListManager: React.FC = ({ onSelectList }) => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const memoryLists = useAppSelector((state) => state.memory?.memoryLists || []) + const currentListId = useAppSelector((state) => state.memory?.currentListId) + + const [isModalVisible, setIsModalVisible] = useState(false) + const [editingList, setEditingList] = useState(null) + const [newListName, setNewListName] = useState('') + const [newListDescription, setNewListDescription] = useState('') + + // 打开添加/编辑列表的模态框 + const showModal = (list?: MemoryList) => { + if (list) { + setEditingList(list) + setNewListName(list.name) + setNewListDescription(list.description || '') + } else { + setEditingList(null) + setNewListName('') + setNewListDescription('') + } + setIsModalVisible(true) + } + + // 处理模态框确认 + const handleOk = () => { + if (!newListName.trim()) { + return // 名称不能为空 + } + + if (editingList) { + // 编辑现有列表 + dispatch( + editMemoryList({ + id: editingList.id, + name: newListName, + description: newListDescription + }) + ) + } else { + // 添加新列表 + dispatch( + addMemoryList({ + name: newListName, + description: newListDescription, + isActive: false + }) + ) + } + + setIsModalVisible(false) + setNewListName('') + setNewListDescription('') + setEditingList(null) + } + + // 处理模态框取消 + const handleCancel = () => { + setIsModalVisible(false) + setNewListName('') + setNewListDescription('') + setEditingList(null) + } + + // 删除记忆列表 + const handleDelete = (list: MemoryList) => { + confirm({ + title: t('settings.memory.confirmDeleteList'), + icon: , + content: t('settings.memory.confirmDeleteListContent', { name: list.name }), + okText: t('common.delete'), + okType: 'danger', + cancelText: t('common.cancel'), + onOk() { + dispatch(deleteMemoryList(list.id)) + } + }) + } + + // 切换列表激活状态 + const handleToggleActive = (list: MemoryList, checked: boolean) => { + dispatch(toggleMemoryListActive({ id: list.id, isActive: checked })) + } + + // 选择列表 + const handleSelectList = (listId: string) => { + dispatch(setCurrentMemoryList(listId)) + if (onSelectList) { + onSelectList(listId) + } + } + + return ( + +
+ {t('settings.memory.memoryLists')} + +
+ + {memoryLists.length === 0 ? ( + + ) : ( + ( + handleSelectList(list.id)} $isActive={list.id === currentListId}> + +
+ {list.name} + {list.description && {list.description}} +
+ e.stopPropagation()}> + + handleToggleActive(list, checked)} + size="small" + /> + + + + + +
+ {shortMemories.length > 0 ? ( + ( + +
} + description={new Date(memory.createdAt).toLocaleString()} + /> + + )} + /> + ) : ( + + )} + + + ) +} + +export default ShortMemoryManager diff --git a/src/renderer/src/pages/settings/MemorySettings/index.tsx b/src/renderer/src/pages/settings/MemorySettings/index.tsx new file mode 100644 index 0000000000..3df80dea12 --- /dev/null +++ b/src/renderer/src/pages/settings/MemorySettings/index.tsx @@ -0,0 +1,535 @@ +import { + AppstoreOutlined, + DeleteOutlined, + EditOutlined, + PlusOutlined, + SearchOutlined, + UnorderedListOutlined +} from '@ant-design/icons' +import { useTheme } from '@renderer/context/ThemeProvider' +import { TopicManager } from '@renderer/hooks/useTopic' +import { useMemoryService } from '@renderer/services/MemoryService' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import { + addMemory, + clearMemories, + deleteMemory, + editMemory, + setAnalyzeModel, + setAutoAnalyze, + setMemoryActive +} from '@renderer/store/memory' +import { Topic } from '@renderer/types' +import { Button, Empty, Input, List, message, Modal, Radio, Select, Switch, Tag, Tooltip } from 'antd' +import { FC, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { + SettingContainer, + SettingDivider, + SettingGroup, + SettingHelpText, + SettingRow, + SettingRowTitle, + SettingTitle +} from '..' +import MemoryListManager from './MemoryListManager' +import MemoryMindMap from './MemoryMindMap' +import ShortMemoryManager from './ShortMemoryManager' + +const MemorySettings: FC = () => { + const { t } = useTranslation() + const { theme } = useTheme() + const dispatch = useAppDispatch() + const { analyzeAndAddMemories } = useMemoryService() + + // 从 Redux 获取记忆状态 + const memories = useAppSelector((state) => state.memory?.memories || []) + const memoryLists = useAppSelector((state) => state.memory?.memoryLists || []) + const currentListId = useAppSelector((state) => state.memory?.currentListId || null) + const isActive = useAppSelector((state) => state.memory?.isActive || false) + const autoAnalyze = useAppSelector((state) => state.memory?.autoAnalyze || false) + const analyzeModel = useAppSelector((state) => state.memory?.analyzeModel || null) + + // 从 Redux 获取所有模型,不仅仅是可用的模型 + const providers = useAppSelector((state) => state.llm?.providers || []) + + // 使用 useMemo 缓存模型数组,避免不必要的重新渲染 + const models = useMemo(() => { + // 获取所有模型,不过滤可用性 + return providers.flatMap((provider) => provider.models || []) + }, [providers]) + + // 使用 useMemo 缓存模型选项数组,避免不必要的重新渲染 + const modelOptions = useMemo(() => { + if (models.length > 0) { + return models.map((model) => ({ + label: model.name, + value: model.id + })) + } else { + return [ + // 默认模型选项 + { label: 'GPT-3.5 Turbo', value: 'gpt-3.5-turbo' }, + { label: 'GPT-4', value: 'gpt-4' }, + { label: 'Claude 3 Opus', value: 'claude-3-opus-20240229' }, + { label: 'Claude 3 Sonnet', value: 'claude-3-sonnet-20240229' }, + { label: 'Claude 3 Haiku', value: 'claude-3-haiku-20240307' } + ] + } + }, [models]) + + // 如果没有模型,添加一个默认模型 + useEffect(() => { + if (models.length === 0 && !analyzeModel) { + // 设置一个默认模型 ID + dispatch(setAnalyzeModel('gpt-3.5-turbo')) + } + }, [models, analyzeModel, dispatch]) + + // 获取助手列表,用于话题信息补充 + const assistants = useAppSelector((state) => state.assistants?.assistants || []) + + // 加载所有话题 + useEffect(() => { + const loadTopics = async () => { + try { + // 从数据库获取所有话题 + const allTopics = await TopicManager.getAllTopics() + if (allTopics && allTopics.length > 0) { + // 获取话题的完整信息 + const fullTopics = allTopics.map((dbTopic) => { + // 尝试从 Redux 中找到完整的话题信息 + for (const assistant of assistants) { + if (assistant.topics) { + const topic = assistant.topics.find((t) => t.id === dbTopic.id) + if (topic) return topic + } + } + // 如果找不到,返回一个基本的话题对象 + return { + id: dbTopic.id, + assistantId: '', + name: `话题 ${dbTopic.id.substring(0, 8)}`, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + messages: dbTopic.messages || [] + } + }) + + // 按更新时间排序,最新的在前 + const sortedTopics = fullTopics.sort((a, b) => { + return new Date(b.updatedAt || b.createdAt).getTime() - new Date(a.updatedAt || a.createdAt).getTime() + }) + setTopics(sortedTopics) + } + } catch (error) { + console.error('Failed to load topics:', error) + } + } + + loadTopics() + }, [assistants]) + + // 本地状态 + const [isAddModalVisible, setIsAddModalVisible] = useState(false) + const [isEditModalVisible, setIsEditModalVisible] = useState(false) + const [isClearModalVisible, setIsClearModalVisible] = useState(false) + const [newMemory, setNewMemory] = useState('') + const [editingMemory, setEditingMemory] = useState<{ id: string; content: string } | null>(null) + const [viewMode, setViewMode] = useState<'list' | 'mindmap'>('list') + const [topics, setTopics] = useState([]) + const [selectedTopicId, setSelectedTopicId] = useState('') + const [categoryFilter, setCategoryFilter] = useState(null) + + // 处理添加记忆 + const handleAddMemory = () => { + if (newMemory.trim()) { + dispatch( + addMemory({ + content: newMemory.trim(), + listId: currentListId || undefined + }) + ) + setNewMemory('') + setIsAddModalVisible(false) + message.success(t('settings.memory.addSuccess')) + } + } + + // 处理编辑记忆 + const handleEditMemory = () => { + if (editingMemory && editingMemory.content.trim()) { + dispatch( + editMemory({ + id: editingMemory.id, + content: editingMemory.content.trim() + }) + ) + setEditingMemory(null) + setIsEditModalVisible(false) + message.success(t('settings.memory.editSuccess')) + } + } + + // 处理删除记忆 + const handleDeleteMemory = (id: string) => { + dispatch(deleteMemory(id)) + message.success(t('settings.memory.deleteSuccess')) + } + + // 处理清空记忆 + const handleClearMemories = () => { + dispatch(clearMemories(currentListId || undefined)) + setIsClearModalVisible(false) + message.success(t('settings.memory.clearSuccess')) + } + + // 处理切换记忆功能 + const handleToggleMemory = (checked: boolean) => { + dispatch(setMemoryActive(checked)) + } + + // 处理切换自动分析 + const handleToggleAutoAnalyze = (checked: boolean) => { + dispatch(setAutoAnalyze(checked)) + } + + // 处理选择分析模型 + const handleSelectModel = (modelId: string) => { + dispatch(setAnalyzeModel(modelId)) + } + + // 手动触发分析 + const handleManualAnalyze = () => { + if (isActive && analyzeModel) { + message.info(t('settings.memory.startingAnalysis') || '开始分析...') + // 如果选择了话题,则分析选定的话题,否则分析当前话题 + analyzeAndAddMemories(selectedTopicId || undefined) + } else { + message.warning(t('settings.memory.cannotAnalyze') || '无法分析,请检查设置') + } + } + + return ( + + + {t('settings.memory.title')} + {t('settings.memory.description')} + + + {/* 记忆功能开关 */} + + {t('settings.memory.enableMemory')} + + + + {/* 自动分析开关 */} + + {t('settings.memory.enableAutoAnalyze')} + + + + {/* 分析模型选择 */} + {autoAnalyze && isActive && ( + + {t('settings.memory.analyzeModel')} + setSelectedTopicId(value)} + placeholder={t('settings.memory.selectTopicPlaceholder') || '选择要分析的话题'} + allowClear + showSearch + filterOption={(input, option) => (option?.label as string).toLowerCase().includes(input.toLowerCase())} + options={topics.map((topic) => ({ + label: topic.name || `话题 ${topic.id.substring(0, 8)}`, + value: topic.id + }))} + popupMatchSelectWidth={false} + /> + + )} + + {/* 手动分析按钮 */} + {isActive && ( + + {t('settings.memory.manualAnalyze') || '手动分析'} + + + )} + + + + {/* 短记忆管理器 */} + + + + + {/* 记忆列表管理器 */} + { + // 当选择了一个记忆列表时,重置分类筛选器 + setCategoryFilter(null) + }} + /> + + + + {/* 记忆列表标题和操作按钮 */} + + {t('settings.memory.memoriesList')} + + setViewMode(e.target.value)} + buttonStyle="solid" + style={{ marginRight: 16 }}> + + {t('settings.memory.listView')} + + + {t('settings.memory.mindmapView')} + + + + + + + + {/* 分类筛选器 */} + {memories.length > 0 && ( + + {t('settings.memory.filterByCategory') || '按分类筛选:'} +
+ setCategoryFilter(null)}> + {t('settings.memory.allCategories') || '全部'} + + {Array.from(new Set(memories.filter((m) => m.category).map((m) => m.category))).map((category) => ( + setCategoryFilter(category || null)}> + {category || t('settings.memory.uncategorized') || '未分类'} + + ))} +
+
+ )} + + {/* 记忆列表 */} + + {viewMode === 'list' ? ( + memories.length > 0 ? ( + (currentListId ? memory.listId === currentListId : true)) + .filter((memory) => categoryFilter === null || memory.category === categoryFilter)} + renderItem={(memory) => ( + + + + + + + + {shortMemories.length > 0 ? ( + ( + + + + + ) + } + + // 切换折叠状态 + const toggleExpand = () => { + setIsExpanded(!isExpanded) + } + + return ( + + + + {title || t(`${translationPrefix}.title`)} + {isExpanded ? : } + + + + {isExpanded && ( + + {description || t(`${translationPrefix}.description`)} + + + {!isShortMemory ? ( +
+ {t(`${translationPrefix}.selectList`)} + +
+ ) : ( +
+ {t(`${translationPrefix}.selectTopic`) || '选择话题'} + +
+ )} + +
+ + {t(`${translationPrefix}.similarityThreshold`)}: {threshold} + + +
+
+ + + + + + + + {isLoading ? ( + + + {t(`${translationPrefix}.analyzing`)} + + ) : ( + {renderResult()} + )} +
+ )} +
+ ) +} + +const StyledCard = styled(Card)` + margin-bottom: 24px; + border-radius: 8px; + overflow: hidden; +` + +const CollapsibleHeader = styled.div` + cursor: pointer; + padding: 12px 16px; + background-color: var(--color-background-secondary, #f5f5f5); + border-bottom: 1px solid var(--color-border, #e8e8e8); + transition: background-color 0.3s; + + &:hover { + background-color: var(--color-background-hover, #e6f7ff); + } +` + +const HeaderContent = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +` + +const CollapsibleContent = styled.div` + padding: 16px; +` + +const ControlsContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: 24px; + margin-bottom: 16px; +` + +const ButtonContainer = styled.div` + display: flex; + gap: 8px; + margin-bottom: 24px; +` + +const LoadingContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 0; + gap: 16px; +` + +const ResultContainer = styled.div` + margin-top: 16px; +` + +// ApplyButtonContainer seems unused, removing it. +// const ApplyButtonContainer = styled.div` +// margin-top: 16px; +// text-align: center; +// ` + +const Select = styled.select` + display: block; + width: 100%; + margin-top: 8px; + padding: 8px; + border: 1px solid var(--color-border); + border-radius: 4px; + background-color: var(--color-background); + color: var(--color-text); +` + +export default MemoryDeduplicationPanel diff --git a/src/renderer/src/pages/settings/MemorySettings/MemoryNode.tsx b/src/renderer/src/pages/settings/MemorySettings/MemoryNode.tsx index 82aa08d553..e285843948 100644 --- a/src/renderer/src/pages/settings/MemorySettings/MemoryNode.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/MemoryNode.tsx @@ -1,19 +1,19 @@ -import { DeleteOutlined, EditOutlined, TagOutlined } from '@ant-design/icons'; -import { Memory } from '@renderer/store/memory'; -import { Button, Card, Tag, Tooltip, Typography } from 'antd'; -import { Handle, Position } from '@xyflow/react'; -import styled from 'styled-components'; +import { DeleteOutlined, EditOutlined, TagOutlined } from '@ant-design/icons' +import { Memory } from '@renderer/store/memory' +import { Handle, Position } from '@xyflow/react' +import { Button, Card, Tag, Tooltip, Typography } from 'antd' +import styled from 'styled-components' interface MemoryNodeProps { data: { - memory: Memory; - onEdit: (id: string) => void; - onDelete: (id: string) => void; - }; + memory: Memory + onEdit: (id: string) => void + onDelete: (id: string) => void + } } const MemoryNode: React.FC = ({ data }) => { - const { memory, onEdit, onDelete } = data; + const { memory, onEdit, onDelete } = data return ( @@ -35,43 +35,31 @@ const MemoryNode: React.FC = ({ data }) => { extra={
-
- } - > + }> {new Date(memory.createdAt).toLocaleString()} {memory.source && {memory.source}}
- ); -}; + ) +} const NodeContainer = styled.div` width: 220px; -`; +` const MemoryMeta = styled.div` display: flex; flex-direction: column; font-size: 12px; color: var(--color-text-secondary); -`; +` -export default MemoryNode; +export default MemoryNode diff --git a/src/renderer/src/pages/settings/MemorySettings/index.tsx b/src/renderer/src/pages/settings/MemorySettings/index.tsx index 3df80dea12..61b0177d2b 100644 --- a/src/renderer/src/pages/settings/MemorySettings/index.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/index.tsx @@ -8,7 +8,7 @@ import { } from '@ant-design/icons' import { useTheme } from '@renderer/context/ThemeProvider' import { TopicManager } from '@renderer/hooks/useTopic' -import { useMemoryService } from '@renderer/services/MemoryService' +import { analyzeAndAddShortMemories, useMemoryService } from '@renderer/services/MemoryService' import { useAppDispatch, useAppSelector } from '@renderer/store' import { addMemory, @@ -16,12 +16,14 @@ import { deleteMemory, editMemory, setAnalyzeModel, + setAnalyzing, setAutoAnalyze, - setMemoryActive + setMemoryActive, + setShortMemoryAnalyzeModel } from '@renderer/store/memory' import { Topic } from '@renderer/types' -import { Button, Empty, Input, List, message, Modal, Radio, Select, Switch, Tag, Tooltip } from 'antd' -import { FC, useEffect, useMemo, useState } from 'react' +import { Button, Empty, Input, List, message, Modal, Radio, Select, Switch, Tabs, Tag, Tooltip } from 'antd' +import { FC, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -34,9 +36,10 @@ import { SettingRowTitle, SettingTitle } from '..' +import CollapsibleShortMemoryManager from './CollapsibleShortMemoryManager' +import MemoryDeduplicationPanel from './MemoryDeduplicationPanel' import MemoryListManager from './MemoryListManager' import MemoryMindMap from './MemoryMindMap' -import ShortMemoryManager from './ShortMemoryManager' const MemorySettings: FC = () => { const { t } = useTranslation() @@ -51,6 +54,8 @@ const MemorySettings: FC = () => { const isActive = useAppSelector((state) => state.memory?.isActive || false) const autoAnalyze = useAppSelector((state) => state.memory?.autoAnalyze || false) const analyzeModel = useAppSelector((state) => state.memory?.analyzeModel || null) + const shortMemoryAnalyzeModel = useAppSelector((state) => state.memory?.shortMemoryAnalyzeModel || null) + const isAnalyzing = useAppSelector((state) => state.memory?.isAnalyzing || false) // 从 Redux 获取所有模型,不仅仅是可用的模型 const providers = useAppSelector((state) => state.llm?.providers || []) @@ -196,233 +201,561 @@ const MemorySettings: FC = () => { dispatch(setAutoAnalyze(checked)) } - // 处理选择分析模型 + // 处理选择长期记忆分析模型 const handleSelectModel = (modelId: string) => { dispatch(setAnalyzeModel(modelId)) } + // 处理选择短期记忆分析模型 + const handleSelectShortMemoryModel = (modelId: string) => { + dispatch(setShortMemoryAnalyzeModel(modelId)) + } + // 手动触发分析 - const handleManualAnalyze = () => { - if (isActive && analyzeModel) { - message.info(t('settings.memory.startingAnalysis') || '开始分析...') - // 如果选择了话题,则分析选定的话题,否则分析当前话题 - analyzeAndAddMemories(selectedTopicId || undefined) - } else { + const handleManualAnalyze = async (isShortMemory: boolean = false) => { + if (!isActive) { message.warning(t('settings.memory.cannotAnalyze') || '无法分析,请检查设置') + return + } + + // 如果没有选择话题,提示用户 + if (!selectedTopicId) { + message.warning(t('settings.memory.selectTopicFirst') || '请先选择要分析的话题') + return + } + + message.info(t('settings.memory.startingAnalysis') || '开始分析...') + + if (isShortMemory) { + // 短期记忆分析 + if (!shortMemoryAnalyzeModel) { + message.warning(t('settings.memory.noShortMemoryModel') || '未设置短期记忆分析模型') + return + } + + try { + // 调用短期记忆分析函数 + const result = await analyzeAndAddShortMemories(selectedTopicId) + + if (result) { + message.success(t('settings.memory.shortMemoryAnalysisSuccess') || '短期记忆分析成功') + } else { + message.info(t('settings.memory.shortMemoryAnalysisNoNew') || '未发现新的短期记忆') + } + } catch (error) { + console.error('Failed to analyze short memories:', error) + message.error(t('settings.memory.shortMemoryAnalysisError') || '短期记忆分析失败') + } + } else { + // 长期记忆分析 + if (!analyzeModel) { + message.warning(t('settings.memory.noAnalyzeModel') || '未设置长期记忆分析模型') + return + } + + // 调用长期记忆分析函数 + analyzeAndAddMemories(selectedTopicId) } } + // 重置分析状态 + const handleResetAnalyzingState = () => { + dispatch(setAnalyzing(false)) + message.success(t('settings.memory.resetAnalyzingState') || '分析状态已重置') + } + + // 添加滚动检测 + const containerRef = useRef(null) + const listContainerRef = useRef(null) + + // 检测滚动状态并添加类 + useEffect(() => { + const container = containerRef.current + const listContainer = listContainerRef.current + if (!container || !listContainer) return + + const checkMainScroll = () => { + if (container.scrollHeight > container.clientHeight) { + container.classList.add('scrollable') + } else { + container.classList.remove('scrollable') + } + } + + const checkListScroll = () => { + if (listContainer.scrollHeight > listContainer.clientHeight) { + listContainer.classList.add('scrollable') + } else { + listContainer.classList.remove('scrollable') + } + } + + // 初始检查 + checkMainScroll() + checkListScroll() + + // 监听窗口大小变化 + window.addEventListener('resize', () => { + checkMainScroll() + checkListScroll() + }) + + // 监听内容变化(使用MutationObserver) + const mainObserver = new MutationObserver(checkMainScroll) + mainObserver.observe(container, { childList: true, subtree: true }) + + const listObserver = new MutationObserver(checkListScroll) + listObserver.observe(listContainer, { childList: true, subtree: true }) + + // 主容器始终保持可滚动状态 + container.style.overflowY = 'auto' + + // 添加滚动指示器 + const addScrollIndicator = () => { + const scrollIndicator = document.createElement('div') + scrollIndicator.className = 'scroll-indicator' + scrollIndicator.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + width: 40px; + height: 40px; + border-radius: 50%; + background-color: var(--color-primary); + opacity: 0.7; + pointer-events: none; + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + transition: opacity 0.3s ease; + ` + + // 添加箭头图标 + scrollIndicator.innerHTML = `` + + document.body.appendChild(scrollIndicator) + + // 2秒后淡出 + setTimeout(() => { + scrollIndicator.style.opacity = '0' + setTimeout(() => { + document.body.removeChild(scrollIndicator) + }, 300) + }, 2000) + } + + // 首次加载时显示滚动指示器 + if (container.scrollHeight > container.clientHeight) { + addScrollIndicator() + } + + // 添加滚动事件监听器,当用户滚动时显示滚动指示器 + let scrollTimeout: NodeJS.Timeout | null = null + const handleContainerScroll = () => { + // 清除之前的定时器 + if (scrollTimeout) { + clearTimeout(scrollTimeout) + } + + // 如果容器可滚动,显示滚动指示器 + if (container.scrollHeight > container.clientHeight) { + // 如果已经滚动到底部,不显示指示器 + if (container.scrollHeight - container.scrollTop - container.clientHeight > 20) { + // 设置定时器,延迟显示滚动指示器 + scrollTimeout = setTimeout(() => { + addScrollIndicator() + }, 500) + } + } + } + + container.addEventListener('scroll', handleContainerScroll) + + return () => { + window.removeEventListener('resize', checkMainScroll) + mainObserver.disconnect() + listObserver.disconnect() + // 移除滚动事件监听器 + container.removeEventListener('scroll', handleContainerScroll) + // 清除定时器 + if (scrollTimeout) { + clearTimeout(scrollTimeout) + } + } + }, []) + return ( - - - {t('settings.memory.title')} - {t('settings.memory.description')} - + + {/* 1. 将 TabsContainer 移到 SettingContainer 顶部 */} + + + + {t('settings.memory.shortMemory') || '短期记忆'} + + ), + children: ( + // 将原来...中的内容放在这里 + + {t('settings.memory.title')} + {t('settings.memory.description')} + - {/* 记忆功能开关 */} - - {t('settings.memory.enableMemory')} - - + {t('settings.memory.shortMemorySettings')} + {t('settings.memory.shortMemoryDescription')} + - {/* 自动分析开关 */} - - {t('settings.memory.enableAutoAnalyze')} - - + {/* 保留原有的短期记忆设置 */} + + {t('settings.memory.enableMemory')} + + + + {t('settings.memory.enableAutoAnalyze')} + + - {/* 分析模型选择 */} - {autoAnalyze && isActive && ( - - {t('settings.memory.analyzeModel')} - + + )} - {/* 话题选择 */} - {isActive && ( - - {t('settings.memory.selectTopic') || '选择话题'} - setSelectedTopicId(value)} + placeholder={t('settings.memory.selectTopicPlaceholder') || '选择要分析的话题'} + allowClear + showSearch + filterOption={(input, option) => + (option?.label as string).toLowerCase().includes(input.toLowerCase()) + } + options={topics.map((topic) => ({ + label: topic.name || `话题 ${topic.id.substring(0, 8)}`, + value: topic.id + }))} + popupMatchSelectWidth={false} + /> + + )} - {/* 手动分析按钮 */} - {isActive && ( - - {t('settings.memory.manualAnalyze') || '手动分析'} - - - )} - - - - {/* 短记忆管理器 */} - - - - - {/* 记忆列表管理器 */} - { - // 当选择了一个记忆列表时,重置分类筛选器 - setCategoryFilter(null) - }} - /> - - - - {/* 记忆列表标题和操作按钮 */} - - {t('settings.memory.memoriesList')} - - setViewMode(e.target.value)} - buttonStyle="solid" - style={{ marginRight: 16 }}> - - {t('settings.memory.listView')} - - - {t('settings.memory.mindmapView')} - - - - - - - - {/* 分类筛选器 */} - {memories.length > 0 && ( - - {t('settings.memory.filterByCategory') || '按分类筛选:'} -
- setCategoryFilter(null)}> - {t('settings.memory.allCategories') || '全部'} - - {Array.from(new Set(memories.filter((m) => m.category).map((m) => m.category))).map((category) => ( - setCategoryFilter(category || null)}> - {category || t('settings.memory.uncategorized') || '未分类'} - - ))} -
-
- )} - - {/* 记忆列表 */} - - {viewMode === 'list' ? ( - memories.length > 0 ? ( - (currentListId ? memory.listId === currentListId : true)) - .filter((memory) => categoryFilter === null || memory.category === categoryFilter)} - renderItem={(memory) => ( - + {/* 手动分析按钮 */} + {isActive && ( + + {t('settings.memory.manualAnalyze') || '手动分析'} + + {isAnalyzing && ( + + )} + + + )} + + + + {/* 短期记忆去重与合并面板 */} + + + + + {/* 短记忆管理器 */} + +
+ ) + }, + { + key: 'longMemory', + label: ( + + + {t('settings.memory.longMemory') || '长期记忆'} + + ), + children: ( + // 将原来...中的内容放在这里 + + {t('settings.memory.title')} + {t('settings.memory.description')} + + + {t('settings.memory.longMemorySettings')} + {t('settings.memory.longMemoryDescription')} + + + {/* 保留原有的长期记忆设置 */} + + {t('settings.memory.enableMemory')} + + + + {t('settings.memory.enableAutoAnalyze')} + + + + {/* 长期记忆分析模型选择 */} + {autoAnalyze && isActive && ( + + {t('settings.memory.analyzeModel') || '长期记忆分析模型'} + setSelectedTopicId(value)} + placeholder={t('settings.memory.selectTopicPlaceholder') || '选择要分析的话题'} + allowClear + showSearch + filterOption={(input, option) => + (option?.label as string).toLowerCase().includes(input.toLowerCase()) + } + options={topics.map((topic) => ({ + label: topic.name || `话题 ${topic.id.substring(0, 8)}`, + value: topic.id + }))} + popupMatchSelectWidth={false} + /> + + )} + + {/* 手动分析按钮 */} + {isActive && ( + + {t('settings.memory.manualAnalyze') || '手动分析'} + + {isAnalyzing && ( + + )} + + + )} + + + + {/* 记忆列表管理器 */} + { + // 当选择了一个记忆列表时,重置分类筛选器 + setCategoryFilter(null) + }} + // disabled={!isActive} // 移除此属性 + /> + + + + {/* 长期记忆去重与合并面板 */} + + + + + {/* 记忆列表标题和操作按钮 */} + + {t('settings.memory.memoriesList')} + + setViewMode(e.target.value)} + buttonStyle="solid" + disabled={!isActive}> + + {t('settings.memory.listView')} + + + {t('settings.memory.mindmapView')} + + + + + + + + {/* 分类筛选器 */} + {memories.length > 0 && isActive && ( + + {t('settings.memory.filterByCategory') || '按分类筛选:'} +
+ setCategoryFilter(null)}> + {t('settings.memory.allCategories') || '全部'} + + {Array.from(new Set(memories.filter((m) => m.category).map((m) => m.category))).map( + (category) => ( + setCategoryFilter(category || null)}> + {category || t('settings.memory.uncategorized') || '未分类'} + + ) + )} +
+
+ )} + + {/* 记忆列表 */} + + {viewMode === 'list' ? ( + memories.length > 0 && isActive ? ( + (currentListId ? memory.listId === currentListId : true)) + .filter((memory) => categoryFilter === null || memory.category === categoryFilter)} + renderItem={(memory) => ( + + + +
+ ) +} + +export default PriorityManagementSettings diff --git a/src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx b/src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx index cf57ba3f1a..211c40f404 100644 --- a/src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx @@ -1,13 +1,13 @@ -import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons' +import { DeleteOutlined } from '@ant-design/icons' import { addShortMemoryItem } from '@renderer/services/MemoryService' import { useAppDispatch, useAppSelector } from '@renderer/store' import { deleteShortMemory, setShortMemoryActive } from '@renderer/store/memory' -import { Button, Empty, Input, List, Modal, Switch, Tooltip, Typography } from 'antd' +import { Button, Empty, Input, List, Switch, Tooltip, Typography } from 'antd' import { useState } from 'react' import { useTranslation } from 'react-i18next' const { Title } = Typography -const { confirm } = Modal +// 不再需要确认对话框 const ShortMemoryManager = () => { const { t } = useTranslation() @@ -40,16 +40,10 @@ const ShortMemoryManager = () => { } } - // 删除短记忆 + // 删除短记忆 - 直接删除无需确认 const handleDeleteMemory = (id: string) => { - confirm({ - title: t('settings.memory.confirmDelete'), - icon: , - content: t('settings.memory.confirmDeleteContent'), - onOk() { - dispatch(deleteShortMemory(id)) - } - }) + // 直接删除记忆,无需确认对话框 + dispatch(deleteShortMemory(id)) } return ( diff --git a/src/renderer/src/pages/settings/MemorySettings/index.tsx b/src/renderer/src/pages/settings/MemorySettings/index.tsx index 61b0177d2b..2458bec4fa 100644 --- a/src/renderer/src/pages/settings/MemorySettings/index.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/index.tsx @@ -40,6 +40,7 @@ import CollapsibleShortMemoryManager from './CollapsibleShortMemoryManager' import MemoryDeduplicationPanel from './MemoryDeduplicationPanel' import MemoryListManager from './MemoryListManager' import MemoryMindMap from './MemoryMindMap' +import PriorityManagementSettings from './PriorityManagementSettings' const MemorySettings: FC = () => { const { t } = useTranslation() @@ -62,19 +63,67 @@ const MemorySettings: FC = () => { // 使用 useMemo 缓存模型数组,避免不必要的重新渲染 const models = useMemo(() => { - // 获取所有模型,不过滤可用性 - return providers.flatMap((provider) => provider.models || []) + // 只获取已启用的提供商的模型 + return providers + .filter(provider => provider.enabled) // 只保留已启用的提供商 + .flatMap((provider) => provider.models || []) }, [providers]) // 使用 useMemo 缓存模型选项数组,避免不必要的重新渲染 const modelOptions = useMemo(() => { if (models.length > 0) { - return models.map((model) => ({ - label: model.name, - value: model.id + // 按提供商分组模型 + const modelsByProvider = models.reduce( + (acc, model) => { + const provider = providers.find((p) => p.models.some((m) => m.id === model.id)) + const providerName = provider ? (provider.isSystem ? t(`provider.${provider.id}`) : provider.name) : '' + + if (!acc[providerName]) { + acc[providerName] = [] + } + + // 检查是否已经存在相同的模型,避免重复 + const isDuplicate = acc[providerName].some((m) => m.value === model.id) + if (!isDuplicate) { + acc[providerName].push({ + label: `${model.name}`, + value: model.id + }) + } + + return acc + }, + {} as Record + ) + + // 转换为Select组件的options格式 + const groupedOptions = Object.entries(modelsByProvider).map(([provider, models]) => ({ + label: provider, + options: models })) + + // 将分组选项展平为单个选项数组,以兼容现有代码 + const flatOptions = models.reduce( + (acc, model) => { + // 检查是否已经存在相同的模型,避免重复 + const isDuplicate = acc.some((m) => m.value === model.id) + if (!isDuplicate) { + acc.push({ + label: model.name, + value: model.id + }) + } + return acc + }, + [] as { label: string; value: string }[] + ) + + return { + groupedOptions, + flatOptions + } } else { - return [ + const defaultOptions = [ // 默认模型选项 { label: 'GPT-3.5 Turbo', value: 'gpt-3.5-turbo' }, { label: 'GPT-4', value: 'gpt-4' }, @@ -82,8 +131,12 @@ const MemorySettings: FC = () => { { label: 'Claude 3 Sonnet', value: 'claude-3-sonnet-20240229' }, { label: 'Claude 3 Haiku', value: 'claude-3-haiku-20240307' } ] + return { + groupedOptions: [], + flatOptions: defaultOptions + } } - }, [models]) + }, [models, providers, t]) // 如果没有模型,添加一个默认模型 useEffect(() => { @@ -435,8 +488,10 @@ const MemorySettings: FC = () => { value={shortMemoryAnalyzeModel} onChange={handleSelectShortMemoryModel} placeholder={t('settings.memory.selectModel') || '选择模型'} - options={modelOptions} + options={modelOptions.groupedOptions} disabled={!isActive || !autoAnalyze} // 确保在未激活或未开启自动分析时禁用 + optionFilterProp="label" + listHeight={300} /> )} @@ -505,6 +560,20 @@ const MemorySettings: FC = () => { ) }, + { + key: 'priorityManagement', + label: ( + + + {t('settings.memory.priorityManagement.title') || '智能优先级管理'} + + ), + children: ( + + + + ) + }, { key: 'longMemory', label: ( @@ -543,8 +612,10 @@ const MemorySettings: FC = () => { value={analyzeModel} onChange={handleSelectModel} placeholder={t('settings.memory.selectModel') || '选择模型'} - options={modelOptions} + options={modelOptions.groupedOptions} disabled={!isActive || !autoAnalyze} + optionFilterProp="label" + listHeight={300} /> )} @@ -707,11 +778,7 @@ const MemorySettings: FC = () => { - {memory.category && ( - - {memory.category} - - )} + {memory.category && {memory.category}} {memory.content} } @@ -900,7 +967,7 @@ const TabLabelContainer = styled.span` const TabDot = styled.span<{ color: string }>` font-size: 18px; - color: ${props => props.color}; + color: ${(props) => props.color}; ` const ButtonsContainer = styled.div` diff --git a/src/renderer/src/services/MemoryService.ts b/src/renderer/src/services/MemoryService.ts index 47967abe76..9471406c73 100644 --- a/src/renderer/src/services/MemoryService.ts +++ b/src/renderer/src/services/MemoryService.ts @@ -5,9 +5,73 @@ import { fetchGenerate } from '@renderer/services/ApiService' // Import fetchGen import { useAppDispatch, useAppSelector } from '@renderer/store' // Removed duplicate import: import store from '@renderer/store'; import store from '@renderer/store' // Import store -import { addMemory, addShortMemory, setAnalyzing } from '@renderer/store/memory' +import { + addAnalysisLatency, + addMemory, + addShortMemory, + setAnalyzing, + updateAnalysisStats, + updatePerformanceMetrics, + updateUserInterest, + updateMemoryPriorities, + accessMemory, + Memory +} from '@renderer/store/memory' import { useCallback, useEffect, useRef } from 'react' // Add useRef back +// 计算对话复杂度,用于调整分析深度 +const calculateConversationComplexity = (conversation: string): 'low' | 'medium' | 'high' => { + const wordCount = conversation.split(/\s+/).length + const sentenceCount = conversation.split(/[.!?]+/).length + const avgSentenceLength = wordCount / (sentenceCount || 1) + + // 简单的复杂度评估算法 + if (wordCount < 100 || avgSentenceLength < 5) { + return 'low' + } else if (wordCount > 500 || avgSentenceLength > 15) { + return 'high' + } else { + return 'medium' + } +} + +// 根据分析深度调整提示词 +const adjustPromptForDepth = (basePrompt: string, depth: 'low' | 'medium' | 'high'): string => { + switch (depth) { + case 'low': + // 简化提示词,减少分析要求 + return basePrompt + .replace(/\u8be6\u7ec6\u5206\u6790/g, '\u7b80\u8981\u5206\u6790') + .replace(/\u63d0\u53d6\u51fa\u91cd\u8981\u7684/g, '\u63d0\u53d6\u51fa\u6700\u91cd\u8981\u7684') + case 'high': + // 增强提示词,要求更深入的分析 + return ( + basePrompt + + '\n\n\u8bf7\u8fdb\u884c\u66f4\u6df1\u5165\u7684\u5206\u6790\uff0c\u8003\u8651\u9690\u542b\u7684\u7528\u6237\u9700\u6c42\u548c\u504f\u597d\uff0c\u8bc6\u522b\u6f5c\u5728\u7684\u5173\u8054\u4fe1\u606f\u3002' + ) + default: + return basePrompt + } +} + +// 提取用户关注点 +const extractUserInterests = (conversation: string): string[] => { + // 简单实现:提取对话中的关键词或主题 + const topics = new Set() + + // 简单的关键词提取,匹配4个或更多字符的单词 + const keywords = conversation.match(/\b\w{4,}\b/g) || [] + const commonWords = ['this', 'that', 'these', 'those', 'with', 'from', 'have', 'what', 'when', 'where', 'which'] + + keywords.forEach((word) => { + if (!commonWords.includes(word.toLowerCase())) { + topics.add(word.toLowerCase()) + } + }) + + return Array.from(topics) +} + // 分析对话内容并提取重要信息 const analyzeConversation = async ( conversation: string, @@ -204,13 +268,26 @@ export const useMemoryService = () => { } try { + // 性能监控:记录开始时间 + const startTime = performance.now() + dispatch(setAnalyzing(true)) console.log('[Memory Analysis] Starting analysis...') console.log(`[Memory Analysis] Analyzing topic: ${targetTopicId}`) console.log('[Memory Analysis] Conversation length:', newConversation.length) + // 自适应分析:根据对话复杂度调整分析深度 + const conversationComplexity = calculateConversationComplexity(newConversation) + let analysisDepth = memoryState.analysisDepth || 'medium' + + // 如果启用了自适应分析,根据复杂度调整深度 + if (memoryState.adaptiveAnalysisEnabled) { + analysisDepth = conversationComplexity + console.log(`[Memory Analysis] Adjusted analysis depth to ${analysisDepth} based on conversation complexity`) + } + // 构建长期记忆分析提示词,包含已有记忆和新对话 - const prompt = ` + const basePrompt = ` 请分析以下对话内容,提取出重要的用户偏好、习惯、需求和背景信息,这些信息在未来的对话中可能有用。 将每条信息分类并按以下格式返回: @@ -236,12 +313,75 @@ ${existingMemoriesContent} ${newConversation} ` + // 根据分析深度调整提示词 + const adjustedPrompt = adjustPromptForDepth(basePrompt, analysisDepth) + // 调用分析函数,传递自定义提示词 - const memories = await analyzeConversation(newConversation, memoryState.analyzeModel!, prompt) + const memories = await analyzeConversation(newConversation, memoryState.analyzeModel!, adjustedPrompt) + + // 用户关注点学习 + if (memoryState.interestTrackingEnabled) { + const newTopics = extractUserInterests(newConversation) + if (newTopics.length > 0) { + console.log(`[Memory Analysis] Extracted user interests: ${newTopics.join(', ')}`) + + // 更新用户关注点 + const now = new Date().toISOString() + const updatedInterests = [...(memoryState.userInterests || [])] + + // 增加新发现的关注点权重 + newTopics.forEach((topic) => { + const existingIndex = updatedInterests.findIndex((i) => i.topic === topic) + if (existingIndex >= 0) { + // 已存在的关注点,增加权重 + const updatedInterest = { + ...updatedInterests[existingIndex], + weight: Math.min(1, updatedInterests[existingIndex].weight + 0.1), + lastUpdated: now + } + store.dispatch(updateUserInterest(updatedInterest)) + } else { + // 新的关注点 + const newInterest = { + topic, + weight: 0.5, // 初始权重 + lastUpdated: now + } + store.dispatch(updateUserInterest(newInterest)) + } + }) + } + } console.log('[Memory Analysis] Analysis complete. Memories extracted:', memories) // 添加提取的记忆 if (memories && memories.length > 0) { + // 性能监控:记录分析时间 + const endTime = performance.now() + const analysisTime = endTime - startTime + + // 更新分析统计数据 + store.dispatch( + updateAnalysisStats({ + totalAnalyses: (memoryState.analysisStats?.totalAnalyses || 0) + 1, + successfulAnalyses: (memoryState.analysisStats?.successfulAnalyses || 0) + 1, + newMemoriesGenerated: (memoryState.analysisStats?.newMemoriesGenerated || 0) + memories.length, + averageAnalysisTime: memoryState.analysisStats?.totalAnalyses + ? ((memoryState.analysisStats.averageAnalysisTime || 0) * + (memoryState.analysisStats.totalAnalyses || 0) + + analysisTime) / + ((memoryState.analysisStats.totalAnalyses || 0) + 1) + : analysisTime, + lastAnalysisTime: Date.now() + }) + ) + + // 性能监控:记录分析延迟 + try { + store.dispatch(addAnalysisLatency(analysisTime)) + } catch (error) { + console.warn('[Memory Analysis] Failed to add analysis latency:', error) + } // 智能去重:使用AI模型检查语义相似的记忆 const existingMemories = store.getState().memory?.memories || [] @@ -280,8 +420,64 @@ ${newConversation} } console.log(`[Memory Analysis] Processed ${memories.length} potential memories, added ${newMemories.length}.`) + + // 自适应分析:根据分析结果调整分析频率 + if (memoryState.adaptiveAnalysisEnabled) { + // 如果分析成功率低,增加分析频率 + const successRate = + (memoryState.analysisStats?.successfulAnalyses || 0) / + Math.max(1, memoryState.analysisStats?.totalAnalyses || 1) + let newFrequency = memoryState.analysisFrequency || 5 + + if (successRate < 0.3 && newFrequency > 3) { + // 成功率低,减少分析频率(增加消息数阈值) + newFrequency += 1 + console.log( + `[Memory Analysis] Low success rate (${successRate.toFixed(2)}), increasing message threshold to ${newFrequency}` + ) + } else if (successRate > 0.7 && newFrequency > 2) { + // 成功率高,增加分析频率(减少消息数阈值) + newFrequency -= 1 + console.log( + `[Memory Analysis] High success rate (${successRate.toFixed(2)}), decreasing message threshold to ${newFrequency}` + ) + } + } } else { console.log('[Memory Analysis] No new memories extracted.') + + // 更新分析统计数据(分析失败) + const endTime = performance.now() + const analysisTime = endTime - startTime + + store.dispatch( + updateAnalysisStats({ + totalAnalyses: (memoryState.analysisStats?.totalAnalyses || 0) + 1, + lastAnalysisTime: Date.now() + }) + ) + + // 性能监控:记录分析延迟 + try { + store.dispatch(addAnalysisLatency(analysisTime)) + } catch (error) { + console.warn('[Memory Analysis] Failed to add analysis latency:', error) + } + } + + // 性能监控:更新性能指标 + if (memoryState.monitoringEnabled) { + try { + store.dispatch( + updatePerformanceMetrics({ + memoryCount: store.getState().memory?.memories.length || 0, + shortMemoryCount: store.getState().memory?.shortMemories.length || 0, + lastPerformanceCheck: Date.now() + }) + ) + } catch (error) { + console.warn('[Memory Analysis] Failed to update performance metrics:', error) + } } } catch (error) { console.error('Failed to analyze and add memories:', error) @@ -302,7 +498,37 @@ ${newConversation} analyzeAndAddMemoriesRef.current = analyzeAndAddMemories }, [analyzeAndAddMemories]) + + + // 记录记忆访问 + const recordMemoryAccess = useCallback((memoryId: string, isShortMemory: boolean = false) => { + store.dispatch(accessMemory({ id: memoryId, isShortMemory })) + }, []) + // Effect 来设置/清除定时器,只依赖于启动条件 + useEffect(() => { + // 定期更新记忆优先级 + const priorityUpdateInterval = setInterval(() => { + const memoryState = store.getState().memory + if (!memoryState?.priorityManagementEnabled) return + + // 检查上次更新时间,避免频繁更新 + const now = Date.now() + const lastUpdate = memoryState.lastPriorityUpdate || 0 + const updateInterval = 30 * 60 * 1000 // 30分钟更新一次 + + if (now - lastUpdate < updateInterval) return + + console.log('[Memory Priority] Updating memory priorities and freshness...') + store.dispatch(updateMemoryPriorities()) + }, 10 * 60 * 1000) // 每10分钟检查一次 + + return () => { + clearInterval(priorityUpdateInterval) + } + }, []) + + // Effect 来设置/清除分析定时器,只依赖于启动条件 useEffect(() => { if (!isActive || !autoAnalyze || !analyzeModel) { console.log('[Memory Analysis Timer] Conditions not met for setting up timer:', { @@ -332,8 +558,8 @@ ${newConversation} // 依赖项只包含决定是否启动定时器的设置 }, [isActive, autoAnalyze, analyzeModel]) - // 返回分析函数,以便在MemoryProvider中使用 - return { analyzeAndAddMemories } + // 返回分析函数和记忆访问函数,以便在其他组件中使用 + return { analyzeAndAddMemories, recordMemoryAccess } } // 手动添加短记忆 @@ -547,12 +773,13 @@ export const applyMemoriesToPrompt = (systemPrompt: string): string => { const state = store.getState() // Use imported store // 确保 state.memory 存在,如果不存在则提供默认值 - const { isActive, memories, memoryLists, shortMemoryActive, shortMemories } = state.memory || { + const { isActive, memories, memoryLists, shortMemoryActive, shortMemories, priorityManagementEnabled } = state.memory || { isActive: false, memories: [], memoryLists: [], shortMemoryActive: false, - shortMemories: [] + shortMemories: [], + priorityManagementEnabled: false } // 获取当前话题ID @@ -564,7 +791,8 @@ export const applyMemoriesToPrompt = (systemPrompt: string): string => { listsCount: memoryLists?.length, shortMemoryActive, shortMemoriesCount: shortMemories?.length, - currentTopicId + currentTopicId, + priorityManagementEnabled }) let result = systemPrompt @@ -573,7 +801,34 @@ export const applyMemoriesToPrompt = (systemPrompt: string): string => { // 处理短记忆 if (shortMemoryActive && shortMemories && shortMemories.length > 0 && currentTopicId) { // 获取当前话题的短记忆 - const topicShortMemories = shortMemories.filter((memory) => memory.topicId === currentTopicId) + let topicShortMemories = shortMemories.filter((memory) => memory.topicId === currentTopicId) + + // 如果启用了智能优先级管理,根据优先级排序 + if (priorityManagementEnabled && topicShortMemories.length > 0) { + // 计算每个记忆的综合分数(重要性 * 衰减因子 * 鲜度) + const scoredMemories = topicShortMemories.map(memory => { + // 记录访问 + store.dispatch(accessMemory({ id: memory.id, isShortMemory: true })) + + // 计算综合分数 + const importance = memory.importance || 0.5 + const decayFactor = memory.decayFactor || 1 + const freshness = memory.freshness || 0.5 + const score = importance * decayFactor * (freshness * 2) // 短期记忆更注重鲜度 + return { memory, score } + }) + + // 按综合分数降序排序 + scoredMemories.sort((a, b) => b.score - a.score) + + // 提取排序后的记忆 + topicShortMemories = scoredMemories.map(item => item.memory) + + // 限制数量,避免提示词过长 + if (topicShortMemories.length > 10) { + topicShortMemories = topicShortMemories.slice(0, 10) + } + } if (topicShortMemories.length > 0) { const shortMemoryPrompt = topicShortMemories.map((memory) => `- ${memory.content}`).join('\n') @@ -592,7 +847,45 @@ export const applyMemoriesToPrompt = (systemPrompt: string): string => { if (activeListIds.length > 0) { // 只获取激活列表中的记忆 - const activeMemories = memories.filter((memory) => activeListIds.includes(memory.listId)) + let activeMemories = memories.filter((memory) => activeListIds.includes(memory.listId)) + + // 如果启用了智能优先级管理,根据优先级排序 + if (priorityManagementEnabled && activeMemories.length > 0) { + // 计算每个记忆的综合分数 + const scoredMemories = activeMemories.map(memory => { + // 记录访问 + store.dispatch(accessMemory({ id: memory.id })) + + // 计算综合分数 + const importance = memory.importance || 0.5 + const decayFactor = memory.decayFactor || 1 + const freshness = memory.freshness || 0.5 + const score = importance * decayFactor * freshness + return { memory, score } + }) + + // 按综合分数降序排序 + scoredMemories.sort((a, b) => b.score - a.score) + + // 限制每个列表的记忆数量 + const maxMemoriesPerList = 5 + const memoriesByList: Record = {} + + // 提取排序后的记忆 + const sortedMemories = scoredMemories.map(item => item.memory) + + sortedMemories.forEach(memory => { + if (!memoriesByList[memory.listId]) { + memoriesByList[memory.listId] = [] + } + if (memoriesByList[memory.listId].length < maxMemoriesPerList) { + memoriesByList[memory.listId].push(memory) + } + }) + + // 重新构建活跃记忆列表 + activeMemories = Object.values(memoriesByList).flat() as Memory[] + } if (activeMemories.length > 0) { // 按列表分组构建记忆提示词 diff --git a/src/renderer/src/services/VectorService.ts b/src/renderer/src/services/VectorService.ts new file mode 100644 index 0000000000..c874f470b4 --- /dev/null +++ b/src/renderer/src/services/VectorService.ts @@ -0,0 +1,260 @@ +// src/renderer/src/services/VectorService.ts + +// 导入Memory和ShortMemory接口 +interface Memory { + id: string + content: string + createdAt: string + source?: string + category?: string + listId: string + analyzedMessageIds?: string[] + lastMessageId?: string + topicId?: string + vectorRepresentation?: number[] + entities?: string[] + keywords?: string[] + importance?: number + accessCount?: number + lastAccessedAt?: string +} + +interface ShortMemory { + id: string + content: string + createdAt: string + topicId: string + analyzedMessageIds?: string[] + lastMessageId?: string + vectorRepresentation?: number[] + entities?: string[] + keywords?: string[] + importance?: number +} +// TODO: Import necessary API clients or libraries for vector embedding (e.g., OpenAI) + +/** + * 计算两个向量之间的余弦相似度 + * @param vecA - 第一个向量 + * @param vecB - 第二个向量 + * @returns 余弦相似度值 (-1 到 1) + */ +function cosineSimilarity(vecA: number[], vecB: number[]): number { + if (!vecA || !vecB || vecA.length !== vecB.length || vecA.length === 0) { + // console.error('Invalid vectors for cosine similarity calculation.', vecA, vecB) + return 0 // 或者抛出错误,取决于错误处理策略 + } + + let dotProduct = 0.0 + let normA = 0.0 + let normB = 0.0 + for (let i = 0; i < vecA.length; i++) { + dotProduct += vecA[i] * vecB[i] + normA += vecA[i] * vecA[i] + normB += vecB[i] * vecB[i] + } + + if (normA === 0 || normB === 0) { + // console.warn('Zero vector encountered in cosine similarity calculation.') + return 0 // 避免除以零 + } + + return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)) +} + +// 简单的内存缓存来存储向量表示 +const vectorCache = new Map() + +/** + * VectorService 类负责处理记忆内容的向量化和相似度计算 + */ +class VectorService { + /** + * 获取给定文本的向量表示。 + * 优先从缓存获取,否则调用API生成。 + * @param text - 需要向量化的文本 + * @param modelId - 使用的向量化模型ID (TODO: 需要从设置或状态中获取) + * @returns 文本的向量表示 (number[]) 或 null (如果失败) + */ + async getVector(text: string, modelId: string = 'text-embedding-ada-002'): Promise { + if (!text || text.trim() === '') { + return null + } + + const cacheKey = `${modelId}:${text}` + if (vectorCache.has(cacheKey)) { + return vectorCache.get(cacheKey)! + } + + try { + // TODO: 实现调用向量化API的逻辑 + console.log(`[VectorService] Requesting vector for text (length: ${text.length})...`) + // 示例: const response = await openai.embeddings.create({ model: modelId, input: text }); + // const vector = response?.data?.[0]?.embedding; + + // --- 占位符逻辑 --- + // 实际应调用 API 获取向量 + // 这里生成一个随机向量作为占位符,维度需与模型一致 + const placeholderVector = Array.from({ length: 1536 }, () => Math.random() * 2 - 1) // 假设 ada-002 是 1536 维 + const vector = placeholderVector + // --- 占位符结束 --- + + if (vector) { + vectorCache.set(cacheKey, vector) + console.log(`[VectorService] Vector obtained and cached for text (length: ${text.length}).`) + return vector + } else { + console.error('[VectorService] Failed to get vector embedding.') + return null + } + } catch (error) { + console.error('[VectorService] Error getting vector embedding:', error) + return null + } + } + + /** + * 确保一个记忆项具有向量表示。 + * 如果没有,则尝试生成并更新。 + * @param memory - 记忆项 (Memory 或 ShortMemory) + * @returns 更新后的记忆项 (如果成功生成向量) 或原记忆项 + */ + async ensureVectorRepresentation(memory: Memory | ShortMemory): Promise { + if (memory.vectorRepresentation && memory.vectorRepresentation.length > 0) { + return memory // 已经有向量了 + } + + // 从状态或设置中获取 vectorizeModel + const vectorizeModel = 'text-embedding-ada-002' // 暂时硬编码 + const vector = await this.getVector(memory.content, vectorizeModel) + + if (vector) { + return { ...memory, vectorRepresentation: vector } + } + + return memory // 无法生成向量,返回原样 + } + + /** + * 计算两个记忆项之间的语义相似度。 + * @param memoryA - 第一个记忆项 + * @param memoryB - 第二个记忆项 + * @returns 相似度分数 (0 到 1) 或 0 (如果无法计算) + */ + async calculateSimilarity(memoryA: Memory | ShortMemory, memoryB: Memory | ShortMemory): Promise { + try { + const memoryAWithVector = await this.ensureVectorRepresentation(memoryA) + const memoryBWithVector = await this.ensureVectorRepresentation(memoryB) + + if ( + memoryAWithVector.vectorRepresentation && + memoryBWithVector.vectorRepresentation && + memoryAWithVector.vectorRepresentation.length > 0 && + memoryBWithVector.vectorRepresentation.length > 0 + ) { + const similarity = cosineSimilarity( + memoryAWithVector.vectorRepresentation, + memoryBWithVector.vectorRepresentation + ) + // 将余弦相似度 (-1 到 1) 映射到 0 到 1 范围 (可选,但通常更直观) + return (similarity + 1) / 2 + } else { + // console.warn('[VectorService] Could not calculate similarity due to missing vectors.') + return 0 + } + } catch (error) { + console.error('[VectorService] Error calculating similarity:', error) + return 0 + } + } + + /** + * 查找与给定记忆最相似的记忆项列表。 + * @param targetMemory - 目标记忆项 + * @param candidates - 候选记忆项列表 + * @param topN - 返回最相似的 N 个结果 + * @param threshold - 相似度阈值 (0 到 1) + * @returns 最相似的记忆项列表及其相似度分数 + */ + async findSimilarMemories( + targetMemory: Memory | ShortMemory, + candidates: (Memory | ShortMemory)[], + topN: number = 5, + threshold: number = 0.7 // 默认阈值 + ): Promise<{ memory: Memory | ShortMemory; similarity: number }[]> { + const targetMemoryWithVector = await this.ensureVectorRepresentation(targetMemory) + + if (!targetMemoryWithVector.vectorRepresentation || targetMemoryWithVector.vectorRepresentation.length === 0) { + console.warn('[VectorService] Target memory has no vector representation. Cannot find similar memories.') + return [] + } + + const results: { memory: Memory | ShortMemory; similarity: number }[] = [] + + for (const candidate of candidates) { + // 排除目标记忆自身 + if (candidate.id === targetMemory.id) { + continue + } + + const similarity = await this.calculateSimilarity(targetMemoryWithVector, candidate) + if (similarity >= threshold) { + results.push({ memory: candidate, similarity }) + } + } + + // 按相似度降序排序 + results.sort((a, b) => b.similarity - a.similarity) + + // 返回前 N 个结果 + return results.slice(0, topN) + } + + /** + * 计算查询文本与一组记忆项的相似度。 + * @param queryText - 查询文本 + * @param candidates - 候选记忆项列表 + * @param topN - 返回最相似的 N 个结果 + * @param threshold - 相似度阈值 (0 到 1) + * @returns 最相似的记忆项列表及其相似度分数 + */ + async findSimilarMemoriesToQuery( + queryText: string, + candidates: (Memory | ShortMemory)[], + topN: number = 10, + threshold: number = 0.7 + ): Promise<{ memory: Memory | ShortMemory; similarity: number }[]> { + const queryVector = await this.getVector(queryText) + if (!queryVector) { + console.warn('[VectorService] Could not get vector for query text. Cannot find similar memories.') + return [] + } + + const results: { memory: Memory | ShortMemory; similarity: number }[] = [] + + for (const candidate of candidates) { + const candidateWithVector = await this.ensureVectorRepresentation(candidate) + if (candidateWithVector.vectorRepresentation && candidateWithVector.vectorRepresentation.length > 0) { + const similarity = cosineSimilarity(queryVector, candidateWithVector.vectorRepresentation) + const normalizedSimilarity = (similarity + 1) / 2 // 归一化到 0-1 + if (normalizedSimilarity >= threshold) { + results.push({ memory: candidate, similarity: normalizedSimilarity }) + } + } + } + + results.sort((a, b) => b.similarity - a.similarity) + return results.slice(0, topN) + } + + /** + * 清空向量缓存 + */ + clearCache(): void { + vectorCache.clear() + console.log('[VectorService] Vector cache cleared.') + } +} + +// 导出 VectorService 的单例 +export const vectorService = new VectorService() diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index bfc0632d5a..1cefe7a78a 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -10,7 +10,7 @@ import copilot from './copilot' import knowledge from './knowledge' import llm from './llm' import mcp from './mcp' -import memory from './memory' +import memory, { memoryPersistenceMiddleware } from './memory' import messagesReducer from './messages' import migrate from './migrate' import minapps from './minapps' @@ -45,7 +45,7 @@ const persistedReducer = persistReducer( key: 'cherry-studio', storage, version: 95, - blacklist: ['runtime', 'messages'], + blacklist: ['runtime', 'messages', 'memory'], migrate }, rootReducer @@ -59,7 +59,7 @@ const store = configureStore({ serializableCheck: { ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER] } - }) + }).concat(memoryPersistenceMiddleware) }, devTools: true }) diff --git a/src/renderer/src/store/memory.ts b/src/renderer/src/store/memory.ts index 9d32350f00..1aaf3236cd 100644 --- a/src/renderer/src/store/memory.ts +++ b/src/renderer/src/store/memory.ts @@ -1,5 +1,6 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' import { nanoid } from 'nanoid' +import log from 'electron-log' // 记忆列表接口 export interface MemoryList { @@ -22,6 +23,14 @@ export interface Memory { analyzedMessageIds?: string[] // 记录该记忆是从哪些消息中分析出来的 lastMessageId?: string // 分析时的最后一条消息的ID,用于跟踪分析进度 topicId?: string // 关联的对话话题ID,用于跟踪该记忆来自哪个话题 + vector?: number[] // 记忆的向量表示,用于语义搜索 + entities?: string[] // 记忆中提取的实体 + keywords?: string[] // 记忆中提取的关键词 + importance?: number // 记忆的重要性评分(0-1) + accessCount?: number // 记忆被访问的次数 + lastAccessedAt?: string // 记忆最后被访问的时间 + decayFactor?: number // 记忆衰减因子(0-1),值越小衰减越大 + freshness?: number // 记忆鲜度评分(0-1),基于创建时间和最后访问时间 } // 短记忆项接口 @@ -32,6 +41,39 @@ export interface ShortMemory { topicId: string // 关联的对话话题ID analyzedMessageIds?: string[] // 记录该记忆是从哪些消息中分析出来的 lastMessageId?: string // 分析时的最后一条消息的ID,用于跟踪分析进度 + vector?: number[] // 记忆的向量表示,用于语义搜索 + entities?: string[] // 记忆中提取的实体 + keywords?: string[] // 记忆中提取的关键词 + importance?: number // 记忆的重要性评分(0-1) + accessCount?: number // 记忆被访问的次数 + lastAccessedAt?: string // 记忆最后被访问的时间 + decayFactor?: number // 记忆衰减因子(0-1),值越小衰减越快 + freshness?: number // 记忆鲜度评分(0-1),基于创建时间和最后访问时间 +} + +// 分析统计数据接口 +export interface AnalysisStats { + totalAnalyses: number // 总分析次数 + successfulAnalyses: number // 成功分析次数(生成了新记忆) + newMemoriesGenerated: number // 生成的新记忆数量 + averageAnalysisTime: number // 平均分析时间(毫秒) + lastAnalysisTime: number // 上次分析时间戳 +} + +// 性能指标接口 +export interface PerformanceMetrics { + analysisLatency: number[] // 最近的分析延迟时间(毫秒) + memoryRetrievalLatency: number[] // 最近的记忆检索延迟时间(毫秒) + memoryCount: number // 当前记忆数量 + shortMemoryCount: number // 当前短期记忆数量 + lastPerformanceCheck: number // 上次性能检查时间 +} + +// 用户关注点接口 +export interface UserInterest { + topic: string // 关注主题 + weight: number // 权重(0-1) + lastUpdated: string // 上次更新时间 } export interface MemoryState { @@ -44,8 +86,30 @@ export interface MemoryState { autoAnalyze: boolean // 是否自动分析 analyzeModel: string | null // 用于长期记忆分析的模型ID shortMemoryAnalyzeModel: string | null // 用于短期记忆分析的模型ID + vectorizeModel: string | null // 用于向量化的模型ID lastAnalyzeTime: number | null // 上次分析时间 isAnalyzing: boolean // 是否正在分析 + + // 自适应分析相关 + adaptiveAnalysisEnabled: boolean // 是否启用自适应分析 + analysisFrequency: number // 分析频率(消息数) + analysisDepth: 'low' | 'medium' | 'high' // 分析深度 + analysisStats: AnalysisStats // 分析统计数据 + + // 用户关注点相关 + interestTrackingEnabled: boolean // 是否启用兴趣跟踪 + userInterests: UserInterest[] // 用户关注点 + + // 性能监控相关 + monitoringEnabled: boolean // 是否启用性能监控 + performanceMetrics: PerformanceMetrics // 性能指标 + + // 智能优先级与时效性管理相关 + priorityManagementEnabled: boolean // 是否启用智能优先级管理 + decayEnabled: boolean // 是否启用记忆衰减功能 + freshnessEnabled: boolean // 是否启用记忆鲜度评估 + decayRate: number // 记忆衰减速率(0-1) + lastPriorityUpdate: number // 上次优先级更新时间 } // 创建默认记忆列表 @@ -68,8 +132,42 @@ const initialState: MemoryState = { autoAnalyze: true, analyzeModel: 'gpt-3.5-turbo', // 设置默认长期记忆分析模型 shortMemoryAnalyzeModel: 'gpt-3.5-turbo', // 设置默认短期记忆分析模型 + vectorizeModel: 'gpt-3.5-turbo', // 设置默认向量化模型 lastAnalyzeTime: null, - isAnalyzing: false + isAnalyzing: false, + + // 自适应分析相关 + adaptiveAnalysisEnabled: true, // 默认启用自适应分析 + analysisFrequency: 5, // 默认每5条消息分析一次 + analysisDepth: 'medium', // 默认分析深度 + analysisStats: { + totalAnalyses: 0, + successfulAnalyses: 0, + newMemoriesGenerated: 0, + averageAnalysisTime: 0, + lastAnalysisTime: 0 + }, + + // 用户关注点相关 + interestTrackingEnabled: true, // 默认启用兴趣跟踪 + userInterests: [], + + // 性能监控相关 + monitoringEnabled: true, // 默认启用性能监控 + performanceMetrics: { + analysisLatency: [], + memoryRetrievalLatency: [], + memoryCount: 0, + shortMemoryCount: 0, + lastPerformanceCheck: Date.now() + }, + + // 智能优先级与时效性管理相关 + priorityManagementEnabled: true, // 默认启用智能优先级管理 + decayEnabled: true, // 默认启用记忆衰减功能 + freshnessEnabled: true, // 默认启用记忆鲜度评估 + decayRate: 0.05, // 默认衰减速率,每天减少5% + lastPriorityUpdate: Date.now() // 初始化为当前时间 } const memorySlice = createSlice({ @@ -168,6 +266,10 @@ const memorySlice = createSlice({ setShortMemoryAnalyzeModel: (state, action: PayloadAction) => { state.shortMemoryAnalyzeModel = action.payload }, + // 设置向量化模型 + setVectorizeModel: (state, action: PayloadAction) => { + state.vectorizeModel = action.payload + }, // 设置分析状态 setAnalyzing: (state, action: PayloadAction) => { @@ -341,7 +443,6 @@ const memorySlice = createSlice({ state.shortMemories = [] return } - state.shortMemories = state.shortMemories.filter((memory) => memory.id !== action.payload) }, @@ -367,7 +468,236 @@ const memorySlice = createSlice({ // 设置短记忆功能是否激活 setShortMemoryActive: (state, action: PayloadAction) => { state.shortMemoryActive = action.payload + }, + + // 自适应分析相关的reducer + setAdaptiveAnalysisEnabled: (state, action: PayloadAction) => { + state.adaptiveAnalysisEnabled = action.payload + }, + + setAnalysisFrequency: (state, action: PayloadAction) => { + state.analysisFrequency = action.payload + }, + + setAnalysisDepth: (state, action: PayloadAction<'low' | 'medium' | 'high'>) => { + state.analysisDepth = action.payload + }, + + updateAnalysisStats: (state, action: PayloadAction>) => { + state.analysisStats = { ...state.analysisStats, ...action.payload } + }, + + // 用户关注点相关的reducer + setInterestTrackingEnabled: (state, action: PayloadAction) => { + state.interestTrackingEnabled = action.payload + }, + + updateUserInterest: (state, action: PayloadAction) => { + const index = state.userInterests.findIndex((i) => i.topic === action.payload.topic) + if (index >= 0) { + state.userInterests[index] = action.payload + } else { + state.userInterests.push(action.payload) + } + }, + + // 性能监控相关的reducer + setMonitoringEnabled: (state, action: PayloadAction) => { + state.monitoringEnabled = action.payload + }, + + updatePerformanceMetrics: (state, action: PayloadAction>) => { + state.performanceMetrics = { ...state.performanceMetrics, ...action.payload } + }, + + addAnalysisLatency: (state, action: PayloadAction) => { + // 确保 performanceMetrics 存在 + if (!state.performanceMetrics) { + state.performanceMetrics = { + analysisLatency: [], + memoryRetrievalLatency: [], + memoryCount: 0, + shortMemoryCount: 0, + lastPerformanceCheck: Date.now() + } + } + + // 确保 analysisLatency 存在 + if (!state.performanceMetrics.analysisLatency) { + state.performanceMetrics.analysisLatency = [] + } + + const latencies = [...state.performanceMetrics.analysisLatency, action.payload].slice(-10) // 保留最近10次 + state.performanceMetrics.analysisLatency = latencies + }, + + addMemoryRetrievalLatency: (state, action: PayloadAction) => { + // 确保 performanceMetrics 存在 + if (!state.performanceMetrics) { + state.performanceMetrics = { + analysisLatency: [], + memoryRetrievalLatency: [], + memoryCount: 0, + shortMemoryCount: 0, + lastPerformanceCheck: Date.now() + } + } + + // 确保 memoryRetrievalLatency 存在 + if (!state.performanceMetrics.memoryRetrievalLatency) { + state.performanceMetrics.memoryRetrievalLatency = [] + } + + const latencies = [...state.performanceMetrics.memoryRetrievalLatency, action.payload].slice(-10) // 保留最近10次 + state.performanceMetrics.memoryRetrievalLatency = latencies + }, + + // 智能优先级与时效性管理相关的reducer + setPriorityManagementEnabled: (state, action: PayloadAction) => { + state.priorityManagementEnabled = action.payload + }, + + setDecayEnabled: (state, action: PayloadAction) => { + state.decayEnabled = action.payload + }, + + setFreshnessEnabled: (state, action: PayloadAction) => { + state.freshnessEnabled = action.payload + }, + + setDecayRate: (state, action: PayloadAction) => { + state.decayRate = action.payload + }, + + // 更新记忆优先级 + updateMemoryPriorities: (state) => { + const now = Date.now() + + // 更新长期记忆优先级 + if (state.memories && state.memories.length > 0) { + state.memories.forEach(memory => { + // 计算时间衰减因子 + if (state.decayEnabled && memory.lastAccessedAt) { + const daysSinceLastAccess = (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60 * 24) + const decayFactor = Math.max(0, 1 - (daysSinceLastAccess * state.decayRate)) + memory.decayFactor = decayFactor + } else { + memory.decayFactor = 1 // 无衰减 + } + + // 计算鲜度评分 + if (state.freshnessEnabled) { + const daysSinceCreation = (now - new Date(memory.createdAt).getTime()) / (1000 * 60 * 60 * 24) + const lastAccessDays = memory.lastAccessedAt + ? (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60 * 24) + : daysSinceCreation + + // 鲜度评分结合创建时间和最后访问时间 + const creationFreshness = Math.max(0, 1 - (daysSinceCreation / 30)) // 30天内创建的记忆较新 + const accessFreshness = Math.max(0, 1 - (lastAccessDays / 7)) // 7天内访问的记忆较新 + memory.freshness = (creationFreshness * 0.3) + (accessFreshness * 0.7) // 加权平均 + } + }) + } + + // 更新短期记忆优先级 + if (state.shortMemories && state.shortMemories.length > 0) { + state.shortMemories.forEach(memory => { + // 计算时间衰减因子 + if (state.decayEnabled && memory.lastAccessedAt) { + const hoursSinceLastAccess = (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60) + const decayFactor = Math.max(0, 1 - (hoursSinceLastAccess * state.decayRate * 4)) // 短期记忆衰减更快 + memory.decayFactor = decayFactor + } else { + memory.decayFactor = 1 // 无衰减 + } + + // 计算鲜度评分 + if (state.freshnessEnabled) { + const hoursSinceCreation = (now - new Date(memory.createdAt).getTime()) / (1000 * 60 * 60) + const lastAccessHours = memory.lastAccessedAt + ? (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60) + : hoursSinceCreation + + // 短期记忆的鲜度评分更注重最近性 + const creationFreshness = Math.max(0, 1 - (hoursSinceCreation / 24)) // 24小时内创建的记忆较新 + const accessFreshness = Math.max(0, 1 - (lastAccessHours / 6)) // 6小时内访问的记忆较新 + memory.freshness = (creationFreshness * 0.2) + (accessFreshness * 0.8) // 加权平均,更注重访问时间 + } + }) + } + + state.lastPriorityUpdate = now + }, + + // 更新记忆鲜度 + updateMemoryFreshness: (state) => { + if (!state.freshnessEnabled) return + + const now = Date.now() + + // 更新长期记忆鲜度 + if (state.memories && state.memories.length > 0) { + state.memories.forEach(memory => { + const daysSinceCreation = (now - new Date(memory.createdAt).getTime()) / (1000 * 60 * 60 * 24) + const lastAccessDays = memory.lastAccessedAt + ? (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60 * 24) + : daysSinceCreation + + const creationFreshness = Math.max(0, 1 - (daysSinceCreation / 30)) + const accessFreshness = Math.max(0, 1 - (lastAccessDays / 7)) + memory.freshness = (creationFreshness * 0.3) + (accessFreshness * 0.7) + }) + } + + // 更新短期记忆鲜度 + if (state.shortMemories && state.shortMemories.length > 0) { + state.shortMemories.forEach(memory => { + const hoursSinceCreation = (now - new Date(memory.createdAt).getTime()) / (1000 * 60 * 60) + const lastAccessHours = memory.lastAccessedAt + ? (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60) + : hoursSinceCreation + + const creationFreshness = Math.max(0, 1 - (hoursSinceCreation / 24)) + const accessFreshness = Math.max(0, 1 - (lastAccessHours / 6)) + memory.freshness = (creationFreshness * 0.2) + (accessFreshness * 0.8) + }) + } + }, + + // 记录记忆访问 + accessMemory: (state, action: PayloadAction<{ id: string; isShortMemory?: boolean }>) => { + const { id, isShortMemory } = action.payload + const now = new Date().toISOString() + + if (isShortMemory) { + // 更新短期记忆访问信息 + const memory = state.shortMemories?.find(m => m.id === id) + if (memory) { + memory.accessCount = (memory.accessCount || 0) + 1 + memory.lastAccessedAt = now + } + } else { + // 更新长期记忆访问信息 + const memory = state.memories?.find(m => m.id === id) + if (memory) { + memory.accessCount = (memory.accessCount || 0) + 1 + memory.lastAccessedAt = now + } + } } + }, + extraReducers: (builder) => { + builder + .addCase(loadMemoryData.fulfilled, (state, action) => { + if (action.payload) { + // 更新状态中的记忆数据 + state.memoryLists = action.payload.memoryLists || state.memoryLists + state.memories = action.payload.memories || state.memories + state.shortMemories = action.payload.shortMemories || state.shortMemories + log.info('Memory data loaded into state') + } + }) } }) @@ -379,6 +709,7 @@ export const { setAutoAnalyze, setAnalyzeModel, setShortMemoryAnalyzeModel, + setVectorizeModel, setAnalyzing, importMemories, clearMemories, @@ -391,7 +722,83 @@ export const { addShortMemory, deleteShortMemory, clearShortMemories, - setShortMemoryActive + setShortMemoryActive, + + // 自适应分析相关的action + setAdaptiveAnalysisEnabled, + setAnalysisFrequency, + setAnalysisDepth, + updateAnalysisStats, + + // 用户关注点相关的action + setInterestTrackingEnabled, + updateUserInterest, + + // 性能监控相关的action + setMonitoringEnabled, + updatePerformanceMetrics, + addAnalysisLatency, + addMemoryRetrievalLatency, + + // 智能优先级与时效性管理相关的action + setPriorityManagementEnabled, + setDecayEnabled, + setFreshnessEnabled, + setDecayRate, + updateMemoryPriorities, + updateMemoryFreshness, + accessMemory } = memorySlice.actions +// 加载记忆数据的异步 thunk +export const loadMemoryData = createAsyncThunk( + 'memory/loadData', + async () => { + try { + log.info('Loading memory data from file...') + const data = await window.api.memory.loadData() + log.info('Memory data loaded successfully') + return data + } catch (error) { + log.error('Failed to load memory data:', error) + return null + } + } +) + +// 保存记忆数据的异步 thunk +export const saveMemoryData = createAsyncThunk( + 'memory/saveData', + async (data: Partial) => { + try { + log.info('Saving memory data to file...') + const result = await window.api.memory.saveData(data) + log.info('Memory data saved successfully') + return result + } catch (error) { + log.error('Failed to save memory data:', error) + return false + } + } +) + +// 创建一个中间件来自动保存记忆数据的变化 +export const memoryPersistenceMiddleware = (store) => (next) => (action) => { + const result = next(action) + + // 如果是记忆相关的操作,保存数据到文件 + if (action.type.startsWith('memory/') && + !action.type.includes('loadData') && + !action.type.includes('saveData')) { + const state = store.getState().memory + store.dispatch(saveMemoryData({ + memoryLists: state.memoryLists, + memories: state.memories, + shortMemories: state.shortMemories + })) + } + + return result +} + export default memorySlice.reducer diff --git a/yarn.lock b/yarn.lock index 03f1f5217a..c00c99146a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3304,6 +3304,38 @@ __metadata: languageName: node linkType: hard +"@types/d3-array@npm:*": + version: 3.2.1 + resolution: "@types/d3-array@npm:3.2.1" + checksum: 10c0/38bf2c778451f4b79ec81a2288cb4312fe3d6449ecdf562970cc339b60f280f31c93a024c7ff512607795e79d3beb0cbda123bb07010167bce32927f71364bca + languageName: node + linkType: hard + +"@types/d3-axis@npm:*": + version: 3.0.6 + resolution: "@types/d3-axis@npm:3.0.6" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/d756d42360261f44d8eefd0950c5bb0a4f67a46dd92069da3f723ac36a1e8cb2b9ce6347d836ef19d5b8aef725dbcf8fdbbd6cfbff676ca4b0642df2f78b599a + languageName: node + linkType: hard + +"@types/d3-brush@npm:*": + version: 3.0.6 + resolution: "@types/d3-brush@npm:3.0.6" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/fd6e2ac7657a354f269f6b9c58451ffae9d01b89ccb1eb6367fd36d635d2f1990967215ab498e0c0679ff269429c57fad6a2958b68f4d45bc9f81d81672edc01 + languageName: node + linkType: hard + +"@types/d3-chord@npm:*": + version: 3.0.6 + resolution: "@types/d3-chord@npm:3.0.6" + checksum: 10c0/c5a25eb5389db01e63faec0c5c2ec7cc41c494e9b3201630b494c4e862a60f1aa83fabbc33a829e7e1403941e3c30d206c741559b14406ac2a4239cfdf4b4c17 + languageName: node + linkType: hard + "@types/d3-color@npm:*": version: 3.1.3 resolution: "@types/d3-color@npm:3.1.3" @@ -3311,7 +3343,31 @@ __metadata: languageName: node linkType: hard -"@types/d3-drag@npm:^3.0.7": +"@types/d3-contour@npm:*": + version: 3.0.6 + resolution: "@types/d3-contour@npm:3.0.6" + dependencies: + "@types/d3-array": "npm:*" + "@types/geojson": "npm:*" + checksum: 10c0/e7d83e94719af4576ceb5ac7f277c5806f83ba6c3631744ae391cffc3641f09dfa279470b83053cd0b2acd6784e8749c71141d05bdffa63ca58ffb5b31a0f27c + languageName: node + linkType: hard + +"@types/d3-delaunay@npm:*": + version: 6.0.4 + resolution: "@types/d3-delaunay@npm:6.0.4" + checksum: 10c0/d154a8864f08c4ea23ecb9bdabcef1c406a25baa8895f0cb08a0ed2799de0d360e597552532ce7086ff0cdffa8f3563f9109d18f0191459d32bb620a36939123 + languageName: node + linkType: hard + +"@types/d3-dispatch@npm:*": + version: 3.0.6 + resolution: "@types/d3-dispatch@npm:3.0.6" + checksum: 10c0/405eb7d0ec139fbf72fa6a43b0f3ca8a1f913bb2cb38f607827e63fca8d4393f021f32f3b96b33c93ddbd37789453a0b3624f14f504add5308fd9aec8a46dda0 + languageName: node + linkType: hard + +"@types/d3-drag@npm:*, @types/d3-drag@npm:^3.0.7": version: 3.0.7 resolution: "@types/d3-drag@npm:3.0.7" dependencies: @@ -3320,6 +3376,59 @@ __metadata: languageName: node linkType: hard +"@types/d3-dsv@npm:*": + version: 3.0.7 + resolution: "@types/d3-dsv@npm:3.0.7" + checksum: 10c0/c0f01da862465594c8a28278b51c850af3b4239cc22b14fd1a19d7a98f93d94efa477bf59d8071beb285dca45bf614630811451e18e7c52add3a0abfee0a1871 + languageName: node + linkType: hard + +"@types/d3-ease@npm:*": + version: 3.0.2 + resolution: "@types/d3-ease@npm:3.0.2" + checksum: 10c0/aff5a1e572a937ee9bff6465225d7ba27d5e0c976bd9eacdac2e6f10700a7cb0c9ea2597aff6b43a6ed850a3210030870238894a77ec73e309b4a9d0333f099c + languageName: node + linkType: hard + +"@types/d3-fetch@npm:*": + version: 3.0.7 + resolution: "@types/d3-fetch@npm:3.0.7" + dependencies: + "@types/d3-dsv": "npm:*" + checksum: 10c0/3d147efa52a26da1a5d40d4d73e6cebaaa964463c378068062999b93ea3731b27cc429104c21ecbba98c6090e58ef13429db6399238c5e3500162fb3015697a0 + languageName: node + linkType: hard + +"@types/d3-force@npm:*": + version: 3.0.10 + resolution: "@types/d3-force@npm:3.0.10" + checksum: 10c0/c82b459079a106b50e346c9b79b141f599f2fc4f598985a5211e72c7a2e20d35bd5dc6e91f306b323c8bfa325c02c629b1645f5243f1c6a55bd51bc85cccfa92 + languageName: node + linkType: hard + +"@types/d3-format@npm:*": + version: 3.0.4 + resolution: "@types/d3-format@npm:3.0.4" + checksum: 10c0/3ac1600bf9061a59a228998f7cd3f29e85cbf522997671ba18d4d84d10a2a1aff4f95aceb143fa9960501c3ec351e113fc75884e6a504ace44dc1744083035ee + languageName: node + linkType: hard + +"@types/d3-geo@npm:*": + version: 3.1.0 + resolution: "@types/d3-geo@npm:3.1.0" + dependencies: + "@types/geojson": "npm:*" + checksum: 10c0/3745a93439038bb5b0b38facf435f7079812921d46406f5d38deaee59e90084ff742443c7ea0a8446df81a0d81eaf622fe7068cf4117a544bd4aa3b2dc182f88 + languageName: node + linkType: hard + +"@types/d3-hierarchy@npm:*": + version: 3.1.7 + resolution: "@types/d3-hierarchy@npm:3.1.7" + checksum: 10c0/873711737d6b8e7b6f1dda0bcd21294a48f75024909ae510c5d2c21fad2e72032e0958def4d9f68319d3aaac298ad09c49807f8bfc87a145a82693b5208613c7 + languageName: node + linkType: hard + "@types/d3-interpolate@npm:*": version: 3.0.4 resolution: "@types/d3-interpolate@npm:3.0.4" @@ -3329,6 +3438,50 @@ __metadata: languageName: node linkType: hard +"@types/d3-path@npm:*": + version: 3.1.1 + resolution: "@types/d3-path@npm:3.1.1" + checksum: 10c0/2c36eb31ebaf2ce4712e793fd88087117976f7c4ed69cc2431825f999c8c77cca5cea286f3326432b770739ac6ccd5d04d851eb65e7a4dbcc10c982b49ad2c02 + languageName: node + linkType: hard + +"@types/d3-polygon@npm:*": + version: 3.0.2 + resolution: "@types/d3-polygon@npm:3.0.2" + checksum: 10c0/f46307bb32b6c2aef8c7624500e0f9b518de8f227ccc10170b869dc43e4c542560f6c8d62e9f087fac45e198d6e4b623e579c0422e34c85baf56717456d3f439 + languageName: node + linkType: hard + +"@types/d3-quadtree@npm:*": + version: 3.0.6 + resolution: "@types/d3-quadtree@npm:3.0.6" + checksum: 10c0/7eaa0a4d404adc856971c9285e1c4ab17e9135ea669d847d6db7e0066126a28ac751864e7ce99c65d526e130f56754a2e437a1617877098b3bdcc3ef23a23616 + languageName: node + linkType: hard + +"@types/d3-random@npm:*": + version: 3.0.3 + resolution: "@types/d3-random@npm:3.0.3" + checksum: 10c0/5f4fea40080cd6d4adfee05183d00374e73a10c530276a6455348983dda341003a251def28565a27c25d9cf5296a33e870e397c9d91ff83fb7495a21c96b6882 + languageName: node + linkType: hard + +"@types/d3-scale-chromatic@npm:*": + version: 3.1.0 + resolution: "@types/d3-scale-chromatic@npm:3.1.0" + checksum: 10c0/93c564e02d2e97a048e18fe8054e4a935335da6ab75a56c3df197beaa87e69122eef0dfbeb7794d4a444a00e52e3123514ee27cec084bd21f6425b7037828cc2 + languageName: node + linkType: hard + +"@types/d3-scale@npm:*": + version: 4.0.9 + resolution: "@types/d3-scale@npm:4.0.9" + dependencies: + "@types/d3-time": "npm:*" + checksum: 10c0/4ac44233c05cd50b65b33ecb35d99fdf07566bcdbc55bc1306b2f27d1c5134d8c560d356f2c8e76b096e9125ffb8d26d95f78d56e210d1c542cb255bdf31d6c8 + languageName: node + linkType: hard + "@types/d3-selection@npm:*, @types/d3-selection@npm:^3.0.10": version: 3.0.11 resolution: "@types/d3-selection@npm:3.0.11" @@ -3336,7 +3489,37 @@ __metadata: languageName: node linkType: hard -"@types/d3-transition@npm:^3.0.8": +"@types/d3-shape@npm:*": + version: 3.1.7 + resolution: "@types/d3-shape@npm:3.1.7" + dependencies: + "@types/d3-path": "npm:*" + checksum: 10c0/38e59771c1c4c83b67aa1f941ce350410522a149d2175832fdc06396b2bb3b2c1a2dd549e0f8230f9f24296ee5641a515eaf10f55ee1ef6c4f83749e2dd7dcfd + languageName: node + linkType: hard + +"@types/d3-time-format@npm:*": + version: 4.0.3 + resolution: "@types/d3-time-format@npm:4.0.3" + checksum: 10c0/9ef5e8e2b96b94799b821eed5d61a3d432c7903247966d8ad951b8ce5797fe46554b425cb7888fa5bf604b4663c369d7628c0328ffe80892156671c58d1a7f90 + languageName: node + linkType: hard + +"@types/d3-time@npm:*": + version: 3.0.4 + resolution: "@types/d3-time@npm:3.0.4" + checksum: 10c0/6d9e2255d63f7a313a543113920c612e957d70da4fb0890931da6c2459010291b8b1f95e149a538500c1c99e7e6c89ffcce5554dd29a31ff134a38ea94b6d174 + languageName: node + linkType: hard + +"@types/d3-timer@npm:*": + version: 3.0.2 + resolution: "@types/d3-timer@npm:3.0.2" + checksum: 10c0/c644dd9571fcc62b1aa12c03bcad40571553020feeb5811f1d8a937ac1e65b8a04b759b4873aef610e28b8714ac71c9885a4d6c127a048d95118f7e5b506d9e1 + languageName: node + linkType: hard + +"@types/d3-transition@npm:*, @types/d3-transition@npm:^3.0.8": version: 3.0.9 resolution: "@types/d3-transition@npm:3.0.9" dependencies: @@ -3345,7 +3528,7 @@ __metadata: languageName: node linkType: hard -"@types/d3-zoom@npm:^3.0.8": +"@types/d3-zoom@npm:*, @types/d3-zoom@npm:^3.0.8": version: 3.0.8 resolution: "@types/d3-zoom@npm:3.0.8" dependencies: @@ -3355,6 +3538,44 @@ __metadata: languageName: node linkType: hard +"@types/d3@npm:^7": + version: 7.4.3 + resolution: "@types/d3@npm:7.4.3" + dependencies: + "@types/d3-array": "npm:*" + "@types/d3-axis": "npm:*" + "@types/d3-brush": "npm:*" + "@types/d3-chord": "npm:*" + "@types/d3-color": "npm:*" + "@types/d3-contour": "npm:*" + "@types/d3-delaunay": "npm:*" + "@types/d3-dispatch": "npm:*" + "@types/d3-drag": "npm:*" + "@types/d3-dsv": "npm:*" + "@types/d3-ease": "npm:*" + "@types/d3-fetch": "npm:*" + "@types/d3-force": "npm:*" + "@types/d3-format": "npm:*" + "@types/d3-geo": "npm:*" + "@types/d3-hierarchy": "npm:*" + "@types/d3-interpolate": "npm:*" + "@types/d3-path": "npm:*" + "@types/d3-polygon": "npm:*" + "@types/d3-quadtree": "npm:*" + "@types/d3-random": "npm:*" + "@types/d3-scale": "npm:*" + "@types/d3-scale-chromatic": "npm:*" + "@types/d3-selection": "npm:*" + "@types/d3-shape": "npm:*" + "@types/d3-time": "npm:*" + "@types/d3-time-format": "npm:*" + "@types/d3-timer": "npm:*" + "@types/d3-transition": "npm:*" + "@types/d3-zoom": "npm:*" + checksum: 10c0/a9c6d65b13ef3b42c87f2a89ea63a6d5640221869f97d0657b0cb2f1dac96a0f164bf5605643c0794e0de3aa2bf05df198519aaf15d24ca135eb0e8bd8a9d879 + languageName: node + linkType: hard + "@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.6": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" @@ -3406,6 +3627,13 @@ __metadata: languageName: node linkType: hard +"@types/geojson@npm:*": + version: 7946.0.16 + resolution: "@types/geojson@npm:7946.0.16" + checksum: 10c0/1ff24a288bd5860b766b073ead337d31d73bdc715e5b50a2cee5cb0af57a1ed02cc04ef295f5fa68dc40fe3e4f104dd31282b2b818a5ba3231bc1001ba084e3c + languageName: node + linkType: hard + "@types/hast@npm:^3.0.0, @types/hast@npm:^3.0.4": version: 3.0.4 resolution: "@types/hast@npm:3.0.4" @@ -3925,6 +4153,7 @@ __metadata: "@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch" "@tryfabric/martian": "npm:^1.2.4" "@types/adm-zip": "npm:^0" + "@types/d3": "npm:^7" "@types/diff": "npm:^7" "@types/fs-extra": "npm:^11" "@types/lodash": "npm:^4.17.5" @@ -3947,6 +4176,7 @@ __metadata: babel-plugin-styled-components: "npm:^2.1.4" browser-image-compression: "npm:^2.0.2" color: "npm:^5.0.0" + d3: "npm:^7.9.0" dayjs: "npm:^1.11.11" dexie: "npm:^4.0.8" dexie-react-hooks: "npm:^1.1.7" @@ -5405,6 +5635,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:7": + version: 7.2.0 + resolution: "commander@npm:7.2.0" + checksum: 10c0/8d690ff13b0356df7e0ebbe6c59b4712f754f4b724d4f473d3cc5b3fdcf978e3a5dc3078717858a2ceb50b0f84d0660a7f22a96cdc50fb877d0c9bb31593d23a + languageName: node + linkType: hard + "commander@npm:9.2.0": version: 9.2.0 resolution: "commander@npm:9.2.0" @@ -5688,21 +5925,77 @@ __metadata: languageName: node linkType: hard -"d3-color@npm:1 - 3": +"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:2.5.0 - 3, d3-array@npm:3, d3-array@npm:^3.2.0": + version: 3.2.4 + resolution: "d3-array@npm:3.2.4" + dependencies: + internmap: "npm:1 - 2" + checksum: 10c0/08b95e91130f98c1375db0e0af718f4371ccacef7d5d257727fe74f79a24383e79aba280b9ffae655483ffbbad4fd1dec4ade0119d88c4749f388641c8bf8c50 + languageName: node + linkType: hard + +"d3-axis@npm:3": + version: 3.0.0 + resolution: "d3-axis@npm:3.0.0" + checksum: 10c0/a271e70ba1966daa5aaf6a7f959ceca3e12997b43297e757c7b945db2e1ead3c6ee226f2abcfa22abbd4e2e28bd2b71a0911794c4e5b911bbba271328a582c78 + languageName: node + linkType: hard + +"d3-brush@npm:3": + version: 3.0.0 + resolution: "d3-brush@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-drag: "npm:2 - 3" + d3-interpolate: "npm:1 - 3" + d3-selection: "npm:3" + d3-transition: "npm:3" + checksum: 10c0/07baf00334c576da2f68a91fc0da5732c3a5fa19bd3d7aed7fd24d1d674a773f71a93e9687c154176f7246946194d77c48c2d8fed757f5dcb1a4740067ec50a8 + languageName: node + linkType: hard + +"d3-chord@npm:3": + version: 3.0.1 + resolution: "d3-chord@npm:3.0.1" + dependencies: + d3-path: "npm:1 - 3" + checksum: 10c0/baa6013914af3f4fe1521f0d16de31a38eb8a71d08ff1dec4741f6f45a828661e5cd3935e39bd14e3032bdc78206c283ca37411da21d46ec3cfc520be6e7a7ce + languageName: node + linkType: hard + +"d3-color@npm:1 - 3, d3-color@npm:3": version: 3.1.0 resolution: "d3-color@npm:3.1.0" checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c languageName: node linkType: hard -"d3-dispatch@npm:1 - 3": +"d3-contour@npm:4": + version: 4.0.2 + resolution: "d3-contour@npm:4.0.2" + dependencies: + d3-array: "npm:^3.2.0" + checksum: 10c0/98bc5fbed6009e08707434a952076f39f1cd6ed8b9288253cc3e6a3286e4e80c63c62d84954b20e64bf6e4ededcc69add54d3db25e990784a59c04edd3449032 + languageName: node + linkType: hard + +"d3-delaunay@npm:6": + version: 6.0.4 + resolution: "d3-delaunay@npm:6.0.4" + dependencies: + delaunator: "npm:5" + checksum: 10c0/57c3aecd2525664b07c4c292aa11cf49b2752c0cf3f5257f752999399fe3c592de2d418644d79df1f255471eec8057a9cc0c3062ed7128cb3348c45f69597754 + languageName: node + linkType: hard + +"d3-dispatch@npm:1 - 3, d3-dispatch@npm:3": version: 3.0.1 resolution: "d3-dispatch@npm:3.0.1" checksum: 10c0/6eca77008ce2dc33380e45d4410c67d150941df7ab45b91d116dbe6d0a3092c0f6ac184dd4602c796dc9e790222bad3ff7142025f5fd22694efe088d1d941753 languageName: node linkType: hard -"d3-drag@npm:2 - 3, d3-drag@npm:^3.0.0": +"d3-drag@npm:2 - 3, d3-drag@npm:3, d3-drag@npm:^3.0.0": version: 3.0.0 resolution: "d3-drag@npm:3.0.0" dependencies: @@ -5712,14 +6005,78 @@ __metadata: languageName: node linkType: hard -"d3-ease@npm:1 - 3": +"d3-dsv@npm:1 - 3, d3-dsv@npm:3": + version: 3.0.1 + resolution: "d3-dsv@npm:3.0.1" + dependencies: + commander: "npm:7" + iconv-lite: "npm:0.6" + rw: "npm:1" + bin: + csv2json: bin/dsv2json.js + csv2tsv: bin/dsv2dsv.js + dsv2dsv: bin/dsv2dsv.js + dsv2json: bin/dsv2json.js + json2csv: bin/json2dsv.js + json2dsv: bin/json2dsv.js + json2tsv: bin/json2dsv.js + tsv2csv: bin/dsv2dsv.js + tsv2json: bin/dsv2json.js + checksum: 10c0/10e6af9e331950ed258f34ab49ac1b7060128ef81dcf32afc790bd1f7e8c3cc2aac7f5f875250a83f21f39bb5925fbd0872bb209f8aca32b3b77d32bab8a65ab + languageName: node + linkType: hard + +"d3-ease@npm:1 - 3, d3-ease@npm:3": version: 3.0.1 resolution: "d3-ease@npm:3.0.1" checksum: 10c0/fec8ef826c0cc35cda3092c6841e07672868b1839fcaf556e19266a3a37e6bc7977d8298c0fcb9885e7799bfdcef7db1baaba9cd4dcf4bc5e952cf78574a88b0 languageName: node linkType: hard -"d3-interpolate@npm:1 - 3": +"d3-fetch@npm:3": + version: 3.0.1 + resolution: "d3-fetch@npm:3.0.1" + dependencies: + d3-dsv: "npm:1 - 3" + checksum: 10c0/4f467a79bf290395ac0cbb5f7562483f6a18668adc4c8eb84c9d3eff048b6f6d3b6f55079ba1ebf1908dabe000c941d46be447f8d78453b2dad5fb59fb6aa93b + languageName: node + linkType: hard + +"d3-force@npm:3": + version: 3.0.0 + resolution: "d3-force@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-quadtree: "npm:1 - 3" + d3-timer: "npm:1 - 3" + checksum: 10c0/220a16a1a1ac62ba56df61028896e4b52be89c81040d20229c876efc8852191482c233f8a52bb5a4e0875c321b8e5cb6413ef3dfa4d8fe79eeb7d52c587f52cf + languageName: node + linkType: hard + +"d3-format@npm:1 - 3, d3-format@npm:3": + version: 3.1.0 + resolution: "d3-format@npm:3.1.0" + checksum: 10c0/049f5c0871ebce9859fc5e2f07f336b3c5bfff52a2540e0bac7e703fce567cd9346f4ad1079dd18d6f1e0eaa0599941c1810898926f10ac21a31fd0a34b4aa75 + languageName: node + linkType: hard + +"d3-geo@npm:3": + version: 3.1.1 + resolution: "d3-geo@npm:3.1.1" + dependencies: + d3-array: "npm:2.5.0 - 3" + checksum: 10c0/d32270dd2dc8ac3ea63e8805d63239c4c8ec6c0d339d73b5e5a30a87f8f54db22a78fb434369799465eae169503b25f9a107c642c8a16c32a3285bc0e6d8e8c1 + languageName: node + linkType: hard + +"d3-hierarchy@npm:3": + version: 3.1.2 + resolution: "d3-hierarchy@npm:3.1.2" + checksum: 10c0/6dcdb480539644aa7fc0d72dfc7b03f99dfbcdf02714044e8c708577e0d5981deb9d3e99bbbb2d26422b55bcc342ac89a0fa2ea6c9d7302e2fc0951dd96f89cf + languageName: node + linkType: hard + +"d3-interpolate@npm:1 - 3, d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:3": version: 3.0.1 resolution: "d3-interpolate@npm:3.0.1" dependencies: @@ -5728,6 +6085,57 @@ __metadata: languageName: node linkType: hard +"d3-path@npm:1 - 3, d3-path@npm:3, d3-path@npm:^3.1.0": + version: 3.1.0 + resolution: "d3-path@npm:3.1.0" + checksum: 10c0/dc1d58ec87fa8319bd240cf7689995111a124b141428354e9637aa83059eb12e681f77187e0ada5dedfce346f7e3d1f903467ceb41b379bfd01cd8e31721f5da + languageName: node + linkType: hard + +"d3-polygon@npm:3": + version: 3.0.1 + resolution: "d3-polygon@npm:3.0.1" + checksum: 10c0/e236aa7f33efa9a4072907af7dc119f85b150a0716759d4fe5f12f62573018264a6cbde8617fbfa6944a7ae48c1c0c8d3f39ae72e11f66dd471e9b5e668385df + languageName: node + linkType: hard + +"d3-quadtree@npm:1 - 3, d3-quadtree@npm:3": + version: 3.0.1 + resolution: "d3-quadtree@npm:3.0.1" + checksum: 10c0/18302d2548bfecaef788152397edec95a76400fd97d9d7f42a089ceb68d910f685c96579d74e3712d57477ed042b056881b47cd836a521de683c66f47ce89090 + languageName: node + linkType: hard + +"d3-random@npm:3": + version: 3.0.1 + resolution: "d3-random@npm:3.0.1" + checksum: 10c0/987a1a1bcbf26e6cf01fd89d5a265b463b2cea93560fc17d9b1c45e8ed6ff2db5924601bcceb808de24c94133f000039eb7fa1c469a7a844ccbf1170cbb25b41 + languageName: node + linkType: hard + +"d3-scale-chromatic@npm:3": + version: 3.1.0 + resolution: "d3-scale-chromatic@npm:3.1.0" + dependencies: + d3-color: "npm:1 - 3" + d3-interpolate: "npm:1 - 3" + checksum: 10c0/9a3f4671ab0b971f4a411b42180d7cf92bfe8e8584e637ce7e698d705e18d6d38efbd20ec64f60cc0dfe966c20d40fc172565bc28aaa2990c0a006360eed91af + languageName: node + linkType: hard + +"d3-scale@npm:4": + version: 4.0.2 + resolution: "d3-scale@npm:4.0.2" + dependencies: + d3-array: "npm:2.10.0 - 3" + d3-format: "npm:1 - 3" + d3-interpolate: "npm:1.2.0 - 3" + d3-time: "npm:2.1.1 - 3" + d3-time-format: "npm:2 - 4" + checksum: 10c0/65d9ad8c2641aec30ed5673a7410feb187a224d6ca8d1a520d68a7d6eac9d04caedbff4713d1e8545be33eb7fec5739983a7ab1d22d4e5ad35368c6729d362f1 + languageName: node + linkType: hard + "d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0": version: 3.0.0 resolution: "d3-selection@npm:3.0.0" @@ -5735,14 +6143,41 @@ __metadata: languageName: node linkType: hard -"d3-timer@npm:1 - 3": +"d3-shape@npm:3": + version: 3.2.0 + resolution: "d3-shape@npm:3.2.0" + dependencies: + d3-path: "npm:^3.1.0" + checksum: 10c0/f1c9d1f09926daaf6f6193ae3b4c4b5521e81da7d8902d24b38694517c7f527ce3c9a77a9d3a5722ad1e3ff355860b014557b450023d66a944eabf8cfde37132 + languageName: node + linkType: hard + +"d3-time-format@npm:2 - 4, d3-time-format@npm:4": + version: 4.1.0 + resolution: "d3-time-format@npm:4.1.0" + dependencies: + d3-time: "npm:1 - 3" + checksum: 10c0/735e00fb25a7fd5d418fac350018713ae394eefddb0d745fab12bbff0517f9cdb5f807c7bbe87bb6eeb06249662f8ea84fec075f7d0cd68609735b2ceb29d206 + languageName: node + linkType: hard + +"d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3, d3-time@npm:3": + version: 3.1.0 + resolution: "d3-time@npm:3.1.0" + dependencies: + d3-array: "npm:2 - 3" + checksum: 10c0/a984f77e1aaeaa182679b46fbf57eceb6ebdb5f67d7578d6f68ef933f8eeb63737c0949991618a8d29472dbf43736c7d7f17c452b2770f8c1271191cba724ca1 + languageName: node + linkType: hard + +"d3-timer@npm:1 - 3, d3-timer@npm:3": version: 3.0.1 resolution: "d3-timer@npm:3.0.1" checksum: 10c0/d4c63cb4bb5461d7038aac561b097cd1c5673969b27cbdd0e87fa48d9300a538b9e6f39b4a7f0e3592ef4f963d858c8a9f0e92754db73116770856f2fc04561a languageName: node linkType: hard -"d3-transition@npm:2 - 3": +"d3-transition@npm:2 - 3, d3-transition@npm:3": version: 3.0.1 resolution: "d3-transition@npm:3.0.1" dependencies: @@ -5757,7 +6192,7 @@ __metadata: languageName: node linkType: hard -"d3-zoom@npm:^3.0.0": +"d3-zoom@npm:3, d3-zoom@npm:^3.0.0": version: 3.0.0 resolution: "d3-zoom@npm:3.0.0" dependencies: @@ -5770,6 +6205,44 @@ __metadata: languageName: node linkType: hard +"d3@npm:^7.9.0": + version: 7.9.0 + resolution: "d3@npm:7.9.0" + dependencies: + d3-array: "npm:3" + d3-axis: "npm:3" + d3-brush: "npm:3" + d3-chord: "npm:3" + d3-color: "npm:3" + d3-contour: "npm:4" + d3-delaunay: "npm:6" + d3-dispatch: "npm:3" + d3-drag: "npm:3" + d3-dsv: "npm:3" + d3-ease: "npm:3" + d3-fetch: "npm:3" + d3-force: "npm:3" + d3-format: "npm:3" + d3-geo: "npm:3" + d3-hierarchy: "npm:3" + d3-interpolate: "npm:3" + d3-path: "npm:3" + d3-polygon: "npm:3" + d3-quadtree: "npm:3" + d3-random: "npm:3" + d3-scale: "npm:4" + d3-scale-chromatic: "npm:3" + d3-selection: "npm:3" + d3-shape: "npm:3" + d3-time: "npm:3" + d3-time-format: "npm:4" + d3-timer: "npm:3" + d3-transition: "npm:3" + d3-zoom: "npm:3" + checksum: 10c0/3dd9c08c73cfaa69c70c49e603c85e049c3904664d9c79a1a52a0f52795828a1ff23592dc9a7b2257e711d68a615472a13103c212032f38e016d609796e087e8 + languageName: node + linkType: hard + "dashdash@npm:^1.12.0": version: 1.14.1 resolution: "dashdash@npm:1.14.1" @@ -6052,6 +6525,15 @@ __metadata: languageName: node linkType: hard +"delaunator@npm:5": + version: 5.0.1 + resolution: "delaunator@npm:5.0.1" + dependencies: + robust-predicates: "npm:^3.0.2" + checksum: 10c0/3d7ea4d964731c5849af33fec0a271bc6753487b331fd7d43ccb17d77834706e1c383e6ab8fda0032da955e7576d1083b9603cdaf9cbdfd6b3ebd1fb8bb675a5 + languageName: node + linkType: hard + "delay@npm:^6.0.0": version: 6.0.0 resolution: "delay@npm:6.0.0" @@ -8961,7 +9443,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": +"iconv-lite@npm:0.6, iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -9103,6 +9585,13 @@ __metadata: languageName: node linkType: hard +"internmap@npm:1 - 2": + version: 2.0.3 + resolution: "internmap@npm:2.0.3" + checksum: 10c0/8cedd57f07bbc22501516fbfc70447f0c6812871d471096fad9ea603516eacc2137b633633daf432c029712df0baefd793686388ddf5737e3ea15074b877f7ed + languageName: node + linkType: hard + "invert-kv@npm:^1.0.0": version: 1.0.0 resolution: "invert-kv@npm:1.0.0" @@ -14426,6 +14915,13 @@ __metadata: languageName: node linkType: hard +"robust-predicates@npm:^3.0.2": + version: 3.0.2 + resolution: "robust-predicates@npm:3.0.2" + checksum: 10c0/4ecd53649f1c2d49529c85518f2fa69ffb2f7a4453f7fd19c042421c7b4d76c3efb48bc1c740c8f7049346d7cb58cf08ee0c9adaae595cc23564d360adb1fde4 + languageName: node + linkType: hard + "rollup-plugin-visualizer@npm:^5.12.0": version: 5.14.0 resolution: "rollup-plugin-visualizer@npm:5.14.0" @@ -14547,6 +15043,13 @@ __metadata: languageName: node linkType: hard +"rw@npm:1": + version: 1.3.3 + resolution: "rw@npm:1.3.3" + checksum: 10c0/b1e1ef37d1e79d9dc7050787866e30b6ddcb2625149276045c262c6b4d53075ddc35f387a856a8e76f0d0df59f4cd58fe24707e40797ebee66e542b840ed6a53 + languageName: node + linkType: hard + "safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" From 909849e69aec6daa87b2db502e3df26d6b154e34 Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Sun, 13 Apr 2025 20:53:32 +0800 Subject: [PATCH 06/11] =?UTF-8?q?=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/store/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 1cefe7a78a..9146c7e96a 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -44,7 +44,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 95, + version: 96, blacklist: ['runtime', 'messages', 'memory'], migrate }, From 445d4b879d043b808370385bae0c6eb95c702915 Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Sun, 13 Apr 2025 21:36:23 +0800 Subject: [PATCH 07/11] =?UTF-8?q?bug=E4=BF=AE=E6=94=B9=E4=B8=A2=E5=A4=B1?= =?UTF-8?q?=E8=AE=B0=E5=BF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/services/MemoryService.ts | 129 +++++++++++++++------ src/renderer/src/store/index.ts | 4 +- src/renderer/src/store/memory.ts | 37 ++---- 3 files changed, 106 insertions(+), 64 deletions(-) diff --git a/src/renderer/src/services/MemoryService.ts b/src/renderer/src/services/MemoryService.ts index 9471406c73..dd2ce8484a 100644 --- a/src/renderer/src/services/MemoryService.ts +++ b/src/renderer/src/services/MemoryService.ts @@ -15,7 +15,8 @@ import { updateUserInterest, updateMemoryPriorities, accessMemory, - Memory + Memory, + saveMemoryData // <-- 添加 saveMemoryData } from '@renderer/store/memory' import { useCallback, useEffect, useRef } from 'react' // Add useRef back @@ -580,11 +581,12 @@ export const addShortMemoryItem = ( ) } +// 分析对话内容并提取重要信息添加到短期记忆 // 分析对话内容并提取重要信息添加到短期记忆 export const analyzeAndAddShortMemories = async (topicId: string) => { if (!topicId) { console.log('[Short Memory Analysis] No topic ID provided') - return + return false } // 获取当前记忆状态 @@ -594,7 +596,7 @@ export const analyzeAndAddShortMemories = async (topicId: string) => { if (!shortMemoryAnalyzeModel) { console.log('[Short Memory Analysis] No short memory analyze model set') - return + return false } // 获取对话内容 @@ -612,13 +614,13 @@ export const analyzeAndAddShortMemories = async (topicId: string) => { } } catch (error) { console.error(`[Short Memory Analysis] Failed to get messages for topic ${topicId}:`, error) - return + return false } } if (!messages || messages.length === 0) { console.log('[Short Memory Analysis] No messages to analyze.') - return + return false } // 获取现有的短期记忆 @@ -638,7 +640,7 @@ export const analyzeAndAddShortMemories = async (topicId: string) => { if (newMessages.length === 0) { console.log('[Short Memory Analysis] No new messages to analyze.') - return + return false } console.log(`[Short Memory Analysis] Found ${newMessages.length} new messages to analyze.`) @@ -698,7 +700,7 @@ ${newConversation} if (!model) { console.error(`[Short Memory Analysis] Model ${shortMemoryAnalyzeModel} not found`) - return + return false } // 调用AI生成文本 @@ -710,37 +712,67 @@ ${newConversation} }) console.log('[Short Memory Analysis] AI.generateText response:', result) - if (!result) { - console.log('[Short Memory Analysis] No result from AI analysis.') - return + if (!result || typeof result !== 'string' || result.trim() === '') { + console.log('[Short Memory Analysis] No valid result from AI analysis.') + return false } - // 解析结果 - const lines = result - .split('\n') - .map((line: string) => line.trim()) - .filter((line: string) => { - // 匹配以数字和点开头的行(如"1.", "2.")或者以短横线开头的行(如"-") - return /^\d+\./.test(line) || line.startsWith('-') - }) - .map((line: string) => { - // 如果是数字开头,移除数字和点,如果是短横线开头,移除短横线 - if (/^\d+\./.test(line)) { - return line.replace(/^\d+\.\s*/, '').trim() - } else if (line.startsWith('-')) { - return line.substring(1).trim() - } - return line - }) - .filter(Boolean) + // 改进的记忆提取逻辑 + let extractedLines: string[] = [] - console.log('[Short Memory Analysis] Extracted items:', lines) + // 首先尝试匹配带有数字或短横线的列表项 + const listItemRegex = /(?:^|\n)(?:\d+\.\s*|\-\s*)(.+?)(?=\n\d+\.\s*|\n\-\s*|\n\n|$)/gs + let match + while ((match = listItemRegex.exec(result)) !== null) { + if (match[1] && match[1].trim()) { + extractedLines.push(match[1].trim()) + } + } - // 过滤掉已存在的记忆 + // 如果没有找到列表项,则尝试按行分割并过滤 + if (extractedLines.length === 0) { + extractedLines = result + .split('\n') + .map(line => line.trim()) + .filter(line => { + // 过滤掉空行和非内容行(如标题、分隔符等) + return line && + !line.startsWith('#') && + !line.startsWith('---') && + !line.startsWith('===') && + !line.includes('没有找到新的重要信息') && + !line.includes('No new important information') + }) + // 清理行首的数字、点和短横线 + .map(line => line.replace(/^(\d+\.\s*|\-\s*)/, '').trim()) + } + + console.log('[Short Memory Analysis] Extracted items:', extractedLines) + + if (extractedLines.length === 0) { + console.log('[Short Memory Analysis] No memory items extracted from the analysis result.') + return false + } + + // 过滤掉已存在的记忆(使用更严格的比较) const existingContents = topicShortMemories.map((memory) => memory.content.toLowerCase()) - const newMemories = lines.filter((content: string) => !existingContents.includes(content.toLowerCase())) + const newMemories = extractedLines.filter((content: string) => { + const normalizedContent = content.toLowerCase() + // 检查是否与现有记忆完全匹配或高度相似 + return !existingContents.some(existingContent => + existingContent === normalizedContent || + // 简单的相似度检查 - 如果一个字符串包含另一个的80%以上的内容 + (existingContent.includes(normalizedContent) && normalizedContent.length > existingContent.length * 0.8) || + (normalizedContent.includes(existingContent) && existingContent.length > normalizedContent.length * 0.8) + ) + }) - console.log(`[Short Memory Analysis] Found ${lines.length} items, ${newMemories.length} are new`) + console.log(`[Short Memory Analysis] Found ${extractedLines.length} items, ${newMemories.length} are new`) + + if (newMemories.length === 0) { + console.log('[Short Memory Analysis] No new memories to add after filtering.') + return false + } // 收集新分析的消息ID const newMessageIds = newMessages.map((msg) => msg.id) @@ -749,12 +781,39 @@ ${newConversation} const lastMessageId = messages[messages.length - 1]?.id // 添加新的短期记忆 + const addedMemories: string[] = [] // Explicitly type addedMemories for (const content of newMemories) { - addShortMemoryItem(content, topicId, newMessageIds, lastMessageId) - console.log(`[Short Memory Analysis] Added new short memory: "${content}" to topic ${topicId}`) + try { + store.dispatch( + addShortMemory({ + content, + topicId, + analyzedMessageIds: newMessageIds, + lastMessageId: lastMessageId + }) + ) + addedMemories.push(content) + console.log(`[Short Memory Analysis] Added new short memory: "${content}" to topic ${topicId}`) + } catch (error) { + console.error(`[Short Memory Analysis] Failed to add memory: "${content}"`, error) + } } - return newMemories.length > 0 + // 显式触发保存操作,确保数据被持久化 + try { + const state = store.getState().memory + await store.dispatch(saveMemoryData({ + memoryLists: state.memoryLists, + memories: state.memories, + shortMemories: state.shortMemories + })).unwrap() // 使用unwrap()来等待异步操作完成并处理错误 + console.log('[Short Memory Analysis] Memory data saved successfully') + } catch (error) { + console.error('[Short Memory Analysis] Failed to save memory data:', error) + // 即使保存失败,我们仍然返回true,因为记忆已经添加到Redux状态中 + } + + return addedMemories.length > 0 } catch (error) { console.error('[Short Memory Analysis] Failed to analyze and add short memories:', error) return false diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 9146c7e96a..7c9029a7d0 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -10,7 +10,7 @@ import copilot from './copilot' import knowledge from './knowledge' import llm from './llm' import mcp from './mcp' -import memory, { memoryPersistenceMiddleware } from './memory' +import memory from './memory' // Removed import of memoryPersistenceMiddleware import messagesReducer from './messages' import migrate from './migrate' import minapps from './minapps' @@ -59,7 +59,7 @@ const store = configureStore({ serializableCheck: { ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER] } - }).concat(memoryPersistenceMiddleware) + }) // Removed concat of memoryPersistenceMiddleware }, devTools: true }) diff --git a/src/renderer/src/store/memory.ts b/src/renderer/src/store/memory.ts index 1aaf3236cd..756220636d 100644 --- a/src/renderer/src/store/memory.ts +++ b/src/renderer/src/store/memory.ts @@ -755,15 +755,15 @@ export const loadMemoryData = createAsyncThunk( 'memory/loadData', async () => { try { - log.info('Loading memory data from file...') + // log.info('Loading memory data from file...') // Removed direct log call from renderer const data = await window.api.memory.loadData() - log.info('Memory data loaded successfully') + // log.info('Memory data loaded successfully') // Removed direct log call from renderer return data } catch (error) { - log.error('Failed to load memory data:', error) - return null + console.error('Failed to load memory data:', error) // Use console.error instead of log.error + return null // Ensure the thunk returns null on error } - } + } // <-- Add missing closing brace for the async function ) // 保存记忆数据的异步 thunk @@ -771,34 +771,17 @@ export const saveMemoryData = createAsyncThunk( 'memory/saveData', async (data: Partial) => { try { - log.info('Saving memory data to file...') + // log.info('Saving memory data to file...') // Removed direct log call from renderer const result = await window.api.memory.saveData(data) - log.info('Memory data saved successfully') + // log.info('Memory data saved successfully') // Removed direct log call from renderer return result } catch (error) { - log.error('Failed to save memory data:', error) + console.error('Failed to save memory data:', error) // Use console.error instead of log.error return false } } ) -// 创建一个中间件来自动保存记忆数据的变化 -export const memoryPersistenceMiddleware = (store) => (next) => (action) => { - const result = next(action) - - // 如果是记忆相关的操作,保存数据到文件 - if (action.type.startsWith('memory/') && - !action.type.includes('loadData') && - !action.type.includes('saveData')) { - const state = store.getState().memory - store.dispatch(saveMemoryData({ - memoryLists: state.memoryLists, - memories: state.memories, - shortMemories: state.shortMemories - })) - } - - return result -} - +// Middleware removed to prevent duplicate saves triggered by batch additions. +// Explicit saves should be handled where needed, e.g., at the end of analysis functions. export default memorySlice.reducer From e9347337b203bb8d7de4614f66d69014f96eba36 Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Sun, 13 Apr 2025 22:42:26 +0800 Subject: [PATCH 08/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/services/MemoryFileService.ts | 82 ++- .../src/components/MemoryProvider.tsx | 13 +- .../ContextualRecommendationSettings.tsx | 141 +++++ .../pages/settings/MemorySettings/index.tsx | 45 +- .../src/services/ContextualMemoryService.ts | 530 ++++++++++++++++++ src/renderer/src/services/MemoryService.ts | 308 +++++++++- src/renderer/src/store/memory.ts | 84 ++- 7 files changed, 1169 insertions(+), 34 deletions(-) create mode 100644 src/renderer/src/pages/settings/MemorySettings/ContextualRecommendationSettings.tsx create mode 100644 src/renderer/src/services/ContextualMemoryService.ts diff --git a/src/main/services/MemoryFileService.ts b/src/main/services/MemoryFileService.ts index 4aea44f8ea..258f7a3366 100644 --- a/src/main/services/MemoryFileService.ts +++ b/src/main/services/MemoryFileService.ts @@ -10,35 +10,48 @@ const memoryDataPath = path.join(getConfigDir(), 'memory-data.json') export class MemoryFileService { constructor() { - this.ensureMemoryFileExists() this.registerIpcHandlers() } - private async ensureMemoryFileExists() { - try { - const directory = path.dirname(memoryDataPath) - await fs.mkdir(directory, { recursive: true }) - try { - await fs.access(memoryDataPath) - } catch (error) { - // 文件不存在,创建一个空文件 - await fs.writeFile(memoryDataPath, JSON.stringify({ - memoryLists: [], - memories: [], - shortMemories: [] - }, null, 2)) - } - } catch (error) { - log.error('Failed to ensure memory file exists:', error) - } - } - private registerIpcHandlers() { // 读取记忆数据 ipcMain.handle(IpcChannel.Memory_LoadData, async () => { 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') - return JSON.parse(data) + 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 @@ -48,7 +61,32 @@ export class MemoryFileService { // 保存记忆数据 ipcMain.handle(IpcChannel.Memory_SaveData, async (_, data) => { try { - await fs.writeFile(memoryDataPath, JSON.stringify(data, null, 2)) + // 确保配置目录存在 + 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) + } + + // 尝试读取现有数据并合并 + 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 mergedData = { ...existingData, ...data } + + // 保存合并后的数据 + 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) diff --git a/src/renderer/src/components/MemoryProvider.tsx b/src/renderer/src/components/MemoryProvider.tsx index 6df2b28b38..6d782911d1 100644 --- a/src/renderer/src/components/MemoryProvider.tsx +++ b/src/renderer/src/components/MemoryProvider.tsx @@ -41,8 +41,19 @@ const MemoryProvider: FC = ({ children }) => { // 在组件挂载时加载记忆数据 useEffect(() => { console.log('[MemoryProvider] Loading memory data from file') + // 使用Redux Thunk加载记忆数据 dispatch(loadMemoryData()) - }, []) + .then((result) => { + if (result.payload) { + console.log('[MemoryProvider] Memory data loaded successfully via Redux Thunk') + } else { + console.log('[MemoryProvider] No memory data loaded or loading failed') + } + }) + .catch(error => { + console.error('[MemoryProvider] Error loading memory data:', error) + }) + }, [dispatch]) // 当对话更新时,触发记忆分析 useEffect(() => { diff --git a/src/renderer/src/pages/settings/MemorySettings/ContextualRecommendationSettings.tsx b/src/renderer/src/pages/settings/MemorySettings/ContextualRecommendationSettings.tsx new file mode 100644 index 0000000000..ed97d5e7ed --- /dev/null +++ b/src/renderer/src/pages/settings/MemorySettings/ContextualRecommendationSettings.tsx @@ -0,0 +1,141 @@ +import { InfoCircleOutlined } from '@ant-design/icons' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import { + setContextualRecommendationEnabled, + setAutoRecommendMemories, + setRecommendationThreshold, + clearCurrentRecommendations +} from '@renderer/store/memory' +import { Button, InputNumber, Slider, Switch, Tooltip } from 'antd' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..' + +const SliderContainer = styled.div` + display: flex; + align-items: center; + width: 100%; + max-width: 300px; + margin-right: 16px; +` + +const ContextualRecommendationSettings: FC = () => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + + // 获取相关状态 + const contextualRecommendationEnabled = useAppSelector((state) => state.memory.contextualRecommendationEnabled) + const autoRecommendMemories = useAppSelector((state) => state.memory.autoRecommendMemories) + const recommendationThreshold = useAppSelector((state) => state.memory.recommendationThreshold) + + // 处理开关状态变化 + const handleContextualRecommendationToggle = (checked: boolean) => { + dispatch(setContextualRecommendationEnabled(checked)) + } + + const handleAutoRecommendToggle = (checked: boolean) => { + dispatch(setAutoRecommendMemories(checked)) + } + + // 处理推荐阈值变化 + const handleThresholdChange = (value: number | null) => { + if (value !== null) { + dispatch(setRecommendationThreshold(value)) + } + } + + // 清除当前推荐 + const handleClearRecommendations = () => { + dispatch(clearCurrentRecommendations()) + } + + return ( + + {t('settings.memory.contextualRecommendation.title') || '上下文感知记忆推荐'} + + {t('settings.memory.contextualRecommendation.description') || + '根据当前对话上下文智能推荐相关记忆,提高AI回复的相关性和连贯性。'} + + + + + {t('settings.memory.contextualRecommendation.enable') || '启用上下文感知记忆推荐'} + + + + + + + + + + + + {t('settings.memory.contextualRecommendation.autoRecommend') || '自动推荐记忆'} + + + + + + + + + + {t('settings.memory.contextualRecommendation.threshold') || '推荐阈值'} + + + + +
+ + + + +
+
+ + + + {t('settings.memory.contextualRecommendation.clearRecommendations') || '清除当前推荐'} + + + + + + +
+ ) +} + +export default ContextualRecommendationSettings diff --git a/src/renderer/src/pages/settings/MemorySettings/index.tsx b/src/renderer/src/pages/settings/MemorySettings/index.tsx index 2458bec4fa..cebea3d924 100644 --- a/src/renderer/src/pages/settings/MemorySettings/index.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/index.tsx @@ -9,7 +9,7 @@ import { import { useTheme } from '@renderer/context/ThemeProvider' import { TopicManager } from '@renderer/hooks/useTopic' import { analyzeAndAddShortMemories, useMemoryService } from '@renderer/services/MemoryService' -import { useAppDispatch, useAppSelector } from '@renderer/store' +import store, { useAppDispatch, useAppSelector } from '@renderer/store' import { addMemory, clearMemories, @@ -41,6 +41,7 @@ import MemoryDeduplicationPanel from './MemoryDeduplicationPanel' import MemoryListManager from './MemoryListManager' import MemoryMindMap from './MemoryMindMap' import PriorityManagementSettings from './PriorityManagementSettings' +import ContextualRecommendationSettings from './ContextualRecommendationSettings' const MemorySettings: FC = () => { const { t } = useTranslation() @@ -255,13 +256,49 @@ const MemorySettings: FC = () => { } // 处理选择长期记忆分析模型 - const handleSelectModel = (modelId: string) => { + const handleSelectModel = async (modelId: string) => { dispatch(setAnalyzeModel(modelId)) + console.log('[Memory Settings] Analyze model set:', modelId) + + // 手动保存到JSON文件 + try { + const state = store.getState().memory + await window.api.memory.saveData({ + analyzeModel: modelId, + shortMemoryAnalyzeModel: state.shortMemoryAnalyzeModel, + vectorizeModel: state.vectorizeModel, + // 确保其他必要的数据也被保存 + memoryLists: state.memoryLists || [], + memories: state.memories || [], + shortMemories: state.shortMemories || [] + }) + console.log('[Memory Settings] Analyze model saved to file successfully:', modelId) + } catch (error) { + console.error('[Memory Settings] Failed to save analyze model to file:', error) + } } // 处理选择短期记忆分析模型 - const handleSelectShortMemoryModel = (modelId: string) => { + const handleSelectShortMemoryModel = async (modelId: string) => { dispatch(setShortMemoryAnalyzeModel(modelId)) + console.log('[Memory Settings] Short memory analyze model set:', modelId) + + // 手动保存到JSON文件 + try { + const state = store.getState().memory + await window.api.memory.saveData({ + analyzeModel: state.analyzeModel, + shortMemoryAnalyzeModel: modelId, + vectorizeModel: state.vectorizeModel, + // 确保其他必要的数据也被保存 + memoryLists: state.memoryLists || [], + memories: state.memories || [], + shortMemories: state.shortMemories || [] + }) + console.log('[Memory Settings] Short memory analyze model saved to file successfully:', modelId) + } catch (error) { + console.error('[Memory Settings] Failed to save short memory analyze model to file:', error) + } } // 手动触发分析 @@ -571,6 +608,8 @@ const MemorySettings: FC = () => { children: ( + + ) }, diff --git a/src/renderer/src/services/ContextualMemoryService.ts b/src/renderer/src/services/ContextualMemoryService.ts new file mode 100644 index 0000000000..f3a954d814 --- /dev/null +++ b/src/renderer/src/services/ContextualMemoryService.ts @@ -0,0 +1,530 @@ +// src/renderer/src/services/ContextualMemoryService.ts + +import store from '@renderer/store' +import { vectorService } from './VectorService' +import { addMemoryRetrievalLatency } from '@renderer/store/memory' +import { fetchGenerate } from '@renderer/services/ApiService' +import { Message } from '@renderer/types' +import { TopicManager } from '@renderer/hooks/useTopic' + +// 记忆项接口(从store/memory.ts导入) +interface Memory { + id: string + content: string + createdAt: string + source?: string + category?: string + listId: string + analyzedMessageIds?: string[] + lastMessageId?: string + topicId?: string + vector?: number[] + entities?: string[] + keywords?: string[] + importance?: number + accessCount?: number + lastAccessedAt?: string + decayFactor?: number + freshness?: number +} + +interface ShortMemory { + id: string + content: string + createdAt: string + topicId: string + analyzedMessageIds?: string[] + lastMessageId?: string + vector?: number[] + entities?: string[] + keywords?: string[] + importance?: number + accessCount?: number + lastAccessedAt?: string + decayFactor?: number + freshness?: number +} + +// 记忆推荐结果接口 +export interface MemoryRecommendation { + memory: Memory | ShortMemory + relevanceScore: number + source: 'long-term' | 'short-term' + matchReason?: string +} + +/** + * ContextualMemoryService 类负责实现上下文感知的记忆推荐和检索功能 + */ +class ContextualMemoryService { + /** + * 基于当前对话上下文推荐相关记忆 + * @param messages - 当前对话的消息列表 + * @param topicId - 当前对话的话题ID + * @param limit - 返回的最大记忆数量 + * @returns 推荐的记忆列表,按相关性排序 + */ + async getContextualMemoryRecommendations( + messages: Message[], + topicId: string, + limit: number = 5 + ): Promise { + console.log(`[ContextualMemory] Getting contextual memory recommendations for topic ${topicId}`) + + const startTime = performance.now() + + try { + // 获取当前状态 + const state = store.getState() + const memoryState = state.memory + + if (!memoryState) { + console.log('[ContextualMemory] Memory state not available') + return [] + } + + // 检查记忆功能是否激活 + if (!memoryState.isActive && !memoryState.shortMemoryActive) { + console.log('[ContextualMemory] Memory features are not active') + return [] + } + + // 获取最近的消息作为上下文 + const recentMessages = messages.slice(-5) + if (recentMessages.length === 0) { + console.log('[ContextualMemory] No recent messages available') + return [] + } + + // 构建上下文查询文本 + const contextQuery = this._buildContextQuery(recentMessages) + console.log(`[ContextualMemory] Context query: ${contextQuery}`) + + // 并行获取长期记忆和短期记忆的推荐 + const [longTermRecommendations, shortTermRecommendations] = await Promise.all([ + this._getLongTermMemoryRecommendations(contextQuery, topicId), + this._getShortTermMemoryRecommendations(contextQuery, topicId) + ]) + + // 合并并排序推荐结果 + let allRecommendations = [...longTermRecommendations, ...shortTermRecommendations] + + // 按相关性分数排序 + allRecommendations.sort((a, b) => b.relevanceScore - a.relevanceScore) + + // 限制返回数量 + const limitedRecommendations = allRecommendations.slice(0, limit) + + // 记录性能指标 + const endTime = performance.now() + const latency = endTime - startTime + store.dispatch(addMemoryRetrievalLatency(latency)) + + console.log(`[ContextualMemory] Found ${limitedRecommendations.length} recommendations in ${latency.toFixed(2)}ms`) + + return limitedRecommendations + } catch (error) { + console.error('[ContextualMemory] Error getting contextual memory recommendations:', error) + return [] + } + } + + /** + * 基于当前对话主题自动提取相关记忆 + * @param topicId - 当前对话的话题ID + * @param limit - 返回的最大记忆数量 + * @returns 与当前主题相关的记忆列表 + */ + async getTopicRelatedMemories(topicId: string, limit: number = 10): Promise { + console.log(`[ContextualMemory] Getting topic-related memories for topic ${topicId}`) + + try { + // 获取当前状态 + const state = store.getState() + const memoryState = state.memory + const messagesState = state.messages + + if (!memoryState || !messagesState) { + console.log('[ContextualMemory] Required state not available') + return [] + } + + // 获取话题信息 + // 使用TopicManager获取话题 + let topicQuery = '' + try { + const topic = await TopicManager.getTopic(topicId) + if (!topic) { + console.log(`[ContextualMemory] Topic ${topicId} not found`) + return [] + } + + // 使用话题ID作为查询 + // 注意:TopicManager.getTopic返回的类型只有id和messages属性 + topicQuery = `Topic ${topicId}` + if (!topicQuery.trim()) { + console.log('[ContextualMemory] No topic information available for query') + return [] + } + } catch (error) { + console.error(`[ContextualMemory] Error getting topic ${topicId}:`, error) + return [] + } + + console.log(`[ContextualMemory] Topic query: ${topicQuery}`) + + // 并行获取长期记忆和短期记忆的推荐 + const [longTermRecommendations, shortTermRecommendations] = await Promise.all([ + this._getLongTermMemoryRecommendations(topicQuery, topicId), + this._getShortTermMemoryRecommendations(topicQuery, topicId) + ]) + + // 合并并排序推荐结果 + let allRecommendations = [...longTermRecommendations, ...shortTermRecommendations] + + // 按相关性分数排序 + allRecommendations.sort((a, b) => b.relevanceScore - a.relevanceScore) + + // 限制返回数量 + const limitedRecommendations = allRecommendations.slice(0, limit) + + console.log(`[ContextualMemory] Found ${limitedRecommendations.length} topic-related memories`) + + return limitedRecommendations + } catch (error) { + console.error('[ContextualMemory] Error getting topic-related memories:', error) + return [] + } + } + + /** + * 使用语义搜索查找与查询相关的记忆 + * @param query - 搜索查询 + * @param limit - 返回的最大记忆数量 + * @returns 与查询相关的记忆列表 + */ + async searchMemoriesBySemantics(query: string, limit: number = 10): Promise { + console.log(`[ContextualMemory] Semantic search for: ${query}`) + + try { + // 获取当前状态 + const state = store.getState() + const memoryState = state.memory + + if (!memoryState) { + console.log('[ContextualMemory] Memory state not available') + return [] + } + + // 并行获取长期记忆和短期记忆的推荐 + const [longTermRecommendations, shortTermRecommendations] = await Promise.all([ + this._getLongTermMemoryRecommendations(query), + this._getShortTermMemoryRecommendations(query) + ]) + + // 合并并排序推荐结果 + let allRecommendations = [...longTermRecommendations, ...shortTermRecommendations] + + // 按相关性分数排序 + allRecommendations.sort((a, b) => b.relevanceScore - a.relevanceScore) + + // 限制返回数量 + const limitedRecommendations = allRecommendations.slice(0, limit) + + console.log(`[ContextualMemory] Found ${limitedRecommendations.length} memories matching query`) + + return limitedRecommendations + } catch (error) { + console.error('[ContextualMemory] Error searching memories by semantics:', error) + return [] + } + } + + /** + * 使用AI分析当前对话上下文,提取关键信息并推荐相关记忆 + * @param messages - 当前对话的消息列表 + * @param limit - 返回的最大记忆数量 + * @returns 基于AI分析的相关记忆推荐 + */ + async getAIEnhancedMemoryRecommendations(messages: Message[], limit: number = 5): Promise { + console.log('[ContextualMemory] Getting AI-enhanced memory recommendations') + + try { + // 获取当前状态 + const state = store.getState() + const memoryState = state.memory + + if (!memoryState) { + console.log('[ContextualMemory] Memory state not available') + return [] + } + + // 获取分析模型 + const analyzeModel = memoryState.analyzeModel + if (!analyzeModel) { + console.log('[ContextualMemory] No analyze model set') + return [] + } + + // 获取最近的消息作为上下文 + const recentMessages = messages.slice(-10) + if (recentMessages.length === 0) { + console.log('[ContextualMemory] No recent messages available') + return [] + } + + // 构建对话内容 + const conversation = recentMessages.map(msg => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n') + + // 构建提示词 + const prompt = ` +请分析以下对话内容,提取出关键信息和主题,以便我可以找到相关的记忆。 + +请提供: +1. 对话的主要主题 +2. 用户可能关心的关键信息点 +3. 可能与此对话相关的背景知识或上下文 + +请以简洁的关键词和短语形式回答,每行一个要点,不要使用编号或项目符号。 + +对话内容: +${conversation} +` + + // 调用AI生成文本 + console.log('[ContextualMemory] Calling AI for context analysis...') + const result = await fetchGenerate({ + prompt: prompt, + content: conversation, + modelId: analyzeModel + }) + + if (!result || typeof result !== 'string' || result.trim() === '') { + console.log('[ContextualMemory] No valid result from AI analysis') + return [] + } + + // 提取关键信息 + const keyPoints = result + .split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#') && !line.startsWith('-')) + + console.log('[ContextualMemory] Extracted key points:', keyPoints) + + // 使用提取的关键信息作为查询 + const enhancedQuery = keyPoints.join(' ') + + // 获取相关记忆 + return await this.searchMemoriesBySemantics(enhancedQuery, limit) + } catch (error) { + console.error('[ContextualMemory] Error getting AI-enhanced memory recommendations:', error) + return [] + } + } + + /** + * 构建上下文查询文本 + * @param messages - 消息列表 + * @returns 构建的查询文本 + * @private + */ + private _buildContextQuery(messages: Message[]): string { + // 提取最近消息的内容 + const messageContents = messages.map(msg => msg.content || '').filter(content => content.trim() !== '') + + // 如果没有有效内容,返回空字符串 + if (messageContents.length === 0) { + return '' + } + + // 合并消息内容,最多使用最近的3条消息 + return messageContents.slice(-3).join(' ') + } + + /** + * 获取与查询相关的长期记忆推荐 + * @param query - 查询文本 + * @param topicId - 可选的话题ID,用于过滤记忆 + * @returns 长期记忆推荐列表 + * @private + */ + private async _getLongTermMemoryRecommendations( + query: string, + topicId?: string + ): Promise { + // 获取当前状态 + const state = store.getState() + const memoryState = state.memory + + // 检查长期记忆功能是否激活 + if (!memoryState || !memoryState.isActive) { + return [] + } + + // 获取所有激活的记忆列表 + const activeListIds = memoryState.memoryLists + .filter(list => list.isActive) + .map(list => list.id) + + if (activeListIds.length === 0) { + return [] + } + + // 获取激活列表中的记忆 + const memories = memoryState.memories.filter(memory => activeListIds.includes(memory.listId)) + + if (memories.length === 0) { + return [] + } + + // 使用向量服务查找相似记忆 + const results = await vectorService.findSimilarMemoriesToQuery( + query, + memories, + 20, // 获取更多结果,后续会进一步优化排序 + 0.5 // 降低阈值以获取更多潜在相关记忆 + ) + + // 转换为推荐格式 + const recommendations: MemoryRecommendation[] = results.map(result => ({ + memory: result.memory as Memory, + relevanceScore: result.similarity, + source: 'long-term', + matchReason: '语义相似' + })) + + // 应用高级排序优化 + return this._optimizeRelevanceRanking(recommendations, query, topicId) + } + + /** + * 获取与查询相关的短期记忆推荐 + * @param query - 查询文本 + * @param topicId - 可选的话题ID,用于过滤记忆 + * @returns 短期记忆推荐列表 + * @private + */ + private async _getShortTermMemoryRecommendations( + query: string, + topicId?: string + ): Promise { + // 获取当前状态 + const state = store.getState() + const memoryState = state.memory + + // 检查短期记忆功能是否激活 + if (!memoryState || !memoryState.shortMemoryActive) { + return [] + } + + // 获取短期记忆 + let shortMemories = memoryState.shortMemories + + // 如果指定了话题ID,只获取该话题的短期记忆 + if (topicId) { + shortMemories = shortMemories.filter(memory => memory.topicId === topicId) + } + + if (shortMemories.length === 0) { + return [] + } + + // 使用向量服务查找相似记忆 + const results = await vectorService.findSimilarMemoriesToQuery( + query, + shortMemories, + 20, // 获取更多结果,后续会进一步优化排序 + 0.5 // 降低阈值以获取更多潜在相关记忆 + ) + + // 转换为推荐格式 + const recommendations: MemoryRecommendation[] = results.map(result => ({ + memory: result.memory as ShortMemory, + relevanceScore: result.similarity, + source: 'short-term', + matchReason: '与当前对话相关' + })) + + // 应用高级排序优化 + return this._optimizeRelevanceRanking(recommendations, query, topicId) + } + + /** + * 优化记忆推荐的相关性排序 + * @param recommendations - 初始推荐列表 + * @param query - 查询文本 + * @param topicId - 可选的话题ID + * @returns 优化排序后的推荐列表 + * @private + */ + private _optimizeRelevanceRanking( + recommendations: MemoryRecommendation[], + query: string, + topicId?: string + ): MemoryRecommendation[] { + if (recommendations.length === 0) { + return [] + } + + // 获取当前状态 + const state = store.getState() + const memoryState = state.memory + + // 应用多因素排序优化 + return recommendations.map(rec => { + const memory = rec.memory + let adjustedScore = rec.relevanceScore + + // 1. 考虑记忆的重要性 + if (memory.importance && memoryState.priorityManagementEnabled) { + adjustedScore *= (1 + memory.importance * 0.5) // 重要性最多提升50%的分数 + } + + // 2. 考虑记忆的鲜度 + if (memory.freshness && memoryState.freshnessEnabled) { + adjustedScore *= (1 + memory.freshness * 0.3) // 鲜度最多提升30%的分数 + } + + // 3. 考虑记忆的衰减因子 + if (memory.decayFactor && memoryState.decayEnabled) { + adjustedScore *= memory.decayFactor // 直接应用衰减因子 + } + + // 4. 如果记忆与当前话题相关,提高分数 + if (topicId && memory.topicId === topicId) { + adjustedScore *= 1.2 // 提高20%的分数 + } + + // 5. 考虑访问频率,常用的记忆可能更相关 + if (memory.accessCount && memory.accessCount > 0) { + // 访问次数越多,提升越大,但有上限 + const accessBoost = Math.min(memory.accessCount / 10, 0.2) // 最多提升20% + adjustedScore *= (1 + accessBoost) + } + + // 6. 考虑关键词匹配 + if (memory.keywords && memory.keywords.length > 0) { + const queryLower = query.toLowerCase() + const keywordMatches = memory.keywords.filter(keyword => + queryLower.includes(keyword.toLowerCase()) + ).length + + if (keywordMatches > 0) { + // 关键词匹配越多,提升越大 + const keywordBoost = Math.min(keywordMatches * 0.1, 0.3) // 最多提升30% + adjustedScore *= (1 + keywordBoost) + } + } + + // 返回调整后的推荐 + return { + ...rec, + relevanceScore: adjustedScore + } + }).sort((a, b) => b.relevanceScore - a.relevanceScore) // 按调整后的分数重新排序 + } +} + +// 导出 ContextualMemoryService 的单例 +export const contextualMemoryService = new ContextualMemoryService() diff --git a/src/renderer/src/services/MemoryService.ts b/src/renderer/src/services/MemoryService.ts index dd2ce8484a..bec80e933d 100644 --- a/src/renderer/src/services/MemoryService.ts +++ b/src/renderer/src/services/MemoryService.ts @@ -16,9 +16,15 @@ import { updateMemoryPriorities, accessMemory, Memory, - saveMemoryData // <-- 添加 saveMemoryData + saveMemoryData, + updateCurrentRecommendations, + setRecommending, + clearCurrentRecommendations, + MemoryRecommendation } from '@renderer/store/memory' import { useCallback, useEffect, useRef } from 'react' // Add useRef back +import { contextualMemoryService } from './ContextualMemoryService' // Import contextual memory service +import { Message } from '@renderer/types' // Import Message type // 计算对话复杂度,用于调整分析深度 const calculateConversationComplexity = (conversation: string): 'low' | 'medium' | 'high' => { @@ -167,6 +173,162 @@ ${conversation} // This function definition is a duplicate, removing it. +/** + * 获取上下文感知的记忆推荐 + * @param messages - 当前对话的消息列表 + * @param topicId - 当前对话的话题ID + * @param limit - 返回的最大记忆数量 + * @returns 推荐的记忆列表,按相关性排序 + */ +export const getContextualMemoryRecommendations = async ( + messages: Message[], + topicId: string, + limit: number = 5 +): Promise => { + try { + // 获取当前状态 + const state = store.getState().memory + + // 检查上下文感知记忆推荐是否启用 + if (!state?.contextualRecommendationEnabled) { + console.log('[ContextualMemory] Contextual recommendation is not enabled') + return [] + } + + // 设置推荐状态 + store.dispatch(setRecommending(true)) + + // 调用上下文感知记忆服务获取推荐 + const recommendations = await contextualMemoryService.getContextualMemoryRecommendations( + messages, + topicId, + limit + ) + + // 转换为Redux状态中的推荐格式 + const memoryRecommendations: MemoryRecommendation[] = recommendations.map(rec => ({ + memoryId: rec.memory.id, + relevanceScore: rec.relevanceScore, + source: rec.source, + matchReason: rec.matchReason + })) + + // 更新Redux状态 + store.dispatch(updateCurrentRecommendations(memoryRecommendations)) + + // 重置推荐状态 + store.dispatch(setRecommending(false)) + + return memoryRecommendations + } catch (error) { + console.error('[ContextualMemory] Error getting contextual memory recommendations:', error) + store.dispatch(setRecommending(false)) + return [] + } +} + +/** + * 基于当前对话主题自动提取相关记忆 + * @param topicId - 当前对话的话题ID + * @param limit - 返回的最大记忆数量 + * @returns 与当前主题相关的记忆列表 + */ +export const getTopicRelatedMemories = async ( + topicId: string, + limit: number = 10 +): Promise => { + try { + // 获取当前状态 + const state = store.getState().memory + + // 检查上下文感知记忆推荐是否启用 + if (!state?.contextualRecommendationEnabled) { + console.log('[ContextualMemory] Contextual recommendation is not enabled') + return [] + } + + // 设置推荐状态 + store.dispatch(setRecommending(true)) + + // 调用上下文感知记忆服务获取推荐 + const recommendations = await contextualMemoryService.getTopicRelatedMemories( + topicId, + limit + ) + + // 转换为Redux状态中的推荐格式 + const memoryRecommendations: MemoryRecommendation[] = recommendations.map(rec => ({ + memoryId: rec.memory.id, + relevanceScore: rec.relevanceScore, + source: rec.source, + matchReason: rec.matchReason + })) + + // 更新Redux状态 + store.dispatch(updateCurrentRecommendations(memoryRecommendations)) + + // 重置推荐状态 + store.dispatch(setRecommending(false)) + + return memoryRecommendations + } catch (error) { + console.error('[ContextualMemory] Error getting topic-related memories:', error) + store.dispatch(setRecommending(false)) + return [] + } +} + +/** + * 使用AI分析当前对话上下文,提取关键信息并推荐相关记忆 + * @param messages - 当前对话的消息列表 + * @param limit - 返回的最大记忆数量 + * @returns 基于AI分析的相关记忆推荐 + */ +export const getAIEnhancedMemoryRecommendations = async ( + messages: Message[], + limit: number = 5 +): Promise => { + try { + // 获取当前状态 + const state = store.getState().memory + + // 检查上下文感知记忆推荐是否启用 + if (!state?.contextualRecommendationEnabled) { + console.log('[ContextualMemory] Contextual recommendation is not enabled') + return [] + } + + // 设置推荐状态 + store.dispatch(setRecommending(true)) + + // 调用上下文感知记忆服务获取推荐 + const recommendations = await contextualMemoryService.getAIEnhancedMemoryRecommendations( + messages, + limit + ) + + // 转换为Redux状态中的推荐格式 + const memoryRecommendations: MemoryRecommendation[] = recommendations.map(rec => ({ + memoryId: rec.memory.id, + relevanceScore: rec.relevanceScore, + source: rec.source, + matchReason: rec.matchReason + })) + + // 更新Redux状态 + store.dispatch(updateCurrentRecommendations(memoryRecommendations)) + + // 重置推荐状态 + store.dispatch(setRecommending(false)) + + return memoryRecommendations + } catch (error) { + console.error('[ContextualMemory] Error getting AI-enhanced memory recommendations:', error) + store.dispatch(setRecommending(false)) + return [] + } +} + // 记忆服务钩子 - 重构版 export const useMemoryService = () => { const dispatch = useAppDispatch() @@ -174,6 +336,8 @@ export const useMemoryService = () => { const isActive = useAppSelector((state) => state.memory?.isActive || false) const autoAnalyze = useAppSelector((state) => state.memory?.autoAnalyze || false) const analyzeModel = useAppSelector((state) => state.memory?.analyzeModel || null) + const contextualRecommendationEnabled = useAppSelector((state) => state.memory?.contextualRecommendationEnabled || false) + const autoRecommendMemories = useAppSelector((state) => state.memory?.autoRecommendMemories || false) // 使用 useCallback 定义分析函数,但减少依赖项 // 增加可选的 topicId 参数,允许分析指定的话题 @@ -559,8 +723,85 @@ ${newConversation} // 依赖项只包含决定是否启动定时器的设置 }, [isActive, autoAnalyze, analyzeModel]) - // 返回分析函数和记忆访问函数,以便在其他组件中使用 - return { analyzeAndAddMemories, recordMemoryAccess } + // 获取上下文感知记忆推荐 + const getContextualRecommendations = useCallback( + async (messages: Message[], topicId: string, limit: number = 5) => { + if (!contextualRecommendationEnabled) { + console.log('[ContextualMemory] Contextual recommendation is not enabled') + return [] + } + + return await getContextualMemoryRecommendations(messages, topicId, limit) + }, + [contextualRecommendationEnabled] + ) + + // 获取主题相关记忆 + const getTopicRecommendations = useCallback( + async (topicId: string, limit: number = 10) => { + if (!contextualRecommendationEnabled) { + console.log('[ContextualMemory] Contextual recommendation is not enabled') + return [] + } + + return await getTopicRelatedMemories(topicId, limit) + }, + [contextualRecommendationEnabled] + ) + + // 获取AI增强记忆推荐 + const getAIRecommendations = useCallback( + async (messages: Message[], limit: number = 5) => { + if (!contextualRecommendationEnabled) { + console.log('[ContextualMemory] Contextual recommendation is not enabled') + return [] + } + + return await getAIEnhancedMemoryRecommendations(messages, limit) + }, + [contextualRecommendationEnabled] + ) + + // 清除当前记忆推荐 + const clearRecommendations = useCallback(() => { + dispatch(clearCurrentRecommendations()) + }, [dispatch]) + + // 自动记忆推荐定时器 + useEffect(() => { + if (!contextualRecommendationEnabled || !autoRecommendMemories) { + return + } + + console.log('[ContextualMemory] Setting up auto recommendation timer...') + + // 每5分钟自动推荐一次记忆 + const intervalId = setInterval(() => { + const state = store.getState() + const currentTopicId = state.messages.currentTopic?.id + const messages = currentTopicId ? state.messages.messagesByTopic?.[currentTopicId] || [] : [] + + if (currentTopicId && messages.length > 0) { + console.log('[ContextualMemory] Auto recommendation triggered') + getContextualRecommendations(messages, currentTopicId) + } + }, 5 * 60 * 1000) // 5分钟 + + return () => { + console.log('[ContextualMemory] Clearing auto recommendation timer') + clearInterval(intervalId) + } + }, [contextualRecommendationEnabled, autoRecommendMemories, getContextualRecommendations]) + + // 返回分析函数、记忆访问函数和记忆推荐函数,以便在其他组件中使用 + return { + analyzeAndAddMemories, + recordMemoryAccess, + getContextualRecommendations, + getTopicRecommendations, + getAIRecommendations, + clearRecommendations + } } // 手动添加短记忆 @@ -722,7 +963,7 @@ ${newConversation} // 首先尝试匹配带有数字或短横线的列表项 const listItemRegex = /(?:^|\n)(?:\d+\.\s*|\-\s*)(.+?)(?=\n\d+\.\s*|\n\-\s*|\n\n|$)/gs - let match + let match: RegExpExecArray | null while ((match = listItemRegex.exec(result)) !== null) { if (match[1] && match[1].trim()) { extractedLines.push(match[1].trim()) @@ -832,13 +1073,24 @@ export const applyMemoriesToPrompt = (systemPrompt: string): string => { const state = store.getState() // Use imported store // 确保 state.memory 存在,如果不存在则提供默认值 - const { isActive, memories, memoryLists, shortMemoryActive, shortMemories, priorityManagementEnabled } = state.memory || { + const { + isActive, + memories, + memoryLists, + shortMemoryActive, + shortMemories, + priorityManagementEnabled, + contextualRecommendationEnabled, + currentRecommendations + } = state.memory || { isActive: false, memories: [], memoryLists: [], shortMemoryActive: false, shortMemories: [], - priorityManagementEnabled: false + priorityManagementEnabled: false, + contextualRecommendationEnabled: false, + currentRecommendations: [] } // 获取当前话题ID @@ -857,6 +1109,50 @@ export const applyMemoriesToPrompt = (systemPrompt: string): string => { let result = systemPrompt let hasContent = false + // 处理上下文感知记忆推荐 + if (contextualRecommendationEnabled && currentRecommendations && currentRecommendations.length > 0) { + // 获取推荐记忆的详细信息 + const recommendedMemories: Array<{content: string, source: string, reason: string}> = [] + + // 处理每个推荐记忆 + for (const recommendation of currentRecommendations) { + // 根据来源查找记忆 + let memory: any = null + if (recommendation.source === 'long-term') { + memory = memories.find(m => m.id === recommendation.memoryId) + } else if (recommendation.source === 'short-term') { + memory = shortMemories.find(m => m.id === recommendation.memoryId) + } + + if (memory) { + recommendedMemories.push({ + content: memory.content, + source: recommendation.source === 'long-term' ? '长期记忆' : '短期记忆', + reason: recommendation.matchReason || '与当前对话相关' + }) + + // 记录访问 + store.dispatch(accessMemory({ + id: memory.id, + isShortMemory: recommendation.source === 'short-term' + })) + } + } + + if (recommendedMemories.length > 0) { + // 构建推荐记忆提示词 + const recommendedMemoryPrompt = recommendedMemories + .map((memory, index) => `${index + 1}. ${memory.content} (来源: ${memory.source}, 原因: ${memory.reason})`) + .join('\n') + + console.log('[Memory] Contextual memory recommendations:', recommendedMemoryPrompt) + + // 添加推荐记忆到提示词 + result = `${result}\n\n当前对话的相关记忆(按相关性排序):\n${recommendedMemoryPrompt}` + hasContent = true + } + } + // 处理短记忆 if (shortMemoryActive && shortMemories && shortMemories.length > 0 && currentTopicId) { // 获取当前话题的短记忆 diff --git a/src/renderer/src/store/memory.ts b/src/renderer/src/store/memory.ts index 756220636d..1a5c46266c 100644 --- a/src/renderer/src/store/memory.ts +++ b/src/renderer/src/store/memory.ts @@ -76,6 +76,14 @@ export interface UserInterest { lastUpdated: string // 上次更新时间 } +// 记忆推荐结果接口 +export interface MemoryRecommendation { + memoryId: string + relevanceScore: number + source: 'long-term' | 'short-term' + matchReason?: string +} + export interface MemoryState { memoryLists: MemoryList[] // 记忆列表 memories: Memory[] // 所有记忆项 @@ -110,6 +118,14 @@ export interface MemoryState { freshnessEnabled: boolean // 是否启用记忆鲜度评估 decayRate: number // 记忆衰减速率(0-1) lastPriorityUpdate: number // 上次优先级更新时间 + + // 上下文感知记忆推荐相关 + contextualRecommendationEnabled: boolean // 是否启用上下文感知记忆推荐 + autoRecommendMemories: boolean // 是否自动推荐记忆 + recommendationThreshold: number // 推荐阈值(0-1) + currentRecommendations: MemoryRecommendation[] // 当前的记忆推荐 + isRecommending: boolean // 是否正在推荐记忆 + lastRecommendTime: number | null // 上次推荐时间 } // 创建默认记忆列表 @@ -167,7 +183,15 @@ const initialState: MemoryState = { decayEnabled: true, // 默认启用记忆衰减功能 freshnessEnabled: true, // 默认启用记忆鲜度评估 decayRate: 0.05, // 默认衰减速率,每天减少5% - lastPriorityUpdate: Date.now() // 初始化为当前时间 + lastPriorityUpdate: Date.now(), // 初始化为当前时间 + + // 上下文感知记忆推荐相关 + contextualRecommendationEnabled: true, // 默认启用上下文感知记忆推荐 + autoRecommendMemories: true, // 默认自动推荐记忆 + recommendationThreshold: 0.7, // 默认推荐阈值 + currentRecommendations: [], // 初始化空的推荐列表 + isRecommending: false, // 初始化为非推荐状态 + lastRecommendTime: null // 初始化为空 } const memorySlice = createSlice({ @@ -685,6 +709,37 @@ const memorySlice = createSlice({ memory.lastAccessedAt = now } } + }, + + // 设置上下文感知记忆推荐是否启用 + setContextualRecommendationEnabled: (state, action: PayloadAction) => { + state.contextualRecommendationEnabled = action.payload + }, + + // 设置是否自动推荐记忆 + setAutoRecommendMemories: (state, action: PayloadAction) => { + state.autoRecommendMemories = action.payload + }, + + // 设置推荐阈值 + setRecommendationThreshold: (state, action: PayloadAction) => { + state.recommendationThreshold = action.payload + }, + + // 更新当前的记忆推荐 + updateCurrentRecommendations: (state, action: PayloadAction) => { + state.currentRecommendations = action.payload + state.lastRecommendTime = Date.now() + }, + + // 设置是否正在推荐记忆 + setRecommending: (state, action: PayloadAction) => { + state.isRecommending = action.payload + }, + + // 清除当前的记忆推荐 + clearCurrentRecommendations: (state) => { + state.currentRecommendations = [] } }, extraReducers: (builder) => { @@ -695,6 +750,23 @@ const memorySlice = createSlice({ state.memoryLists = action.payload.memoryLists || state.memoryLists state.memories = action.payload.memories || state.memories state.shortMemories = action.payload.shortMemories || state.shortMemories + + // 更新模型选择 + if (action.payload.analyzeModel) { + state.analyzeModel = action.payload.analyzeModel + console.log('[Memory Reducer] Loaded analyze model:', action.payload.analyzeModel) + } + + if (action.payload.shortMemoryAnalyzeModel) { + state.shortMemoryAnalyzeModel = action.payload.shortMemoryAnalyzeModel + console.log('[Memory Reducer] Loaded short memory analyze model:', action.payload.shortMemoryAnalyzeModel) + } + + if (action.payload.vectorizeModel) { + state.vectorizeModel = action.payload.vectorizeModel + console.log('[Memory Reducer] Loaded vectorize model:', action.payload.vectorizeModel) + } + log.info('Memory data loaded into state') } }) @@ -747,7 +819,15 @@ export const { setDecayRate, updateMemoryPriorities, updateMemoryFreshness, - accessMemory + accessMemory, + + // 上下文感知记忆推荐相关的action + setContextualRecommendationEnabled, + setAutoRecommendMemories, + setRecommendationThreshold, + updateCurrentRecommendations, + setRecommending, + clearCurrentRecommendations } = memorySlice.actions // 加载记忆数据的异步 thunk From 51fc167b2aebaa129cee7b22eed08610988d3f4d Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Sun, 13 Apr 2025 23:34:58 +0800 Subject: [PATCH 09/11] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E8=AE=B0?= =?UTF-8?q?=E5=BF=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/services/MemoryFileService.ts | 30 +++++++++++++++-- src/renderer/src/i18n/locales/zh-cn.json | 20 +++++++++++ .../pages/settings/MemorySettings/index.tsx | 31 ++++------------- src/renderer/src/services/MemoryService.ts | 33 ++++++++++++------- src/renderer/src/store/memory.ts | 23 ++++++++++--- 5 files changed, 95 insertions(+), 42 deletions(-) diff --git a/src/main/services/MemoryFileService.ts b/src/main/services/MemoryFileService.ts index 258f7a3366..5c01294c31 100644 --- a/src/main/services/MemoryFileService.ts +++ b/src/main/services/MemoryFileService.ts @@ -81,8 +81,34 @@ export class MemoryFileService { // 如果文件不存在或读取失败,使用空对象 } - // 合并数据,优先使用新数据 - const mergedData = { ...existingData, ...data } + // 合并数据,注意数组的处理 + const mergedData = { ...existingData } + + // 处理每个属性 + 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)) + + // 将新记忆添加到现有记忆中,避免重复 + value.forEach(item => { + if (item.id && !existingIds.has(item.id)) { + mergedData[key].push(item) + existingIds.add(item.id) + } + }) + } else { + // 其他数组属性,使用新值 + mergedData[key] = value + } + } else { + // 非数组属性,直接使用新值 + mergedData[key] = value + } + }) // 保存合并后的数据 await fs.writeFile(memoryDataPath, JSON.stringify(mergedData, null, 2)) diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 60801b6a47..c49627a549 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1070,6 +1070,26 @@ "enable": "启用智能优先级管理", "enableTip": "启用后,系统将根据重要性、访问频率和时间因素自动排序记忆", "decay": "记忆衰减", + "decayRate": "衰减速率", + "decayRateTip": "值越大,记忆衰减越快。0.05表示每天衰减5%", + "freshness": "记忆鲜度", + "freshnessTip": "考虑记忆的创建时间和最后访问时间,优先显示较新的记忆", + "updateNow": "立即更新", + "updateNowTip": "立即更新所有记忆的优先级排序", + "update": "更新" + }, + "contextualRecommendation": { + "title": "上下文感知记忆推荐", + "description": "根据当前对话上下文,智能推荐相关的记忆内容。", + "enable": "启用上下文感知记忆推荐", + "enableTip": "启用后,系统将根据当前对话上下文自动推荐相关记忆", + "autoRecommend": "自动推荐记忆", + "autoRecommendTip": "启用后,系统将定期自动分析当前对话并推荐相关记忆", + "threshold": "推荐阈值", + "thresholdTip": "设置记忆推荐的相似度阈值,值越高要求越严格", + "clearRecommendations": "清除当前推荐", + "clearRecommendationsTip": "清除当前的记忆推荐列表", + "clear": "清除", "decayTip": "随着时间推移,未访问的记忆重要性会逐渐降低", "decayRate": "衰减速率", "decayRateTip": "值越大,记忆衰减越快。0.05表示每天衰减5%", diff --git a/src/renderer/src/pages/settings/MemorySettings/index.tsx b/src/renderer/src/pages/settings/MemorySettings/index.tsx index cebea3d924..e072b54fae 100644 --- a/src/renderer/src/pages/settings/MemorySettings/index.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/index.tsx @@ -9,7 +9,7 @@ import { import { useTheme } from '@renderer/context/ThemeProvider' import { TopicManager } from '@renderer/hooks/useTopic' import { analyzeAndAddShortMemories, useMemoryService } from '@renderer/services/MemoryService' -import store, { useAppDispatch, useAppSelector } from '@renderer/store' +import { useAppDispatch, useAppSelector } from '@renderer/store' import { addMemory, clearMemories, @@ -19,7 +19,8 @@ import { setAnalyzing, setAutoAnalyze, setMemoryActive, - setShortMemoryAnalyzeModel + setShortMemoryAnalyzeModel, + saveMemoryData } from '@renderer/store/memory' import { Topic } from '@renderer/types' import { Button, Empty, Input, List, message, Modal, Radio, Select, Switch, Tabs, Tag, Tooltip } from 'antd' @@ -260,18 +261,9 @@ const MemorySettings: FC = () => { dispatch(setAnalyzeModel(modelId)) console.log('[Memory Settings] Analyze model set:', modelId) - // 手动保存到JSON文件 + // 使用Redux Thunk保存到JSON文件 try { - const state = store.getState().memory - await window.api.memory.saveData({ - analyzeModel: modelId, - shortMemoryAnalyzeModel: state.shortMemoryAnalyzeModel, - vectorizeModel: state.vectorizeModel, - // 确保其他必要的数据也被保存 - memoryLists: state.memoryLists || [], - memories: state.memories || [], - shortMemories: state.shortMemories || [] - }) + await dispatch(saveMemoryData({ analyzeModel: modelId })).unwrap() console.log('[Memory Settings] Analyze model saved to file successfully:', modelId) } catch (error) { console.error('[Memory Settings] Failed to save analyze model to file:', error) @@ -283,18 +275,9 @@ const MemorySettings: FC = () => { dispatch(setShortMemoryAnalyzeModel(modelId)) console.log('[Memory Settings] Short memory analyze model set:', modelId) - // 手动保存到JSON文件 + // 使用Redux Thunk保存到JSON文件 try { - const state = store.getState().memory - await window.api.memory.saveData({ - analyzeModel: state.analyzeModel, - shortMemoryAnalyzeModel: modelId, - vectorizeModel: state.vectorizeModel, - // 确保其他必要的数据也被保存 - memoryLists: state.memoryLists || [], - memories: state.memories || [], - shortMemories: state.shortMemories || [] - }) + await dispatch(saveMemoryData({ shortMemoryAnalyzeModel: modelId })).unwrap() console.log('[Memory Settings] Short memory analyze model saved to file successfully:', modelId) } catch (error) { console.error('[Memory Settings] Failed to save short memory analyze model to file:', error) diff --git a/src/renderer/src/services/MemoryService.ts b/src/renderer/src/services/MemoryService.ts index bec80e933d..da39f38cf9 100644 --- a/src/renderer/src/services/MemoryService.ts +++ b/src/renderer/src/services/MemoryService.ts @@ -899,33 +899,42 @@ export const analyzeAndAddShortMemories = async (topicId: string) => { // 构建短期记忆分析提示词,包含已有记忆和新对话 const prompt = ` -请对以下对话内容进行详细分析和总结,提取对当前对话至关重要的上下文信息。 +请对以下对话内容进行非常详细的分析和总结,提取对当前对话至关重要的上下文信息。请注意,这个分析将用于生成短期记忆,帮助AI理解当前对话的完整上下文。 分析要求: -1. 详细总结用户的每一句话中表达的关键信息、需求和意图 -2. 分析AI回复中的重要内容和对用户问题的解决方案 -3. 识别对话中的重要事实、数据和具体细节 -4. 捕捉对话的逻辑发展和转折点 +1. 非常详细地总结用户的每一句话中表达的关键信息、需求和意图 +2. 全面分析AI回复中的重要内容和对用户问题的解决方案 +3. 详细记录对话中的重要事实、数据、代码示例和具体细节 +4. 清晰捕捉对话的逻辑发展、转折点和关键决策 5. 提取对理解当前对话上下文必不可少的信息 +6. 记录用户提出的具体问题和关注点 +7. 捕捉用户在对话中表达的偏好、困惑和反馈 +8. 记录对话中提到的文件、路径、变量名等具体技术细节 -与长期记忆不同,短期记忆应该关注当前对话的具体细节和上下文,而不是用户的长期偏好。每条短期记忆应该是对对话片段的精准总结,确保不遗漏任何重要信息。 +与长期记忆不同,短期记忆应该非常详细地关注当前对话的具体细节和上下文。每条短期记忆应该是对对话片段的精准总结,确保不遗漏任何重要信息。 + +请注意,对于长对话(超过5万字),您应该生成至少15-20条详细的记忆条目,确保完整捕捉对话的所有重要方面。对于超长对话(超过8万字),应生成至少20-30条记忆条目。 ${ existingMemoriesContent ? `以下是已经提取的重要信息: ${existingMemoriesContent} -请分析新的对话内容,提取出新的重要信息,避免重复已有信息。确保新提取的信息与已有信息形成连贯的上下文理解。` - : '请对对话进行全面分析,确保不遗漏任何重要细节。每条总结应该是完整的句子,清晰表达一个重要的上下文信息。' +请分析新的对话内容,提取出新的重要信息,避免重复已有信息。确保新提取的信息与已有信息形成连贯的上下文理解。对于新的对话内容,请提供非常详细的分析。` + : '请对对话进行非常全面和详细的分析,确保不遗漏任何重要细节。每条总结应该是完整的句子,清晰表达一个重要的上下文信息。请确保总结足够详细,以便在没有原始对话的情况下也能理解完整的上下文。' } 输出格式: -- 提供完整的上下文总结,数量不限,确保覆盖所有重要信息 -- 每条总结应该是一个完整的句子 +- 提供非常详细的上下文总结,数量不限,确保覆盖所有重要信息 +- 每条总结应该是一个完整的句子,包含充分的上下文信息 - 确保总结内容精准、具体且与当前对话直接相关 - 按重要性排序,最重要的信息放在前面 -- 对于复杂的对话,应提供足够多的条目(至少5-10条)以确保上下文的完整性 -- 如果对话内容简单,可以少于5条,但必须确保完整捕捉所有重要信息 +- 对于复杂的对话,必须提供足够多的条目(至少15-20条)以确保上下文的完整性 +- 对于技术内容,请包含具体的文件名、路径、变量名、函数名等技术细节 +- 对于代码相关的对话,请记录关键的代码片段和实现细节 +- 如果对话内容简单,可以少于15条,但必须确保完整捕捉所有重要信息 + +请记住,您的分析应该非常详细,不要过于简化或概括。对于8万字的对话,100字的总结是远远不够的,应该提供至少500-1000字的详细总结,分成多个条目。 如果没有找到新的重要信息,请返回空字符串。 diff --git a/src/renderer/src/store/memory.ts b/src/renderer/src/store/memory.ts index 1a5c46266c..18e3394f86 100644 --- a/src/renderer/src/store/memory.ts +++ b/src/renderer/src/store/memory.ts @@ -1,6 +1,7 @@ import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' import { nanoid } from 'nanoid' import log from 'electron-log' +import store from '@renderer/store' // 记忆列表接口 export interface MemoryList { @@ -851,12 +852,26 @@ export const saveMemoryData = createAsyncThunk( 'memory/saveData', async (data: Partial) => { try { - // log.info('Saving memory data to file...') // Removed direct log call from renderer - const result = await window.api.memory.saveData(data) - // log.info('Memory data saved successfully') // Removed direct log call from renderer + console.log('[Memory] Saving memory data to file...', Object.keys(data)) + + // 确保数据完整性 + const state = store.getState().memory + const completeData = { + ...data, + // 如果没有提供这些字段,则使用当前状态中的值 + memoryLists: data.memoryLists || state.memoryLists, + memories: data.memories || state.memories, + shortMemories: data.shortMemories || state.shortMemories, + analyzeModel: data.analyzeModel || state.analyzeModel, + shortMemoryAnalyzeModel: data.shortMemoryAnalyzeModel || state.shortMemoryAnalyzeModel, + vectorizeModel: data.vectorizeModel || state.vectorizeModel + } + + const result = await window.api.memory.saveData(completeData) + console.log('[Memory] Memory data saved successfully') return result } catch (error) { - console.error('Failed to save memory data:', error) // Use console.error instead of log.error + console.error('[Memory] Failed to save memory data:', error) return false } } From 370ee605379373cf7edc6dc6035444663cc7d0a2 Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Mon, 14 Apr 2025 17:55:25 +0800 Subject: [PATCH 10/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=BA=9Bbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/shared/IpcChannel.ts | 7 +- src/main/ipc.ts | 18 + src/main/services/MemoryFileService.ts | 365 +++++++++---- src/preload/index.d.ts | 5 +- src/preload/index.ts | 5 +- .../src/components/MemoryProvider.tsx | 131 ++++- .../components/Popups/ShortMemoryPopup.tsx | 59 +- src/renderer/src/i18n/locales/en-us.json | 15 + src/renderer/src/i18n/locales/zh-cn.json | 21 +- .../CollapsibleShortMemoryManager.tsx | 506 ++++++++++++------ .../ContextualRecommendationSettings.tsx | 87 +-- .../HistoricalContextSettings.tsx | 111 ++++ .../MemoryDeduplicationPanel.tsx | 32 +- .../MemorySettings/MemoryListManager.tsx | 64 ++- .../PriorityManagementSettings.tsx | 115 ++-- .../MemorySettings/ShortMemoryManager.tsx | 48 +- .../pages/settings/MemorySettings/index.tsx | 341 ++++++++---- .../providers/AiProvider/OpenAIProvider.ts | 14 +- .../src/services/HistoricalContextService.ts | 241 +++++++++ .../services/MemoryDeduplicationService.ts | 59 +- src/renderer/src/services/MemoryService.ts | 396 +++++++++++--- src/renderer/src/store/memory.ts | 312 ++++++++++- src/renderer/src/store/settings.ts | 7 + src/renderer/src/utils/prompt.ts | 2 +- 24 files changed, 2377 insertions(+), 584 deletions(-) create mode 100644 src/renderer/src/pages/settings/MemorySettings/HistoricalContextSettings.tsx create mode 100644 src/renderer/src/services/HistoricalContextService.ts 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 && ( - diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 86d60b08c5..5f024604f6 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -498,6 +498,12 @@ "copied": "Copied!", "copy.failed": "Copy failed", "copy.success": "Copied!", + "copy_id": "Copy Message ID", + "id_copied": "Message ID copied", + "id_found": "Original message found", + "reference": "Reference message", + "reference.error": "Failed to find original message", + "referenced_message": "Referenced Message", "error.chunk_overlap_too_large": "Chunk overlap cannot be greater than chunk size", "error.dimension_too_large": "Content size is too large", "error.enter.api.host": "Please enter your API host first", @@ -1055,6 +1061,8 @@ "startingAnalysis": "Starting analysis...", "cannotAnalyze": "Cannot analyze, please check settings", "resetAnalyzingState": "Reset Analysis State", + "filterSensitiveInfo": "Filter Sensitive Information", + "filterSensitiveInfoTip": "When enabled, memory function will not extract API keys, passwords, or other sensitive information", "resetLongTermMemory": "Reset Analysis Markers", "resetLongTermMemorySuccess": "Long-term memory analysis markers reset", "resetLongTermMemoryNoChange": "No analysis markers to reset", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 5476f63f6f..89f6ea759b 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -498,6 +498,12 @@ "copied": "已复制", "copy.failed": "复制失败", "copy.success": "复制成功", + "copy_id": "复制消息ID", + "id_copied": "消息ID已复制", + "id_found": "已找到原始消息", + "reference": "引用消息", + "reference.error": "无法找到原始消息", + "referenced_message": "引用的消息", "error.chunk_overlap_too_large": "分段重叠不能大于分段大小", "error.dimension_too_large": "内容尺寸过大", "error.enter.api.host": "请输入您的 API 地址", @@ -1060,6 +1066,8 @@ "startingAnalysis": "开始分析...", "cannotAnalyze": "无法分析,请检查设置", "resetAnalyzingState": "重置分析状态", + "filterSensitiveInfo": "过滤敏感信息", + "filterSensitiveInfoTip": "启用后,记忆功能将不会提取API密钥、密码等敏感信息", "resetLongTermMemory": "重置分析标记", "resetLongTermMemorySuccess": "长期记忆分析标记已重置", "resetLongTermMemoryNoChange": "没有需要重置的分析标记", diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 232b66a2df..ae9806d97d 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -27,7 +27,7 @@ import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon' import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import FileManager from '@renderer/services/FileManager' -import { checkRateLimit, getUserMessage } from '@renderer/services/MessagesService' +import { checkRateLimit, findMessageById, getUserMessage } from '@renderer/services/MessagesService' import { getModelUniqId } from '@renderer/services/ModelService' import { estimateMessageUsage, estimateTextTokens as estimateTxtTokens } from '@renderer/services/TokenService' import { translateText } from '@renderer/services/TranslateService' @@ -180,9 +180,135 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE) try { + // 检查用户输入是否包含消息ID + const uuidRegex = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i + + // 从文本中提取所有消息ID + const matches = text.match(new RegExp(uuidRegex, 'g')) + + // 如果只有ID且没有其他内容,则直接查找原始消息 + if (matches && matches.length > 0 && text.trim() === matches.join(' ')) { + try { + // 创建引用消息 + const userMessage = getUserMessage({ assistant, topic, type: 'text', content: '' }) + userMessage.referencedMessages = [] + + // 处理所有匹配到的ID + let foundAnyMessage = false + for (const messageId of matches) { + console.log(`[引用消息] 尝试查找消息ID: ${messageId}`) + const originalMessage = await findMessageById(messageId) + if (originalMessage) { + userMessage.referencedMessages.push({ + id: originalMessage.id, + content: originalMessage.content, + role: originalMessage.role, + createdAt: originalMessage.createdAt + }) + foundAnyMessage = true + console.log(`[引用消息] 找到消息ID: ${messageId}`) + } else { + console.log(`[引用消息] 未找到消息ID: ${messageId}`) + } + } + + if (foundAnyMessage) { + // 发送引用消息 + userMessage.usage = await estimateMessageUsage(userMessage) + currentMessageId.current = userMessage.id + + dispatch( + _sendMessage(userMessage, assistant, topic, { + mentions: mentionModels + }) + ) + + // 清空输入框 + setText('') + setFiles([]) + setTimeout(() => setText(''), 500) + setTimeout(() => resizeTextArea(), 0) + setExpend(false) + + window.message.success({ + content: + t('message.ids_found', { count: userMessage.referencedMessages.length }) || + `已找到${userMessage.referencedMessages.length}条原始消息`, + key: 'message-id-found' + }) + return + } else { + window.message.error({ + content: t('message.id_not_found') || '未找到原始消息', + key: 'message-id-not-found' + }) + } + } catch (error) { + console.error(`[引用消息] 查找消息ID时出错:`, error) + window.message.error({ content: t('message.id_error') || '查找原始消息时出错', key: 'message-id-error' }) + } + } + + // 如果不是单独的ID或者没有找到原始消息,则正常发送消息 + // 先检查消息内容是否包含消息ID,如果是则将其替换为空字符串 + let messageContent = text + + // 如果消息内容包含消息ID,则将其替换为空字符串 + if (matches && matches.length > 0) { + // 检查是否是纯消息ID + const isOnlyUUID = text.trim() === matches[0] + if (isOnlyUUID) { + messageContent = '' + } else { + // 如果消息内容包含消息ID,则将消息ID替换为空字符串 + for (const match of matches) { + messageContent = messageContent.replace(match, '') + } + // 去除多余的空格 + messageContent = messageContent.replace(/\s+/g, ' ').trim() + } + } + // Dispatch the sendMessage action with all options const uploadedFiles = await FileManager.uploadFiles(files) - const userMessage = getUserMessage({ assistant, topic, type: 'text', content: text }) + const userMessage = getUserMessage({ assistant, topic, type: 'text', content: messageContent }) + + // 如果消息内容包含消息ID,则添加引用 + if (matches && matches.length > 0) { + try { + // 初始化引用消息数组 + userMessage.referencedMessages = [] + + // 处理所有匹配到的ID + for (const messageId of matches) { + console.log(`[引用消息] 尝试查找消息ID作为引用: ${messageId}`) + const originalMessage = await findMessageById(messageId) + if (originalMessage) { + userMessage.referencedMessages.push({ + id: originalMessage.id, + content: originalMessage.content, + role: originalMessage.role, + createdAt: originalMessage.createdAt + }) + console.log(`[引用消息] 找到消息ID作为引用: ${messageId}`) + } else { + console.log(`[引用消息] 未找到消息ID作为引用: ${messageId}`) + } + } + + // 如果找到了引用消息,显示成功提示 + if (userMessage.referencedMessages.length > 0) { + window.message.success({ + content: + t('message.ids_found', { count: userMessage.referencedMessages.length }) || + `已找到${userMessage.referencedMessages.length}条原始消息`, + key: 'message-id-found' + }) + } + } catch (error) { + console.error(`[引用消息] 查找消息ID作为引用时出错:`, error) + } + } if (uploadedFiles) { userMessage.files = uploadedFiles @@ -388,6 +514,20 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const handleKeyDown = (event: React.KeyboardEvent) => { const isEnterPressed = event.keyCode == 13 + // 检查是否是消息ID格式 + if (isEnterPressed && !event.shiftKey) { + const uuidRegex = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i + const currentText = text.trim() + const isUUID = uuidRegex.test(currentText) && currentText.length === 36 + + if (isUUID) { + // 如果是消息ID格式,则不显示ID在对话中 + event.preventDefault() + sendMessage() + return + } + } + // 按下Tab键,自动选中${xxx} if (event.key === 'Tab' && inputFocus) { event.preventDefault() @@ -536,7 +676,19 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const onChange = (e: React.ChangeEvent) => { const newText = e.target.value - setText(newText) + + // 检查是否包含UUID格式的消息ID + const uuidRegex = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i + const matches = newText.match(new RegExp(uuidRegex, 'g')) + + // 如果输入的内容只是一个UUID,不更新文本框内容,直接处理引用 + if (matches && matches.length === 1 && newText.trim() === matches[0]) { + // 不立即更新文本框,等待用户按下回车键时再处理 + setText(newText) + } else { + // 正常更新文本框内容 + setText(newText) + } const textArea = textareaRef.current?.resizableTextArea?.textArea const cursorPosition = textArea?.selectionStart ?? 0 @@ -559,8 +711,41 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = async (event: ClipboardEvent) => { const clipboardText = event.clipboardData?.getData('text') if (clipboardText) { - // Prioritize the text when pasting. - // handled by the default event + // 检查粘贴的内容是否是消息ID + const uuidRegex = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i + const isUUID = uuidRegex.test(clipboardText.trim()) && clipboardText.trim().length === 36 + + if (isUUID) { + // 如果是消息ID,则阻止默认粘贴行为,自定义处理 + event.preventDefault() + + // 获取当前文本框的内容和光标位置 + const textArea = textareaRef.current?.resizableTextArea?.textArea + if (textArea) { + const currentText = textArea.value + const cursorPosition = textArea.selectionStart + const cursorEnd = textArea.selectionEnd + + // 如果有选中文本,则替换选中文本;否则在光标位置插入 + const newText = + currentText.substring(0, cursorPosition) + clipboardText.trim() + currentText.substring(cursorEnd) + + setText(newText) + + // 将光标移到插入的ID后面 + const newCursorPosition = cursorPosition + clipboardText.trim().length + setTimeout(() => { + if (textArea) { + textArea.focus() + textArea.setSelectionRange(newCursorPosition, newCursorPosition) + } + }, 0) + } else { + // 如果无法获取textArea,则直接设置文本 + setText(clipboardText.trim()) + } + } + // 其他文本内容由默认事件处理 } else { for (const file of event.clipboardData?.files || []) { event.preventDefault() diff --git a/src/renderer/src/pages/home/Markdown/Markdown.tsx b/src/renderer/src/pages/home/Markdown/Markdown.tsx index eb96421ea5..cb1d8bf859 100644 --- a/src/renderer/src/pages/home/Markdown/Markdown.tsx +++ b/src/renderer/src/pages/home/Markdown/Markdown.tsx @@ -61,18 +61,18 @@ const Markdown: FC = ({ message }) => { think: (props: any) => { // 将think标签内容渲染为带样式的div return ( -
-
- 思考过程: -
+
+
思考过程:
{props.children}
) diff --git a/src/renderer/src/pages/home/Messages/Message.tsx b/src/renderer/src/pages/home/Messages/Message.tsx index 9da880f17b..c5185da9dc 100644 --- a/src/renderer/src/pages/home/Messages/Message.tsx +++ b/src/renderer/src/pages/home/Messages/Message.tsx @@ -8,6 +8,7 @@ import { getModelUniqId } from '@renderer/services/ModelService' import { Assistant, Message, Topic } from '@renderer/types' import { classNames } from '@renderer/utils' import { Divider, Dropdown } from 'antd' +import { ItemType } from 'antd/es/menu/interface' import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -65,7 +66,11 @@ const MessageItem: FC = ({ const handleContextMenu = useCallback((e: React.MouseEvent) => { e.preventDefault() - const _selectedText = window.getSelection()?.toString() + const _selectedText = window.getSelection()?.toString() || '' + + // 无论是否选中文本,都设置上下文菜单位置 + setContextMenuPosition({ x: e.clientX, y: e.clientY }) + if (_selectedText) { const quotedText = _selectedText @@ -73,8 +78,10 @@ const MessageItem: FC = ({ .map((line) => `> ${line}`) .join('\n') + '\n-------------' setSelectedQuoteText(quotedText) - setContextMenuPosition({ x: e.clientX, y: e.clientY }) setSelectedText(_selectedText) + } else { + setSelectedQuoteText('') + setSelectedText('') } }, []) @@ -134,7 +141,7 @@ const MessageItem: FC = ({ {contextMenuPosition && (
@@ -181,23 +188,46 @@ const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolea : undefined } -const getContextMenuItems = (t: (key: string) => string, selectedQuoteText: string, selectedText: string) => [ - { - key: 'copy', - label: t('common.copy'), - onClick: () => { - navigator.clipboard.writeText(selectedText) - window.message.success({ content: t('message.copied'), key: 'copy-message' }) - } - }, - { - key: 'quote', - label: t('chat.message.quote'), - onClick: () => { - EventEmitter.emit(EVENT_NAMES.QUOTE_TEXT, selectedQuoteText) - } +const getContextMenuItems = ( + t: (key: string) => string, + selectedQuoteText: string, + selectedText: string, + message: Message +): ItemType[] => { + const items: ItemType[] = [] + + // 只有在选中文本时,才添加复制和引用选项 + if (selectedText) { + items.push({ + key: 'copy', + label: t('common.copy'), + onClick: () => { + navigator.clipboard.writeText(selectedText) + window.message.success({ content: t('message.copied'), key: 'copy-message' }) + } + }) + + items.push({ + key: 'quote', + label: t('chat.message.quote'), + onClick: () => { + EventEmitter.emit(EVENT_NAMES.QUOTE_TEXT, selectedQuoteText) + } + }) } -] + + // 添加复制消息ID选项,但不显示ID + items.push({ + key: 'copy_id', + label: t('message.copy_id') || '复制消息ID', + onClick: () => { + navigator.clipboard.writeText(message.id) + window.message.success({ content: t('message.id_copied') || '消息ID已复制', key: 'copy-message-id' }) + } + }) + + return items +} const MessageContainer = styled.div` display: flex; diff --git a/src/renderer/src/pages/home/Messages/MessageContent.tsx b/src/renderer/src/pages/home/Messages/MessageContent.tsx index a0c6ea21bf..1780406c04 100644 --- a/src/renderer/src/pages/home/Messages/MessageContent.tsx +++ b/src/renderer/src/pages/home/Messages/MessageContent.tsx @@ -4,7 +4,7 @@ import { getModelUniqId } from '@renderer/services/ModelService' import { Message, Model } from '@renderer/types' import { getBriefInfo } from '@renderer/utils' import { withMessageThought } from '@renderer/utils/formats' -import { Divider, Flex } from 'antd' +import { Collapse, Divider, Flex } from 'antd' import { clone } from 'lodash' import React, { Fragment, useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -203,8 +203,100 @@ const MessageContent: React.FC = ({ message: _message, model }) => { {message.mentions?.map((model) => {'@' + model.name})} - - + {message.referencedMessages && message.referencedMessages.length > 0 && ( +
+ {message.referencedMessages.map((refMsg, index) => ( + + + {t('message.referenced_message')}{' '} + {message.referencedMessages && message.referencedMessages.length > 1 + ? `(${index + 1}/${message.referencedMessages.length})` + : ''} + + {refMsg.role === 'user' ? t('common.you') : 'AI'} +
+ ), + extra: ( + { + e.stopPropagation() + navigator.clipboard.writeText(refMsg.id) + window.message.success({ + content: t('message.id_copied') || '消息ID已复制', + key: 'copy-reference-id' + }) + }}> + ID: {refMsg.id} + + ), + children: ( +
+
{refMsg.content}
+
+
+ ) + } + ]} + /> + ))} +
+ )} + + {/* 兼容旧版本的referencedMessage */} + {!message.referencedMessages && (message as any).referencedMessage && ( + + {t('message.referenced_message')} + + {(message as any).referencedMessage.role === 'user' ? t('common.you') : 'AI'} + +
+ ), + extra: ( + { + e.stopPropagation() + navigator.clipboard.writeText((message as any).referencedMessage.id) + window.message.success({ + content: t('message.id_copied') || '消息ID已复制', + key: 'copy-reference-id' + }) + }}> + ID: {(message as any).referencedMessage.id} + + ), + children: ( +
+
{(message as any).referencedMessage.content}
+
+
+ ) + } + ]} + /> + )} +
+ + +
{message.metadata?.generateImage && } {message.translatedContent && ( @@ -312,4 +404,132 @@ const SearchEntryPoint = styled.div` margin: 10px 2px; ` +// 引用消息样式 - 使用全局样式 +const referenceStyles = ` + .reference-collapse { + margin-bottom: 8px; + border: 1px solid var(--color-border) !important; + border-radius: 8px !important; + overflow: hidden; + background-color: var(--color-bg-1) !important; + + .ant-collapse-header { + padding: 2px 8px !important; + background-color: var(--color-bg-2); + border-bottom: 1px solid var(--color-border); + font-size: 10px; + display: flex; + justify-content: space-between; + height: 18px; + min-height: 18px; + line-height: 14px; + } + + .ant-collapse-expand-icon { + height: 18px; + line-height: 14px; + padding-top: 0 !important; + margin-top: -2px; + margin-right: 2px; + } + + .ant-collapse-header-text { + flex: 0 1 auto; + max-width: 70%; + } + + .ant-collapse-extra { + flex: 0 0 auto; + margin-left: 10px; + padding-right: 0; + position: relative; + right: 20px; + } + + .reference-header-label { + display: flex; + align-items: center; + gap: 4px; + height: 14px; + line-height: 14px; + } + + .reference-title { + font-weight: 500; + color: var(--color-text-1); + font-size: 10px; + } + + .reference-role { + color: var(--color-text-2); + font-size: 9px; + } + + .reference-id { + color: var(--color-text-3); + font-size: 9px; + cursor: pointer; + padding: 1px 4px; + border-radius: 3px; + transition: background-color 0.2s ease; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; + display: inline-block; + + &:hover { + background-color: var(--color-bg-3); + color: var(--color-text-2); + } + } + + .ant-collapse-extra { + margin-left: auto; + display: flex; + align-items: center; + } + + .ant-collapse-content-box { + padding: 12px !important; + padding-top: 8px !important; + padding-bottom: 2px !important; + } + + .reference-content { + max-height: 200px; + overflow-y: auto; + padding-bottom: 10px; + + .reference-text { + color: var(--color-text-1); + font-size: 14px; + white-space: pre-wrap; + word-break: break-word; + } + + .reference-bottom-spacing { + height: 10px; + } + } + } +` + +// 将样式添加到文档中 +try { + if (typeof document !== 'undefined') { + const styleElement = document.createElement('style') + styleElement.textContent = + referenceStyles + + ` + .message-content-tools { + margin-top: 20px; + } + ` + document.head.appendChild(styleElement) + } +} catch (error) { + console.error('Failed to add reference styles:', error) +} + export default React.memo(MessageContent) diff --git a/src/renderer/src/pages/home/Messages/MessageTokens.tsx b/src/renderer/src/pages/home/Messages/MessageTokens.tsx index 8a782447b8..caf69284e3 100644 --- a/src/renderer/src/pages/home/Messages/MessageTokens.tsx +++ b/src/renderer/src/pages/home/Messages/MessageTokens.tsx @@ -1,16 +1,93 @@ +import { LinkOutlined } from '@ant-design/icons' import { useRuntime } from '@renderer/hooks/useRuntime' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' +import { findMessageById } from '@renderer/services/MessagesService' import { Message } from '@renderer/types' +import { Button, Modal, Tooltip } from 'antd' import { t } from 'i18next' +import { useState } from 'react' import styled from 'styled-components' +// 添加引用消息的弹窗组件 +const ReferenceModal: React.FC<{ message: Message | null; visible: boolean; onClose: () => void }> = ({ + message, + visible, + onClose +}) => { + if (!message) return null + + return ( + + +
{message.role === 'user' ? t('common.you') : 'AI'}
+
{message.content}
+
{new Date(message.createdAt).toLocaleString()}
+
+
+ ) +} + +const ReferenceContent = styled.div` + padding: 10px; + + .message-role { + font-weight: bold; + margin-bottom: 5px; + } + + .message-content { + white-space: pre-wrap; + word-break: break-word; + margin-bottom: 10px; + } + + .message-time { + font-size: 12px; + color: var(--color-text-3); + text-align: right; + } +` + const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({ message, isLastMessage }) => { const { generating } = useRuntime() + const [isModalVisible, setIsModalVisible] = useState(false) + const [referencedMessage, setReferencedMessage] = useState(null) + + // 渲染引用消息弹窗 + const renderReferenceModal = () => { + return + } const locateMessage = () => { EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, false) } + const showReferenceModal = async (e: React.MouseEvent) => { + e.stopPropagation() // 防止触发父元素的点击事件 + + try { + // 复制ID到剪贴板,便于用户手动使用 + navigator.clipboard.writeText(message.id) + + // 查找原始消息 + const originalMessage = await findMessageById(message.id) + if (originalMessage) { + setReferencedMessage(originalMessage) + setIsModalVisible(true) + } + } catch (error) { + console.error('Failed to find referenced message:', error) + window.message.error({ + content: t('message.reference.error') || '无法找到原始消息', + key: 'reference-message-error' + }) + } + } + + const handleModalClose = () => { + setIsModalVisible(false) + } + if (!message.usage) { return
} @@ -18,7 +95,16 @@ const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({ if (message.role === 'user') { return ( - Tokens: {message?.usage?.total_tokens} + Tokens: {message?.usage?.total_tokens} + + diff --git a/src/renderer/src/pages/settings/MemorySettings/HistoricalContextSettings.tsx b/src/renderer/src/pages/settings/MemorySettings/HistoricalContextSettings.tsx index 2d11b9f829..91b9d3b4c5 100644 --- a/src/renderer/src/pages/settings/MemorySettings/HistoricalContextSettings.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/HistoricalContextSettings.tsx @@ -44,7 +44,7 @@ const HistoricalContextSettings: FC = () => { // 遍历所有服务商的模型找到匹配的模型 for (const provider of Object.values(providers)) { - const model = provider.models.find(m => m.id === historicalContextAnalyzeModel) + const model = provider.models.find((m) => m.id === historicalContextAnalyzeModel) if (model) { return `${model.name} | ${provider.name}` } @@ -57,15 +57,17 @@ const HistoricalContextSettings: FC = () => { {t('settings.memory.historicalContext.title') || '历史对话上下文'} - {t('settings.memory.historicalContext.description') || - '允许AI在需要时自动引用历史对话,以提供更连贯的回答。'} + {t('settings.memory.historicalContext.description') || '允许AI在需要时自动引用历史对话,以提供更连贯的回答。'} {t('settings.memory.historicalContext.enable') || '启用历史对话上下文'} - + @@ -75,8 +77,11 @@ const HistoricalContextSettings: FC = () => { {t('settings.memory.analyzeModel') || '分析模型'} - + @@ -86,7 +91,7 @@ const HistoricalContextSettings: FC = () => { let currentModel: { id: string; provider: string; name: string; group: string } | undefined if (historicalContextAnalyzeModel) { for (const provider of Object.values(providers)) { - const model = provider.models.find(m => m.id === historicalContextAnalyzeModel) + const model = provider.models.find((m) => m.id === historicalContextAnalyzeModel) if (model) { currentModel = model break @@ -99,8 +104,7 @@ const HistoricalContextSettings: FC = () => { handleModelChange(selectedModel.id) } }} - style={{ width: 300 }} - > + style={{ width: 300 }}> {historicalContextAnalyzeModel ? getSelectedModelName() : t('settings.memory.selectModel') || '选择模型'} diff --git a/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx b/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx index c68f5bb137..7a7f83b441 100644 --- a/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx @@ -6,9 +6,9 @@ import { deleteMemoryList, editMemoryList, MemoryList, + saveLongTermMemoryData, setCurrentMemoryList, - toggleMemoryListActive, - saveLongTermMemoryData + toggleMemoryListActive } from '@renderer/store/memory' import { Button, Empty, Input, List, Modal, Switch, Tooltip, Typography } from 'antd' import React, { useState } from 'react' @@ -76,10 +76,12 @@ const MemoryListManager: React.FC = ({ onSelectList }) = // 保存到长期记忆文件 try { const state = store.getState().memory - await dispatch(saveLongTermMemoryData({ - memoryLists: state.memoryLists, - currentListId: state.currentListId - })).unwrap() + 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) @@ -114,10 +116,12 @@ const MemoryListManager: React.FC = ({ onSelectList }) = // 保存到长期记忆文件 try { const state = store.getState().memory - await dispatch(saveLongTermMemoryData({ - memoryLists: state.memoryLists, - currentListId: state.currentListId - })).unwrap() + 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) @@ -133,10 +137,12 @@ const MemoryListManager: React.FC = ({ onSelectList }) = // 保存到长期记忆文件 try { const state = store.getState().memory - await dispatch(saveLongTermMemoryData({ - memoryLists: state.memoryLists, - currentListId: state.currentListId - })).unwrap() + 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) @@ -153,10 +159,12 @@ const MemoryListManager: React.FC = ({ onSelectList }) = // 保存到长期记忆文件 try { const state = store.getState().memory - await dispatch(saveLongTermMemoryData({ - memoryLists: state.memoryLists, - currentListId: state.currentListId - })).unwrap() + 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) diff --git a/src/renderer/src/pages/settings/MemorySettings/PriorityManagementSettings.tsx b/src/renderer/src/pages/settings/MemorySettings/PriorityManagementSettings.tsx index d2d2807117..497b2d96b8 100644 --- a/src/renderer/src/pages/settings/MemorySettings/PriorityManagementSettings.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/PriorityManagementSettings.tsx @@ -101,8 +101,11 @@ const PriorityManagementSettings: FC = () => { {t('settings.memory.priorityManagement.enable') || '启用智能优先级管理'} - + @@ -114,23 +117,19 @@ const PriorityManagementSettings: FC = () => { {t('settings.memory.priorityManagement.decay') || '记忆衰减'} - + - + {t('settings.memory.priorityManagement.decayRate') || '衰减速率'} - + @@ -161,30 +160,25 @@ const PriorityManagementSettings: FC = () => { {t('settings.memory.priorityManagement.freshness') || '记忆鲜度'} - + - + {t('settings.memory.priorityManagement.updateNow') || '立即更新优先级'} - + - diff --git a/src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx b/src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx index be6d9a4ed1..3a439b2b42 100644 --- a/src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx @@ -4,8 +4,8 @@ import { useAppDispatch, useAppSelector } from '@renderer/store' import store from '@renderer/store' import { deleteShortMemory, setShortMemoryActive } from '@renderer/store/memory' import { Button, Empty, Input, List, Switch, Tooltip, Typography } from 'antd' -import { useState, useCallback } from 'react' import _ from 'lodash' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' const { Title } = Typography @@ -35,46 +35,52 @@ const ShortMemoryManager = () => { } // 添加新的短记忆 - 使用防抖减少频繁更新 - const handleAddMemory = useCallback(_.debounce(() => { - if (newMemoryContent.trim() && currentTopicId) { - addShortMemoryItem(newMemoryContent.trim(), currentTopicId) - setNewMemoryContent('') // 清空输入框 - } - }, 300), [newMemoryContent, currentTopicId]) + const handleAddMemory = useCallback( + _.debounce(() => { + if (newMemoryContent.trim() && currentTopicId) { + addShortMemoryItem(newMemoryContent.trim(), currentTopicId) + setNewMemoryContent('') // 清空输入框 + } + }, 300), + [newMemoryContent, currentTopicId] + ) // 删除短记忆 - 直接删除无需确认,使用节流避免频繁删除操作 - const handleDeleteMemory = useCallback(_.throttle(async (id: string) => { - // 先从当前状态中获取要删除的记忆之外的所有记忆 - const state = store.getState().memory - const filteredShortMemories = state.shortMemories.filter(memory => memory.id !== id) + const handleDeleteMemory = useCallback( + _.throttle(async (id: string) => { + // 先从当前状态中获取要删除的记忆之外的所有记忆 + const state = store.getState().memory + const filteredShortMemories = state.shortMemories.filter((memory) => memory.id !== id) - // 执行删除操作 - dispatch(deleteShortMemory(id)) + // 执行删除操作 + dispatch(deleteShortMemory(id)) - // 直接使用 window.api.memory.saveData 方法保存过滤后的列表 - try { - // 加载当前文件数据 - const currentData = await window.api.memory.loadData() + // 直接使用 window.api.memory.saveData 方法保存过滤后的列表 + try { + // 加载当前文件数据 + const currentData = await window.api.memory.loadData() - // 替换 shortMemories 数组 - const newData = { - ...currentData, - shortMemories: filteredShortMemories + // 替换 shortMemories 数组 + const newData = { + ...currentData, + shortMemories: filteredShortMemories + } + + // 使用 true 参数强制覆盖文件 + const result = await window.api.memory.saveData(newData, true) + + if (result) { + console.log(`[ShortMemoryManager] Successfully deleted short memory with ID ${id}`) + // 移除消息提示,避免触发界面重新渲染 + } else { + console.error(`[ShortMemoryManager] Failed to delete short memory with ID ${id}`) + } + } catch (error) { + console.error('[ShortMemoryManager] Failed to delete short memory:', error) } - - // 使用 true 参数强制覆盖文件 - const result = await window.api.memory.saveData(newData, true) - - if (result) { - console.log(`[ShortMemoryManager] Successfully deleted short memory with ID ${id}`) - // 移除消息提示,避免触发界面重新渲染 - } else { - console.error(`[ShortMemoryManager] Failed to delete short memory with ID ${id}`) - } - } catch (error) { - console.error('[ShortMemoryManager] Failed to delete short memory:', error) - } - }, 500), [dispatch]) + }, 500), + [dispatch] + ) return (
diff --git a/src/renderer/src/pages/settings/MemorySettings/index.tsx b/src/renderer/src/pages/settings/MemorySettings/index.tsx index 00f52e4a51..487126c898 100644 --- a/src/renderer/src/pages/settings/MemorySettings/index.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/index.tsx @@ -2,6 +2,7 @@ import { AppstoreOutlined, DeleteOutlined, EditOutlined, + InfoCircleOutlined, PlusOutlined, SearchOutlined, UnorderedListOutlined @@ -9,7 +10,11 @@ import { import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' import { useTheme } from '@renderer/context/ThemeProvider' import { TopicManager } from '@renderer/hooks/useTopic' -import { analyzeAndAddShortMemories, resetLongTermMemoryAnalyzedMessageIds, useMemoryService } from '@renderer/services/MemoryService' +import { + analyzeAndAddShortMemories, + resetLongTermMemoryAnalyzedMessageIds, + useMemoryService +} from '@renderer/services/MemoryService' import { useAppDispatch, useAppSelector } from '@renderer/store' import store from '@renderer/store' // Import store for direct access import { @@ -17,14 +22,15 @@ import { clearMemories, deleteMemory, editMemory, + saveAllMemorySettings, + saveLongTermMemoryData, + saveMemoryData, setAnalyzeModel, setAnalyzing, setAutoAnalyze, + setFilterSensitiveInfo, setMemoryActive, - setShortMemoryAnalyzeModel, - saveMemoryData, - saveLongTermMemoryData, - saveAllMemorySettings + setShortMemoryAnalyzeModel } from '@renderer/store/memory' import { Topic } from '@renderer/types' import { Button, Empty, Input, List, message, Modal, Pagination, Radio, Select, Switch, Tabs, Tag, Tooltip } from 'antd' @@ -42,12 +48,12 @@ import { SettingTitle } from '..' import CollapsibleShortMemoryManager from './CollapsibleShortMemoryManager' +import ContextualRecommendationSettings from './ContextualRecommendationSettings' +import HistoricalContextSettings from './HistoricalContextSettings' import MemoryDeduplicationPanel from './MemoryDeduplicationPanel' import MemoryListManager from './MemoryListManager' import MemoryMindMap from './MemoryMindMap' import PriorityManagementSettings from './PriorityManagementSettings' -import ContextualRecommendationSettings from './ContextualRecommendationSettings' -import HistoricalContextSettings from './HistoricalContextSettings' const MemorySettings: FC = () => { const { t } = useTranslation() @@ -61,6 +67,7 @@ const MemorySettings: FC = () => { const currentListId = useAppSelector((state) => state.memory?.currentListId || null) const isActive = useAppSelector((state) => state.memory?.isActive || false) const autoAnalyze = useAppSelector((state) => state.memory?.autoAnalyze || false) + const filterSensitiveInfo = useAppSelector((state) => state.memory?.filterSensitiveInfo ?? true) // 默认启用敏感信息过滤 const analyzeModel = useAppSelector((state) => state.memory?.analyzeModel || null) const shortMemoryAnalyzeModel = useAppSelector((state) => state.memory?.shortMemoryAnalyzeModel || null) const isAnalyzing = useAppSelector((state) => state.memory?.isAnalyzing || false) @@ -72,7 +79,7 @@ const MemorySettings: FC = () => { const models = useMemo(() => { // 只获取已启用的提供商的模型 return providers - .filter(provider => provider.enabled) // 只保留已启用的提供商 + .filter((provider) => provider.enabled) // 只保留已启用的提供商 .flatMap((provider) => provider.models || []) }, [providers]) @@ -177,20 +184,22 @@ const MemorySettings: FC = () => { const handleDeleteMemory = async (id: string) => { // 先从当前状态中获取要删除的记忆之外的所有记忆 const state = store.getState().memory - const filteredMemories = state.memories.filter(memory => memory.id !== id) + const filteredMemories = state.memories.filter((memory) => memory.id !== id) // 执行删除操作 dispatch(deleteMemory(id)) // 保存到长期记忆文件,并强制覆盖 try { - await dispatch(saveLongTermMemoryData({ - memories: filteredMemories, // 直接使用过滤后的数组,而不是使用当前状态 - memoryLists: state.memoryLists, - currentListId: state.currentListId, - analyzeModel: state.analyzeModel, - forceOverwrite: true // 强制覆盖文件,而不是尝试合并 - })).unwrap() + await dispatch( + saveLongTermMemoryData({ + memories: filteredMemories, // 直接使用过滤后的数组,而不是使用当前状态 + memoryLists: state.memoryLists, + currentListId: state.currentListId, + analyzeModel: state.analyzeModel, + forceOverwrite: true // 强制覆盖文件,而不是尝试合并 + }) + ).unwrap() console.log('[Memory Settings] Long-term memories saved to file after deletion (force overwrite)') message.success(t('settings.memory.deleteSuccess')) @@ -225,13 +234,15 @@ const MemorySettings: FC = () => { try { // 直接传递空数组作为 memories,确保完全清空 const state = store.getState().memory - await dispatch(saveLongTermMemoryData({ - memories: [], // 直接使用空数组,而不是使用当前状态 - memoryLists: state.memoryLists, - currentListId: state.currentListId, - analyzeModel: state.analyzeModel, - forceOverwrite: true // 强制覆盖文件,而不是合并 - })).unwrap() + await dispatch( + saveLongTermMemoryData({ + memories: [], // 直接使用空数组,而不是使用当前状态 + memoryLists: state.memoryLists, + currentListId: state.currentListId, + analyzeModel: state.analyzeModel, + forceOverwrite: true // 强制覆盖文件,而不是合并 + }) + ).unwrap() console.log('[Memory Settings] Long-term memories saved to file after clearing (force overwrite)') } catch (error) { console.error('[Memory Settings] Failed to save long-term memory data after clearing:', error) @@ -250,6 +261,20 @@ const MemorySettings: FC = () => { dispatch(setAutoAnalyze(checked)) } + // 处理切换敏感信息过滤 + const handleToggleFilterSensitiveInfo = async (checked: boolean) => { + dispatch(setFilterSensitiveInfo(checked)) + console.log('[Memory Settings] Filter sensitive info set:', checked) + + // 使用Redux Thunk保存到JSON文件 + try { + await dispatch(saveMemoryData({ filterSensitiveInfo: checked })).unwrap() + console.log('[Memory Settings] Filter sensitive info saved to file successfully:', checked) + } catch (error) { + console.error('[Memory Settings] Failed to save filter sensitive info to file:', error) + } + } + // 处理选择长期记忆分析模型 const handleSelectModel = async (modelId: string) => { dispatch(setAnalyzeModel(modelId)) @@ -337,7 +362,7 @@ const MemorySettings: FC = () => { // 遍历所有服务商的模型找到匹配的模型 for (const provider of Object.values(providers)) { - const model = provider.models.find(m => m.id === analyzeModel) + const model = provider.models.find((m) => m.id === analyzeModel) if (model) { return `${model.name} | ${provider.name}` } @@ -352,7 +377,7 @@ const MemorySettings: FC = () => { // 遍历所有服务商的模型找到匹配的模型 for (const provider of Object.values(providers)) { - const model = provider.models.find(m => m.id === shortMemoryAnalyzeModel) + const model = provider.models.find((m) => m.id === shortMemoryAnalyzeModel) if (model) { return `${model.name} | ${provider.name}` } @@ -547,6 +572,23 @@ const MemorySettings: FC = () => { {t('settings.memory.enableAutoAnalyze')} + + + {t('settings.memory.filterSensitiveInfo') || '过滤敏感信息'} + + + + + + {/* 短期记忆分析模型选择 */} @@ -559,7 +601,7 @@ const MemorySettings: FC = () => { let currentModel: { id: string; provider: string; name: string; group: string } | undefined if (shortMemoryAnalyzeModel) { for (const provider of Object.values(providers)) { - const model = provider.models.find(m => m.id === shortMemoryAnalyzeModel) + const model = provider.models.find((m) => m.id === shortMemoryAnalyzeModel) if (model) { currentModel = model break @@ -573,9 +615,10 @@ const MemorySettings: FC = () => { } }} style={{ width: 300 }} - disabled={!isActive} - > - {shortMemoryAnalyzeModel ? getSelectedShortMemoryModelName() : t('settings.memory.selectModel') || '选择模型'} + disabled={!isActive}> + {shortMemoryAnalyzeModel + ? getSelectedShortMemoryModelName() + : t('settings.memory.selectModel') || '选择模型'} @@ -664,13 +707,11 @@ const MemorySettings: FC = () => { {t('settings.memory.saveAllSettings') || '保存所有设置'} - {t('settings.memory.saveAllSettingsDescription') || '将所有记忆功能的设置保存到文件中,确保应用重启后设置仍然生效。'} + {t('settings.memory.saveAllSettingsDescription') || + '将所有记忆功能的设置保存到文件中,确保应用重启后设置仍然生效。'} - @@ -706,6 +747,23 @@ const MemorySettings: FC = () => { {t('settings.memory.enableAutoAnalyze')} + + + {t('settings.memory.filterSensitiveInfo') || '过滤敏感信息'} + + + + + + {/* 长期记忆分析模型选择 */} @@ -716,7 +774,7 @@ const MemorySettings: FC = () => { let currentModel: { id: string; provider: string; name: string; group: string } | undefined if (analyzeModel) { for (const provider of Object.values(providers)) { - const model = provider.models.find(m => m.id === analyzeModel) + const model = provider.models.find((m) => m.id === analyzeModel) if (model) { currentModel = model break @@ -730,8 +788,7 @@ const MemorySettings: FC = () => { } }} style={{ width: 300 }} - disabled={!isActive} - > + disabled={!isActive}> {analyzeModel ? getSelectedModelName() : t('settings.memory.selectModel') || '选择模型'} @@ -868,71 +925,75 @@ const MemorySettings: FC = () => { {viewMode === 'list' ? ( memories.length > 0 && isActive ? (
- (currentListId ? memory.listId === currentListId : true)) + .filter((memory) => categoryFilter === null || memory.category === categoryFilter) + .slice((currentPage - 1) * pageSize, currentPage * pageSize)} + renderItem={(memory) => ( + +
+ } + description={ + + {new Date(memory.createdAt).toLocaleString()} + {memory.source && {memory.source}} + + } + /> + + )} + /> + {/* 分页组件 */} + {memories .filter((memory) => (currentListId ? memory.listId === currentListId : true)) - .filter((memory) => categoryFilter === null || memory.category === categoryFilter) - .slice((currentPage - 1) * pageSize, currentPage * pageSize)} - renderItem={(memory) => ( - -
- } - description={ - - {new Date(memory.createdAt).toLocaleString()} - {memory.source && {memory.source}} - + .filter((memory) => categoryFilter === null || memory.category === categoryFilter).length > + pageSize && ( + + setCurrentPage(page)} + total={ + memories + .filter((memory) => (currentListId ? memory.listId === currentListId : true)) + .filter((memory) => categoryFilter === null || memory.category === categoryFilter) + .length } + pageSize={pageSize} + size="small" + showSizeChanger={false} /> - + )} - /> - {/* 分页组件 */} - {memories - .filter((memory) => (currentListId ? memory.listId === currentListId : true)) - .filter((memory) => categoryFilter === null || memory.category === categoryFilter).length > pageSize && ( - - setCurrentPage(page)} - total={memories - .filter((memory) => (currentListId ? memory.listId === currentListId : true)) - .filter((memory) => categoryFilter === null || memory.category === categoryFilter).length} - pageSize={pageSize} - size="small" - showSizeChanger={false} - /> - - )}
) : ( diff --git a/src/renderer/src/providers/AiProvider/AnthropicProvider.ts b/src/renderer/src/providers/AiProvider/AnthropicProvider.ts index d1e1d611cb..8f0d325cd1 100644 --- a/src/renderer/src/providers/AiProvider/AnthropicProvider.ts +++ b/src/renderer/src/providers/AiProvider/AnthropicProvider.ts @@ -502,7 +502,7 @@ export default class AnthropicProvider extends BaseProvider { // 应用记忆功能到系统提示词 const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService') - const enhancedPrompt = applyMemoriesToPrompt(prompt) + const enhancedPrompt = await applyMemoriesToPrompt(prompt) console.log( '[AnthropicProvider] Applied memories to prompt, length difference:', enhancedPrompt.length - prompt.length diff --git a/src/renderer/src/providers/AiProvider/BaseProvider.ts b/src/renderer/src/providers/AiProvider/BaseProvider.ts index c61e10e43f..2b21ec460f 100644 --- a/src/renderer/src/providers/AiProvider/BaseProvider.ts +++ b/src/renderer/src/providers/AiProvider/BaseProvider.ts @@ -102,10 +102,22 @@ export default abstract class BaseProvider { } public async getMessageContent(message: Message) { - if (isEmpty(message.content)) { + if (isEmpty(message.content) && !message.referencedMessages?.length) { return message.content } + // 处理引用消息 + if (message.referencedMessages && message.referencedMessages.length > 0) { + const refMsg = message.referencedMessages[0] + const roleText = refMsg.role === 'user' ? '用户' : 'AI' + const referencedContent = `===引用消息开始===\n角色: ${roleText}\n内容: ${refMsg.content}\n===引用消息结束===` + // 如果消息内容为空,则直接返回引用内容 + if (isEmpty(message.content.trim())) { + return referencedContent + } + return `${message.content}\n\n${referencedContent}` + } + const webSearchReferences = await this.getWebSearchReferences(message) if (!isEmpty(webSearchReferences)) { diff --git a/src/renderer/src/providers/AiProvider/GeminiProvider.ts b/src/renderer/src/providers/AiProvider/GeminiProvider.ts index 7897ecee96..f731d4575a 100644 --- a/src/renderer/src/providers/AiProvider/GeminiProvider.ts +++ b/src/renderer/src/providers/AiProvider/GeminiProvider.ts @@ -497,7 +497,7 @@ export default class GeminiProvider extends BaseProvider { // 应用记忆功能到系统提示词 const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService') - const enhancedPrompt = applyMemoriesToPrompt(prompt) + const enhancedPrompt = await applyMemoriesToPrompt(prompt) console.log( '[GeminiProvider] Applied memories to prompt, length difference:', enhancedPrompt.length - prompt.length diff --git a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts index 0905dbb494..f9205dd295 100644 --- a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts +++ b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts @@ -659,7 +659,7 @@ export default class OpenAIProvider extends BaseProvider { // 获取当前话题ID const currentTopicId = messages.length > 0 ? messages[0].topicId : undefined // 使用双重类型断言强制转换类型 - const enhancedPrompt = await applyMemoriesToPrompt(originalPrompt as string, currentTopicId) as unknown as string + const enhancedPrompt = (await applyMemoriesToPrompt(originalPrompt as string, currentTopicId)) as unknown as string // 存储原始提示词长度 const originalPromptLength = (originalPrompt as string).length console.log( @@ -768,7 +768,7 @@ export default class OpenAIProvider extends BaseProvider { // 应用记忆功能到系统提示词 const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService') // 使用双重类型断言强制转换类型 - const enhancedPrompt = await applyMemoriesToPrompt(prompt as string) as unknown as string + const enhancedPrompt = (await applyMemoriesToPrompt(prompt as string)) as unknown as string // 存储原始提示词长度 const promptLength = (prompt as string).length console.log('[OpenAIProvider] Applied memories to prompt, length difference:', enhancedPrompt.length - promptLength) diff --git a/src/renderer/src/services/ContextualMemoryService.ts b/src/renderer/src/services/ContextualMemoryService.ts index f3a954d814..18509bdcc4 100644 --- a/src/renderer/src/services/ContextualMemoryService.ts +++ b/src/renderer/src/services/ContextualMemoryService.ts @@ -1,11 +1,12 @@ // src/renderer/src/services/ContextualMemoryService.ts -import store from '@renderer/store' -import { vectorService } from './VectorService' -import { addMemoryRetrievalLatency } from '@renderer/store/memory' -import { fetchGenerate } from '@renderer/services/ApiService' -import { Message } from '@renderer/types' import { TopicManager } from '@renderer/hooks/useTopic' +import { fetchGenerate } from '@renderer/services/ApiService' +import store from '@renderer/store' +import { addMemoryRetrievalLatency } from '@renderer/store/memory' +import { Message } from '@renderer/types' + +import { vectorService } from './VectorService' // 记忆项接口(从store/memory.ts导入) interface Memory { @@ -107,7 +108,7 @@ class ContextualMemoryService { ]) // 合并并排序推荐结果 - let allRecommendations = [...longTermRecommendations, ...shortTermRecommendations] + const allRecommendations = [...longTermRecommendations, ...shortTermRecommendations] // 按相关性分数排序 allRecommendations.sort((a, b) => b.relevanceScore - a.relevanceScore) @@ -120,7 +121,9 @@ class ContextualMemoryService { const latency = endTime - startTime store.dispatch(addMemoryRetrievalLatency(latency)) - console.log(`[ContextualMemory] Found ${limitedRecommendations.length} recommendations in ${latency.toFixed(2)}ms`) + console.log( + `[ContextualMemory] Found ${limitedRecommendations.length} recommendations in ${latency.toFixed(2)}ms` + ) return limitedRecommendations } catch (error) { @@ -180,7 +183,7 @@ class ContextualMemoryService { ]) // 合并并排序推荐结果 - let allRecommendations = [...longTermRecommendations, ...shortTermRecommendations] + const allRecommendations = [...longTermRecommendations, ...shortTermRecommendations] // 按相关性分数排序 allRecommendations.sort((a, b) => b.relevanceScore - a.relevanceScore) @@ -223,7 +226,7 @@ class ContextualMemoryService { ]) // 合并并排序推荐结果 - let allRecommendations = [...longTermRecommendations, ...shortTermRecommendations] + const allRecommendations = [...longTermRecommendations, ...shortTermRecommendations] // 按相关性分数排序 allRecommendations.sort((a, b) => b.relevanceScore - a.relevanceScore) @@ -274,7 +277,7 @@ class ContextualMemoryService { } // 构建对话内容 - const conversation = recentMessages.map(msg => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n') + const conversation = recentMessages.map((msg) => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n') // 构建提示词 const prompt = ` @@ -307,8 +310,8 @@ ${conversation} // 提取关键信息 const keyPoints = result .split('\n') - .map(line => line.trim()) - .filter(line => line && !line.startsWith('#') && !line.startsWith('-')) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#') && !line.startsWith('-')) console.log('[ContextualMemory] Extracted key points:', keyPoints) @@ -331,7 +334,7 @@ ${conversation} */ private _buildContextQuery(messages: Message[]): string { // 提取最近消息的内容 - const messageContents = messages.map(msg => msg.content || '').filter(content => content.trim() !== '') + const messageContents = messages.map((msg) => msg.content || '').filter((content) => content.trim() !== '') // 如果没有有效内容,返回空字符串 if (messageContents.length === 0) { @@ -349,10 +352,7 @@ ${conversation} * @returns 长期记忆推荐列表 * @private */ - private async _getLongTermMemoryRecommendations( - query: string, - topicId?: string - ): Promise { + private async _getLongTermMemoryRecommendations(query: string, topicId?: string): Promise { // 获取当前状态 const state = store.getState() const memoryState = state.memory @@ -363,16 +363,14 @@ ${conversation} } // 获取所有激活的记忆列表 - const activeListIds = memoryState.memoryLists - .filter(list => list.isActive) - .map(list => list.id) + const activeListIds = memoryState.memoryLists.filter((list) => list.isActive).map((list) => list.id) if (activeListIds.length === 0) { return [] } // 获取激活列表中的记忆 - const memories = memoryState.memories.filter(memory => activeListIds.includes(memory.listId)) + const memories = memoryState.memories.filter((memory) => activeListIds.includes(memory.listId)) if (memories.length === 0) { return [] @@ -387,7 +385,7 @@ ${conversation} ) // 转换为推荐格式 - const recommendations: MemoryRecommendation[] = results.map(result => ({ + const recommendations: MemoryRecommendation[] = results.map((result) => ({ memory: result.memory as Memory, relevanceScore: result.similarity, source: 'long-term', @@ -405,10 +403,7 @@ ${conversation} * @returns 短期记忆推荐列表 * @private */ - private async _getShortTermMemoryRecommendations( - query: string, - topicId?: string - ): Promise { + private async _getShortTermMemoryRecommendations(query: string, topicId?: string): Promise { // 获取当前状态 const state = store.getState() const memoryState = state.memory @@ -423,7 +418,7 @@ ${conversation} // 如果指定了话题ID,只获取该话题的短期记忆 if (topicId) { - shortMemories = shortMemories.filter(memory => memory.topicId === topicId) + shortMemories = shortMemories.filter((memory) => memory.topicId === topicId) } if (shortMemories.length === 0) { @@ -439,7 +434,7 @@ ${conversation} ) // 转换为推荐格式 - const recommendations: MemoryRecommendation[] = results.map(result => ({ + const recommendations: MemoryRecommendation[] = results.map((result) => ({ memory: result.memory as ShortMemory, relevanceScore: result.similarity, source: 'short-term', @@ -472,57 +467,57 @@ ${conversation} const memoryState = state.memory // 应用多因素排序优化 - return recommendations.map(rec => { - const memory = rec.memory - let adjustedScore = rec.relevanceScore + return recommendations + .map((rec) => { + const memory = rec.memory + let adjustedScore = rec.relevanceScore - // 1. 考虑记忆的重要性 - if (memory.importance && memoryState.priorityManagementEnabled) { - adjustedScore *= (1 + memory.importance * 0.5) // 重要性最多提升50%的分数 - } - - // 2. 考虑记忆的鲜度 - if (memory.freshness && memoryState.freshnessEnabled) { - adjustedScore *= (1 + memory.freshness * 0.3) // 鲜度最多提升30%的分数 - } - - // 3. 考虑记忆的衰减因子 - if (memory.decayFactor && memoryState.decayEnabled) { - adjustedScore *= memory.decayFactor // 直接应用衰减因子 - } - - // 4. 如果记忆与当前话题相关,提高分数 - if (topicId && memory.topicId === topicId) { - adjustedScore *= 1.2 // 提高20%的分数 - } - - // 5. 考虑访问频率,常用的记忆可能更相关 - if (memory.accessCount && memory.accessCount > 0) { - // 访问次数越多,提升越大,但有上限 - const accessBoost = Math.min(memory.accessCount / 10, 0.2) // 最多提升20% - adjustedScore *= (1 + accessBoost) - } - - // 6. 考虑关键词匹配 - if (memory.keywords && memory.keywords.length > 0) { - const queryLower = query.toLowerCase() - const keywordMatches = memory.keywords.filter(keyword => - queryLower.includes(keyword.toLowerCase()) - ).length - - if (keywordMatches > 0) { - // 关键词匹配越多,提升越大 - const keywordBoost = Math.min(keywordMatches * 0.1, 0.3) // 最多提升30% - adjustedScore *= (1 + keywordBoost) + // 1. 考虑记忆的重要性 + if (memory.importance && memoryState.priorityManagementEnabled) { + adjustedScore *= 1 + memory.importance * 0.5 // 重要性最多提升50%的分数 } - } - // 返回调整后的推荐 - return { - ...rec, - relevanceScore: adjustedScore - } - }).sort((a, b) => b.relevanceScore - a.relevanceScore) // 按调整后的分数重新排序 + // 2. 考虑记忆的鲜度 + if (memory.freshness && memoryState.freshnessEnabled) { + adjustedScore *= 1 + memory.freshness * 0.3 // 鲜度最多提升30%的分数 + } + + // 3. 考虑记忆的衰减因子 + if (memory.decayFactor && memoryState.decayEnabled) { + adjustedScore *= memory.decayFactor // 直接应用衰减因子 + } + + // 4. 如果记忆与当前话题相关,提高分数 + if (topicId && memory.topicId === topicId) { + adjustedScore *= 1.2 // 提高20%的分数 + } + + // 5. 考虑访问频率,常用的记忆可能更相关 + if (memory.accessCount && memory.accessCount > 0) { + // 访问次数越多,提升越大,但有上限 + const accessBoost = Math.min(memory.accessCount / 10, 0.2) // 最多提升20% + adjustedScore *= 1 + accessBoost + } + + // 6. 考虑关键词匹配 + if (memory.keywords && memory.keywords.length > 0) { + const queryLower = query.toLowerCase() + const keywordMatches = memory.keywords.filter((keyword) => queryLower.includes(keyword.toLowerCase())).length + + if (keywordMatches > 0) { + // 关键词匹配越多,提升越大 + const keywordBoost = Math.min(keywordMatches * 0.1, 0.3) // 最多提升30% + adjustedScore *= 1 + keywordBoost + } + } + + // 返回调整后的推荐 + return { + ...rec, + relevanceScore: adjustedScore + } + }) + .sort((a, b) => b.relevanceScore - a.relevanceScore) // 按调整后的分数重新排序 } } diff --git a/src/renderer/src/services/HistoricalContextService.ts b/src/renderer/src/services/HistoricalContextService.ts index 30fd9d9bd5..3df3817e1b 100644 --- a/src/renderer/src/services/HistoricalContextService.ts +++ b/src/renderer/src/services/HistoricalContextService.ts @@ -1,9 +1,9 @@ // src/renderer/src/services/HistoricalContextService.ts -import store from '@renderer/store' import { TopicManager } from '@renderer/hooks/useTopic' import { fetchGenerate } from '@renderer/services/ApiService' -import { Message, Topic } from '@renderer/types' +import store from '@renderer/store' import { ShortMemory } from '@renderer/store/memory' +import { Message } from '@renderer/types' /** * 分析当前对话并决定是否需要调用历史对话 @@ -112,11 +112,11 @@ const analyzeNeedForHistoricalContext = async ( try { // 准备分析提示词 const messagesContent = recentMessages - .map(msg => `${msg.role === 'user' ? '用户' : 'AI'}: ${msg.content}`) + .map((msg) => `${msg.role === 'user' ? '用户' : 'AI'}: ${msg.content}`) .join('\n') const memoriesContent = shortMemories - .map(memory => `话题ID: ${memory.topicId}\n内容: ${memory.content}`) + .map((memory) => `话题ID: ${memory.topicId}\n内容: ${memory.content}`) .join('\n\n') const prompt = ` @@ -153,7 +153,8 @@ ${memoriesContent} // 获取分析模型 const state = store.getState() // 优先使用历史对话上下文分析模型,如果没有设置,则使用短期记忆分析模型或长期记忆分析模型 - const analyzeModel = state.memory?.historicalContextAnalyzeModel || state.memory?.shortMemoryAnalyzeModel || state.memory?.analyzeModel + const analyzeModel = + state.memory?.historicalContextAnalyzeModel || state.memory?.shortMemoryAnalyzeModel || state.memory?.analyzeModel if (!analyzeModel) { console.log('[HistoricalContext] No analyze model set') @@ -199,8 +200,7 @@ ${memoriesContent} } // 如果都失败了,尝试简单的文本分析 - const needsContext = result.toLowerCase().includes('true') && - !result.toLowerCase().includes('false') + const needsContext = result.toLowerCase().includes('true') && !result.toLowerCase().includes('false') const topicIdMatch = result.match(/selectedTopicId["\s:]+([^"\s,}]+)/) const reasonMatch = result.match(/reason["\s:]+"([^"]+)"/) || result.match(/reason["\s:]+([^,}\s]+)/) @@ -229,9 +229,7 @@ const getOriginalDialogContent = async (topicId: string): Promise } // 格式化对话内容 - const dialogContent = messages - .map(msg => `${msg.role === 'user' ? '用户' : 'AI'}: ${msg.content}`) - .join('\n\n') + const dialogContent = messages.map((msg) => `${msg.role === 'user' ? '用户' : 'AI'}: ${msg.content}`).join('\n\n') return dialogContent } catch (error) { diff --git a/src/renderer/src/services/MemoryDeduplicationService.ts b/src/renderer/src/services/MemoryDeduplicationService.ts index b94640ff6b..f72bd61359 100644 --- a/src/renderer/src/services/MemoryDeduplicationService.ts +++ b/src/renderer/src/services/MemoryDeduplicationService.ts @@ -1,7 +1,14 @@ // 记忆去重与合并服务 import { fetchGenerate } from '@renderer/services/ApiService' import store from '@renderer/store' -import { addMemory, addShortMemory, deleteMemory, deleteShortMemory, saveMemoryData, saveLongTermMemoryData } from '@renderer/store/memory' +import { + addMemory, + addShortMemory, + deleteMemory, + deleteShortMemory, + saveLongTermMemoryData, + saveMemoryData +} from '@renderer/store/memory' // 记忆去重与合并的结果接口 export interface DeduplicationResult { @@ -137,7 +144,8 @@ ${memoriesToCheck} if (similarGroupsMatch && similarGroupsMatch[1]) { const groupsText = similarGroupsMatch[1].trim() // 更新正则表达式以匹配新的格式,包括重要性和关键词 - const groupRegex = /-\s*组(\d+)?:\s*\[([\d,\s]+)\]\s*-\s*合并建议:\s*"([^"]+)"\s*-\s*分类:\s*"([^"]+)"\s*(?:-\s*重要性:\s*"([^"]+)")?\s*(?:-\s*关键词:\s*"([^"]+)")?/g + const groupRegex = + /-\s*组(\d+)?:\s*\[([\d,\s]+)\]\s*-\s*合并建议:\s*"([^"]+)"\s*-\s*分类:\s*"([^"]+)"\s*(?:-\s*重要性:\s*"([^"]+)")?\s*(?:-\s*关键词:\s*"([^"]+)")?/g let match: RegExpExecArray | null while ((match = groupRegex.exec(groupsText)) !== null) { @@ -146,7 +154,12 @@ ${memoriesToCheck} const mergedContent = match[3].trim() const category = match[4]?.trim() const importance = match[5] ? parseFloat(match[5].trim()) : undefined - const keywords = match[6] ? match[6].trim().split(',').map((k: string) => k.trim()) : undefined + const keywords = match[6] + ? match[6] + .trim() + .split(',') + .map((k: string) => k.trim()) + : undefined similarGroups.push({ groupId, @@ -299,18 +312,26 @@ export const applyDeduplicationResult = async ( // 保存到文件 if (isShortMemory) { // 短期记忆使用saveMemoryData - await store.dispatch(saveMemoryData({ - shortMemories: currentState.shortMemories - })).unwrap() + await store + .dispatch( + saveMemoryData({ + shortMemories: currentState.shortMemories + }) + ) + .unwrap() console.log('[Memory Deduplication] Short memories saved to file after merging') } else { // 长期记忆使用saveLongTermMemoryData - await store.dispatch(saveLongTermMemoryData({ - memories: currentState.memories, - memoryLists: currentState.memoryLists, - currentListId: currentState.currentListId, - analyzeModel: currentState.analyzeModel - })).unwrap() + await store + .dispatch( + saveLongTermMemoryData({ + memories: currentState.memories, + memoryLists: currentState.memoryLists, + currentListId: currentState.currentListId, + analyzeModel: currentState.analyzeModel + }) + ) + .unwrap() console.log('[Memory Deduplication] Long-term memories saved to file after merging') } } catch (error) { diff --git a/src/renderer/src/services/MemoryService.ts b/src/renderer/src/services/MemoryService.ts index 97962f52b3..4821931c2f 100644 --- a/src/renderer/src/services/MemoryService.ts +++ b/src/renderer/src/services/MemoryService.ts @@ -7,26 +7,27 @@ import { useAppDispatch, useAppSelector } from '@renderer/store' import store from '@renderer/store' // Import store // AiProvider no longer needed as we're using fetchGenerate import { + accessMemory, addAnalysisLatency, addMemory, addShortMemory, - setAnalyzing, - updateAnalysisStats, - updatePerformanceMetrics, - updateUserInterest, - updateMemoryPriorities, - accessMemory, - Memory, - saveMemoryData, - saveLongTermMemoryData, - updateCurrentRecommendations, - setRecommending, clearCurrentRecommendations, - MemoryRecommendation + Memory, + MemoryRecommendation, + saveLongTermMemoryData, + saveMemoryData, + setAnalyzing, + setRecommending, + updateAnalysisStats, + updateCurrentRecommendations, + updateMemoryPriorities, + updatePerformanceMetrics, + updateUserInterest } from '@renderer/store/memory' -import { useCallback, useEffect, useRef } from 'react' // Add useRef back -import { contextualMemoryService } from './ContextualMemoryService' // Import contextual memory service import { Message } from '@renderer/types' // Import Message type +import { useCallback, useEffect, useRef } from 'react' // Add useRef back + +import { contextualMemoryService } from './ContextualMemoryService' // Import contextual memory service // 计算对话复杂度,用于调整分析深度 const calculateConversationComplexity = (conversation: string): 'low' | 'medium' | 'high' => { @@ -45,6 +46,8 @@ const calculateConversationComplexity = (conversation: string): 'low' | 'medium' } // 根据分析深度调整提示词 +// 注意:该函数当前未使用,保留供将来可能的功能扩展 +/* const adjustPromptForDepth = (basePrompt: string, depth: 'low' | 'medium' | 'high'): string => { switch (depth) { case 'low': @@ -73,6 +76,7 @@ const adjustPromptForDepth = (basePrompt: string, depth: 'low' | 'medium' | 'hig return basePrompt } } +*/ // 提取用户关注点 const extractUserInterests = (conversation: string): string[] => { @@ -99,8 +103,11 @@ const analyzeConversation = async ( customPrompt?: string ): Promise> => { try { + // 获取当前的过滤敏感信息设置 + const filterSensitiveInfo = store.getState().memory?.filterSensitiveInfo ?? true + // 使用自定义提示词或默认提示词 - const basePrompt = + let basePrompt = customPrompt || ` 请分析对话内容,提取出重要的用户偏好、习惯、需求和背景信息,这些信息在未来的对话中可能有用。 @@ -117,6 +124,23 @@ const analyzeConversation = async ( 请确保每条信息都是简洁、准确的。如果没有找到重要信息,请返回空字符串。 ` + + // 如果启用了敏感信息过滤,添加相关指令 + if (filterSensitiveInfo) { + basePrompt += ` +## 安全提示: +请注意不要提取任何敏感信息,包括但不限于: +- API密钥、访问令牌或其他凭证 +- 密码或密码提示 +- 私人联系方式(如电话号码、邮箱地址) +- 个人身份信息(如身份证号、社保号) +- 银行账户或支付信息 +- 私密的个人或商业信息 + +如果发现此类信息,请完全忽略,不要以任何形式记录或提取。 +` + } + console.log(`[Memory Analysis] Analyzing conversation using model: ${modelId}`) // 将提示词和对话内容合并到一个系统提示词中 @@ -134,7 +158,7 @@ ${conversation} console.log('[Memory Analysis] Calling fetchGenerate with combined prompt...') const result = await fetchGenerate({ prompt: combinedPrompt, - content: "", // 内容字段留空 + content: '', // 内容字段留空 modelId: modelId }) @@ -169,7 +193,7 @@ ${conversation} } catch (error) { console.error('Failed to analyze conversation with real AI:', error) // Consider logging the specific error details if possible - console.error('Error details:', JSON.stringify(error, null, 2)); + console.error('Error details:', JSON.stringify(error, null, 2)) return [] as Array<{ content: string; category: string }> // Return empty array on error } } @@ -205,14 +229,10 @@ export const getContextualMemoryRecommendations = async ( store.dispatch(setRecommending(true)) // 调用上下文感知记忆服务获取推荐 - const recommendations = await contextualMemoryService.getContextualMemoryRecommendations( - messages, - topicId, - limit - ) + const recommendations = await contextualMemoryService.getContextualMemoryRecommendations(messages, topicId, limit) // 转换为Redux状态中的推荐格式 - const memoryRecommendations: MemoryRecommendation[] = recommendations.map(rec => ({ + const memoryRecommendations: MemoryRecommendation[] = recommendations.map((rec) => ({ memoryId: rec.memory.id, relevanceScore: rec.relevanceScore, source: rec.source, @@ -239,10 +259,7 @@ export const getContextualMemoryRecommendations = async ( * @param limit - 返回的最大记忆数量 * @returns 与当前主题相关的记忆列表 */ -export const getTopicRelatedMemories = async ( - topicId: string, - limit: number = 10 -): Promise => { +export const getTopicRelatedMemories = async (topicId: string, limit: number = 10): Promise => { try { // 获取当前状态 const state = store.getState().memory @@ -257,13 +274,10 @@ export const getTopicRelatedMemories = async ( store.dispatch(setRecommending(true)) // 调用上下文感知记忆服务获取推荐 - const recommendations = await contextualMemoryService.getTopicRelatedMemories( - topicId, - limit - ) + const recommendations = await contextualMemoryService.getTopicRelatedMemories(topicId, limit) // 转换为Redux状态中的推荐格式 - const memoryRecommendations: MemoryRecommendation[] = recommendations.map(rec => ({ + const memoryRecommendations: MemoryRecommendation[] = recommendations.map((rec) => ({ memoryId: rec.memory.id, relevanceScore: rec.relevanceScore, source: rec.source, @@ -308,13 +322,10 @@ export const getAIEnhancedMemoryRecommendations = async ( store.dispatch(setRecommending(true)) // 调用上下文感知记忆服务获取推荐 - const recommendations = await contextualMemoryService.getAIEnhancedMemoryRecommendations( - messages, - limit - ) + const recommendations = await contextualMemoryService.getAIEnhancedMemoryRecommendations(messages, limit) // 转换为Redux状态中的推荐格式 - const memoryRecommendations: MemoryRecommendation[] = recommendations.map(rec => ({ + const memoryRecommendations: MemoryRecommendation[] = recommendations.map((rec) => ({ memoryId: rec.memory.id, relevanceScore: rec.relevanceScore, source: rec.source, @@ -342,7 +353,9 @@ export const useMemoryService = () => { const isActive = useAppSelector((state) => state.memory?.isActive || false) const autoAnalyze = useAppSelector((state) => state.memory?.autoAnalyze || false) const analyzeModel = useAppSelector((state) => state.memory?.analyzeModel || null) - const contextualRecommendationEnabled = useAppSelector((state) => state.memory?.contextualRecommendationEnabled || false) + const contextualRecommendationEnabled = useAppSelector( + (state) => state.memory?.contextualRecommendationEnabled || false + ) const autoRecommendMemories = useAppSelector((state) => state.memory?.autoRecommendMemories || false) // 使用 useCallback 定义分析函数,但减少依赖项 @@ -511,7 +524,7 @@ ${existingMemoriesContent} // 调用分析函数,传递自定义提示词和对话内容 // 将对话内容直接放在提示词中,不再单独传递 - const memories = await analyzeConversation("", memoryState.analyzeModel!, basePrompt) + const memories = await analyzeConversation('', memoryState.analyzeModel!, basePrompt) // 用户关注点学习 if (memoryState.interestTrackingEnabled) { @@ -623,12 +636,16 @@ ${existingMemoriesContent} if (addedNewMemories) { try { const state = store.getState().memory - await store.dispatch(saveLongTermMemoryData({ - memories: state.memories, - memoryLists: state.memoryLists, - currentListId: state.currentListId, - analyzeModel: state.analyzeModel - })).unwrap() + await store + .dispatch( + saveLongTermMemoryData({ + memories: state.memories, + memoryLists: state.memoryLists, + currentListId: state.currentListId, + analyzeModel: state.analyzeModel + }) + ) + .unwrap() console.log('[Memory Analysis] Long-term memories saved to file after analysis') } catch (error) { console.error('[Memory Analysis] Failed to save long-term memory data after analysis:', error) @@ -712,8 +729,6 @@ ${existingMemoriesContent} analyzeAndAddMemoriesRef.current = analyzeAndAddMemories }, [analyzeAndAddMemories]) - - // 记录记忆访问 const recordMemoryAccess = useCallback((memoryId: string, isShortMemory: boolean = false) => { store.dispatch(accessMemory({ id: memoryId, isShortMemory })) @@ -722,20 +737,23 @@ ${existingMemoriesContent} // Effect 来设置/清除定时器,只依赖于启动条件 useEffect(() => { // 定期更新记忆优先级 - const priorityUpdateInterval = setInterval(() => { - const memoryState = store.getState().memory - if (!memoryState?.priorityManagementEnabled) return + const priorityUpdateInterval = setInterval( + () => { + const memoryState = store.getState().memory + if (!memoryState?.priorityManagementEnabled) return - // 检查上次更新时间,避免频繁更新 - const now = Date.now() - const lastUpdate = memoryState.lastPriorityUpdate || 0 - const updateInterval = 30 * 60 * 1000 // 30分钟更新一次 + // 检查上次更新时间,避免频繁更新 + const now = Date.now() + const lastUpdate = memoryState.lastPriorityUpdate || 0 + const updateInterval = 30 * 60 * 1000 // 30分钟更新一次 - if (now - lastUpdate < updateInterval) return + if (now - lastUpdate < updateInterval) return - console.log('[Memory Priority] Updating memory priorities and freshness...') - store.dispatch(updateMemoryPriorities()) - }, 10 * 60 * 1000) // 每10分钟检查一次 + console.log('[Memory Priority] Updating memory priorities and freshness...') + store.dispatch(updateMemoryPriorities()) + }, + 10 * 60 * 1000 + ) // 每10分钟检查一次 return () => { clearInterval(priorityUpdateInterval) @@ -825,16 +843,19 @@ ${existingMemoriesContent} console.log('[ContextualMemory] Setting up auto recommendation timer...') // 每5分钟自动推荐一次记忆 - const intervalId = setInterval(() => { - const state = store.getState() - const currentTopicId = state.messages.currentTopic?.id - const messages = currentTopicId ? state.messages.messagesByTopic?.[currentTopicId] || [] : [] + const intervalId = setInterval( + () => { + const state = store.getState() + const currentTopicId = state.messages.currentTopic?.id + const messages = currentTopicId ? state.messages.messagesByTopic?.[currentTopicId] || [] : [] - if (currentTopicId && messages.length > 0) { - console.log('[ContextualMemory] Auto recommendation triggered') - getContextualRecommendations(messages, currentTopicId) - } - }, 5 * 60 * 1000) // 5分钟 + if (currentTopicId && messages.length > 0) { + console.log('[ContextualMemory] Auto recommendation triggered') + getContextualRecommendations(messages, currentTopicId) + } + }, + 5 * 60 * 1000 + ) // 5分钟 return () => { console.log('[ContextualMemory] Clearing auto recommendation timer') @@ -873,10 +894,14 @@ export const addShortMemoryItem = async ( // 保存到文件,并强制覆盖 try { const state = store.getState().memory - await store.dispatch(saveMemoryData({ - shortMemories: state.shortMemories, - forceOverwrite: true // 强制覆盖文件,确保数据正确保存 - })).unwrap() + await store + .dispatch( + saveMemoryData({ + shortMemories: state.shortMemories, + forceOverwrite: true // 强制覆盖文件,确保数据正确保存 + }) + ) + .unwrap() console.log('[Memory] Short memory saved to file after manual addition (force overwrite)') } catch (error) { console.error('[Memory] Failed to save short memory data after manual addition:', error) @@ -905,12 +930,16 @@ export const addMemoryItem = async ( // 保存到长期记忆文件 try { const state = store.getState().memory - await store.dispatch(saveLongTermMemoryData({ - memories: state.memories, - memoryLists: state.memoryLists, - currentListId: state.currentListId, - analyzeModel: state.analyzeModel - })).unwrap() + await store + .dispatch( + saveLongTermMemoryData({ + memories: state.memories, + memoryLists: state.memoryLists, + currentListId: state.currentListId, + analyzeModel: state.analyzeModel + }) + ) + .unwrap() console.log('[Memory] Long-term memory saved to file after manual addition') } catch (error) { console.error('[Memory] Failed to save long-term memory data after manual addition:', error) @@ -934,7 +963,7 @@ export const resetLongTermMemoryAnalyzedMessageIds = async (topicId: string): Pr // 找到指定话题的所有长期记忆 const memories = state.memories || [] - const topicMemories = memories.filter(memory => memory.topicId === topicId) + const topicMemories = memories.filter((memory) => memory.topicId === topicId) if (topicMemories.length === 0) { console.log(`[Memory Reset] No long-term memories found for topic ${topicId}`) @@ -947,7 +976,7 @@ export const resetLongTermMemoryAnalyzedMessageIds = async (topicId: string): Pr let hasChanges = false // 创建更新后的记忆数组 - const updatedMemories = state.memories.map(memory => { + const updatedMemories = state.memories.map((memory) => { // 只更新指定话题的记忆 if (memory.topicId === topicId && memory.analyzedMessageIds && memory.analyzedMessageIds.length > 0) { hasChanges = true @@ -972,9 +1001,13 @@ export const resetLongTermMemoryAnalyzedMessageIds = async (topicId: string): Pr }) // 保存更改到文件 - await store.dispatch(saveMemoryData({ - memories: updatedMemories - })).unwrap() + await store + .dispatch( + saveMemoryData({ + memories: updatedMemories + }) + ) + .unwrap() // 尝试获取话题的消息,以确保分析时能找到消息 try { @@ -1089,8 +1122,11 @@ export const analyzeAndAddShortMemories = async (topicId: string) => { console.log(`[Short Memory Analysis] Analyzing topic: ${topicId}`) console.log('[Short Memory Analysis] New conversation length:', newConversation.length) + // 获取当前的过滤敏感信息设置 + const filterSensitiveInfo = store.getState().memory?.filterSensitiveInfo ?? true + // 构建短期记忆分析提示词,包含已有记忆和新对话 - const prompt = ` + let prompt = ` 请对以下对话内容进行非常详细的分析和总结,提取对当前对话至关重要的上下文信息。请注意,这个分析将用于生成短期记忆,帮助AI理解当前对话的完整上下文。 分析要求: @@ -1101,7 +1137,22 @@ export const analyzeAndAddShortMemories = async (topicId: string) => { 5. 提取对理解当前对话上下文必不可少的信息 6. 记录用户提出的具体问题和关注点 7. 捕捉用户在对话中表达的偏好、困惑和反馈 -8. 记录对话中提到的文件、路径、变量名等具体技术细节 +8. 记录对话中提到的文件、路径、变量名等具体技术细节` + + // 如果启用了敏感信息过滤,添加相关指令 + if (filterSensitiveInfo) { + prompt += ` +9. 请注意不要提取任何敏感信息,包括但不限于: + - API密钥、访问令牌或其他凭证 + - 密码或密码提示 + - 私人联系方式(如电话号码、邮箱地址) + - 个人身份信息(如身份证号、社保号) + - 银行账户或支付信息 + - 私密的个人或商业信息 + 如果发现此类信息,请完全忽略,不要以任何形式记录或提取。` + } + + prompt += ` 与长期记忆不同,短期记忆应该非常详细地关注当前对话的具体细节和上下文。每条短期记忆应该是对对话片段的精准总结,确保不遗漏任何重要信息。 @@ -1173,7 +1224,7 @@ ${newConversation} let extractedLines: string[] = [] // 首先尝试匹配带有数字或短横线的列表项 - const listItemRegex = /(?:^|\n)(?:\d+\.\s*|\-\s*)(.+?)(?=\n\d+\.\s*|\n\-\s*|\n\n|$)/gs + const listItemRegex = /(?:^|\n)(?:\d+\.\s*|-\s*)(.+?)(?=\n\d+\.\s*|\n-\s*|\n\n|$)/gs let match: RegExpExecArray | null while ((match = listItemRegex.exec(result)) !== null) { if (match[1] && match[1].trim()) { @@ -1185,18 +1236,20 @@ ${newConversation} if (extractedLines.length === 0) { extractedLines = result .split('\n') - .map(line => line.trim()) - .filter(line => { + .map((line) => line.trim()) + .filter((line) => { // 过滤掉空行和非内容行(如标题、分隔符等) - return line && - !line.startsWith('#') && - !line.startsWith('---') && - !line.startsWith('===') && - !line.includes('没有找到新的重要信息') && - !line.includes('No new important information') + return ( + line && + !line.startsWith('#') && + !line.startsWith('---') && + !line.startsWith('===') && + !line.includes('没有找到新的重要信息') && + !line.includes('No new important information') + ) }) // 清理行首的数字、点和短横线 - .map(line => line.replace(/^(\d+\.\s*|\-\s*)/, '').trim()) + .map((line) => line.replace(/^(\d+\.\s*|-\s*)/, '').trim()) } console.log('[Short Memory Analysis] Extracted items:', extractedLines) @@ -1211,11 +1264,12 @@ ${newConversation} const newMemories = extractedLines.filter((content: string) => { const normalizedContent = content.toLowerCase() // 检查是否与现有记忆完全匹配或高度相似 - return !existingContents.some(existingContent => - existingContent === normalizedContent || - // 简单的相似度检查 - 如果一个字符串包含另一个的80%以上的内容 - (existingContent.includes(normalizedContent) && normalizedContent.length > existingContent.length * 0.8) || - (normalizedContent.includes(existingContent) && existingContent.length > normalizedContent.length * 0.8) + return !existingContents.some( + (existingContent) => + existingContent === normalizedContent || + // 简单的相似度检查 - 如果一个字符串包含另一个的80%以上的内容 + (existingContent.includes(normalizedContent) && normalizedContent.length > existingContent.length * 0.8) || + (normalizedContent.includes(existingContent) && existingContent.length > normalizedContent.length * 0.8) ) }) @@ -1254,12 +1308,16 @@ ${newConversation} // 显式触发保存操作,确保数据被持久化,并强制覆盖 try { const state = store.getState().memory - await store.dispatch(saveMemoryData({ - memoryLists: state.memoryLists, - memories: state.memories, - shortMemories: state.shortMemories, - forceOverwrite: true // 强制覆盖文件,确保数据正确保存 - })).unwrap() // 使用unwrap()来等待异步操作完成并处理错误 + await store + .dispatch( + saveMemoryData({ + memoryLists: state.memoryLists, + memories: state.memories, + shortMemories: state.shortMemories, + forceOverwrite: true // 强制覆盖文件,确保数据正确保存 + }) + ) + .unwrap() // 使用unwrap()来等待异步操作完成并处理错误 console.log('[Short Memory Analysis] Memory data saved successfully (force overwrite)') } catch (error) { console.error('[Short Memory Analysis] Failed to save memory data:', error) @@ -1324,16 +1382,16 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri // 处理上下文感知记忆推荐 if (contextualRecommendationEnabled && currentRecommendations && currentRecommendations.length > 0) { // 获取推荐记忆的详细信息 - const recommendedMemories: Array<{content: string, source: string, reason: string}> = [] + const recommendedMemories: Array<{ content: string; source: string; reason: string }> = [] // 处理每个推荐记忆 for (const recommendation of currentRecommendations) { // 根据来源查找记忆 let memory: any = null if (recommendation.source === 'long-term') { - memory = memories.find(m => m.id === recommendation.memoryId) + memory = memories.find((m) => m.id === recommendation.memoryId) } else if (recommendation.source === 'short-term') { - memory = shortMemories.find(m => m.id === recommendation.memoryId) + memory = shortMemories.find((m) => m.id === recommendation.memoryId) } if (memory) { @@ -1344,10 +1402,12 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri }) // 记录访问 - store.dispatch(accessMemory({ - id: memory.id, - isShortMemory: recommendation.source === 'short-term' - })) + store.dispatch( + accessMemory({ + id: memory.id, + isShortMemory: recommendation.source === 'short-term' + }) + ) } } @@ -1355,8 +1415,10 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri // 构建推荐记忆提示词 // 按重要性排序 recommendedMemories.sort((a, b) => { - const memoryA = memories.find(m => m.content === a.content) || shortMemories.find(m => m.content === a.content) - const memoryB = memories.find(m => m.content === b.content) || shortMemories.find(m => m.content === b.content) + const memoryA = + memories.find((m) => m.content === a.content) || shortMemories.find((m) => m.content === a.content) + const memoryB = + memories.find((m) => m.content === b.content) || shortMemories.find((m) => m.content === b.content) const importanceA = memoryA?.importance || 0.5 const importanceB = memoryB?.importance || 0.5 return importanceB - importanceA @@ -1383,7 +1445,7 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri // 如果启用了智能优先级管理,根据优先级排序 if (priorityManagementEnabled && topicShortMemories.length > 0) { // 计算每个记忆的综合分数(重要性 * 衰减因子 * 鲜度) - const scoredMemories = topicShortMemories.map(memory => { + const scoredMemories = topicShortMemories.map((memory) => { // 记录访问 store.dispatch(accessMemory({ id: memory.id, isShortMemory: true })) @@ -1399,7 +1461,7 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri scoredMemories.sort((a, b) => b.score - a.score) // 提取排序后的记忆 - topicShortMemories = scoredMemories.map(item => item.memory) + topicShortMemories = scoredMemories.map((item) => item.memory) // 限制数量,避免提示词过长 if (topicShortMemories.length > 10) { @@ -1437,7 +1499,7 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri // 如果启用了智能优先级管理,根据优先级排序 if (priorityManagementEnabled && activeMemories.length > 0) { // 计算每个记忆的综合分数 - const scoredMemories = activeMemories.map(memory => { + const scoredMemories = activeMemories.map((memory) => { // 记录访问 store.dispatch(accessMemory({ id: memory.id })) @@ -1457,9 +1519,9 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri const memoriesByList: Record = {} // 提取排序后的记忆 - const sortedMemories = scoredMemories.map(item => item.memory) + const sortedMemories = scoredMemories.map((item) => item.memory) - sortedMemories.forEach(memory => { + sortedMemories.forEach((memory) => { if (!memoriesByList[memory.listId]) { memoriesByList[memory.listId] = [] } diff --git a/src/renderer/src/services/MessagesService.ts b/src/renderer/src/services/MessagesService.ts index 14c024d997..c2dff59bd3 100644 --- a/src/renderer/src/services/MessagesService.ts +++ b/src/renderer/src/services/MessagesService.ts @@ -1,5 +1,6 @@ import SearchPopup from '@renderer/components/Popups/SearchPopup' import { DEFAULT_CONTEXTCOUNT } from '@renderer/config/constant' +import db from '@renderer/databases' import { getTopicById } from '@renderer/hooks/useTopic' import i18n from '@renderer/i18n' import { fetchMessagesSummary } from '@renderer/services/ApiService' @@ -200,6 +201,40 @@ export function getMessageModelId(message: Message) { return message?.model?.id || message.modelId } +/** + * 根据消息ID查找消息 + * @param messageId 消息ID + * @returns 找到的消息,如果未找到则返回null + */ +export async function findMessageById(messageId: string): Promise { + console.log(`[findMessageById] 正在查找消息ID: ${messageId}`) + + try { + // 获取所有话题 + const topics = await db.topics.toArray() + console.log(`[findMessageById] 找到 ${topics.length} 个话题`) + + // 遍历所有话题,查找消息 + for (const topic of topics) { + if (!topic.messages || topic.messages.length === 0) { + continue + } + + const message = topic.messages.find((msg) => msg.id === messageId) + if (message) { + console.log(`[findMessageById] 在话题 ${topic.id} 中找到消息`) + return message + } + } + + console.log(`[findMessageById] 未找到消息ID: ${messageId}`) + return null + } catch (error) { + console.error(`[findMessageById] 查找消息时出错:`, error) + return null + } +} + export function resetAssistantMessage(message: Message, model?: Model): Message { return { ...message, diff --git a/src/renderer/src/store/memory.ts b/src/renderer/src/store/memory.ts index 0cc365b34f..6abd9e5c6f 100644 --- a/src/renderer/src/store/memory.ts +++ b/src/renderer/src/store/memory.ts @@ -1,7 +1,6 @@ -import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' -import { nanoid } from 'nanoid' -import log from 'electron-log' +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit' import store, { RootState } from '@renderer/store' +import { nanoid } from 'nanoid' // 记忆列表接口 export interface MemoryList { @@ -93,6 +92,7 @@ export interface MemoryState { isActive: boolean // 记忆功能是否激活 shortMemoryActive: boolean // 短记忆功能是否激活 autoAnalyze: boolean // 是否自动分析 + filterSensitiveInfo: boolean // 是否过滤敏感信息 analyzeModel: string | null // 用于长期记忆分析的模型ID shortMemoryAnalyzeModel: string | null // 用于短期记忆分析的模型ID historicalContextAnalyzeModel: string | null // 用于历史对话上下文分析的模型ID @@ -148,6 +148,7 @@ const initialState: MemoryState = { isActive: true, shortMemoryActive: true, // 默认启用短记忆功能 autoAnalyze: true, + filterSensitiveInfo: true, // 默认启用敏感信息过滤 analyzeModel: 'gpt-3.5-turbo', // 设置默认长期记忆分析模型 shortMemoryAnalyzeModel: 'gpt-3.5-turbo', // 设置默认短期记忆分析模型 historicalContextAnalyzeModel: 'gpt-3.5-turbo', // 设置默认历史对话上下文分析模型 @@ -288,6 +289,11 @@ const memorySlice = createSlice({ state.autoAnalyze = action.payload }, + // 设置是否过滤敏感信息 + setFilterSensitiveInfo: (state, action: PayloadAction) => { + state.filterSensitiveInfo = action.payload + }, + // 设置长期记忆分析模型 setAnalyzeModel: (state, action: PayloadAction) => { state.analyzeModel = action.payload @@ -497,10 +503,12 @@ const memorySlice = createSlice({ const messageIdReferences = new Map() // 统计所有记忆中每个消息ID的引用次数 - state.shortMemories.forEach(memory => { - if (memory.id !== action.payload && memory.analyzedMessageIds) { // 排除要删除的记忆 - memory.analyzedMessageIds.forEach(msgId => { - if (messageIdsToCheck.has(msgId)) { // 只关注要删除的记忆中的消息ID + state.shortMemories.forEach((memory) => { + if (memory.id !== action.payload && memory.analyzedMessageIds) { + // 排除要删除的记忆 + memory.analyzedMessageIds.forEach((msgId) => { + if (messageIdsToCheck.has(msgId)) { + // 只关注要删除的记忆中的消息ID messageIdReferences.set(msgId, (messageIdReferences.get(msgId) || 0) + 1) } }) @@ -508,10 +516,12 @@ const memorySlice = createSlice({ }) // 找出没有被其他记忆引用的消息ID - const unusedMessageIds = Array.from(messageIdsToCheck).filter(msgId => !messageIdReferences.has(msgId)) + const unusedMessageIds = Array.from(messageIdsToCheck).filter((msgId) => !messageIdReferences.has(msgId)) if (unusedMessageIds.length > 0) { - console.log(`[Memory] Found ${unusedMessageIds.length} message IDs that are no longer referenced by any memory`) + console.log( + `[Memory] Found ${unusedMessageIds.length} message IDs that are no longer referenced by any memory` + ) // 将这些消息ID标记为未分析,以便下次分析时重新分析这些消息 // 注意:我们不需要显式地清除标记,因为分析逻辑会检查消息ID是否在任何记忆的analyzedMessageIds中 @@ -655,11 +665,11 @@ const memorySlice = createSlice({ // 更新长期记忆优先级 if (state.memories && state.memories.length > 0) { - state.memories.forEach(memory => { + state.memories.forEach((memory) => { // 计算时间衰减因子 if (state.decayEnabled && memory.lastAccessedAt) { const daysSinceLastAccess = (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60 * 24) - const decayFactor = Math.max(0, 1 - (daysSinceLastAccess * state.decayRate)) + const decayFactor = Math.max(0, 1 - daysSinceLastAccess * state.decayRate) memory.decayFactor = decayFactor } else { memory.decayFactor = 1 // 无衰减 @@ -673,20 +683,20 @@ const memorySlice = createSlice({ : daysSinceCreation // 鲜度评分结合创建时间和最后访问时间 - const creationFreshness = Math.max(0, 1 - (daysSinceCreation / 30)) // 30天内创建的记忆较新 - const accessFreshness = Math.max(0, 1 - (lastAccessDays / 7)) // 7天内访问的记忆较新 - memory.freshness = (creationFreshness * 0.3) + (accessFreshness * 0.7) // 加权平均 + const creationFreshness = Math.max(0, 1 - daysSinceCreation / 30) // 30天内创建的记忆较新 + const accessFreshness = Math.max(0, 1 - lastAccessDays / 7) // 7天内访问的记忆较新 + memory.freshness = creationFreshness * 0.3 + accessFreshness * 0.7 // 加权平均 } }) } // 更新短期记忆优先级 if (state.shortMemories && state.shortMemories.length > 0) { - state.shortMemories.forEach(memory => { + state.shortMemories.forEach((memory) => { // 计算时间衰减因子 if (state.decayEnabled && memory.lastAccessedAt) { const hoursSinceLastAccess = (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60) - const decayFactor = Math.max(0, 1 - (hoursSinceLastAccess * state.decayRate * 4)) // 短期记忆衰减更快 + const decayFactor = Math.max(0, 1 - hoursSinceLastAccess * state.decayRate * 4) // 短期记忆衰减更快 memory.decayFactor = decayFactor } else { memory.decayFactor = 1 // 无衰减 @@ -700,9 +710,9 @@ const memorySlice = createSlice({ : hoursSinceCreation // 短期记忆的鲜度评分更注重最近性 - const creationFreshness = Math.max(0, 1 - (hoursSinceCreation / 24)) // 24小时内创建的记忆较新 - const accessFreshness = Math.max(0, 1 - (lastAccessHours / 6)) // 6小时内访问的记忆较新 - memory.freshness = (creationFreshness * 0.2) + (accessFreshness * 0.8) // 加权平均,更注重访问时间 + const creationFreshness = Math.max(0, 1 - hoursSinceCreation / 24) // 24小时内创建的记忆较新 + const accessFreshness = Math.max(0, 1 - lastAccessHours / 6) // 6小时内访问的记忆较新 + memory.freshness = creationFreshness * 0.2 + accessFreshness * 0.8 // 加权平均,更注重访问时间 } }) } @@ -718,29 +728,29 @@ const memorySlice = createSlice({ // 更新长期记忆鲜度 if (state.memories && state.memories.length > 0) { - state.memories.forEach(memory => { + state.memories.forEach((memory) => { const daysSinceCreation = (now - new Date(memory.createdAt).getTime()) / (1000 * 60 * 60 * 24) const lastAccessDays = memory.lastAccessedAt ? (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60 * 24) : daysSinceCreation - const creationFreshness = Math.max(0, 1 - (daysSinceCreation / 30)) - const accessFreshness = Math.max(0, 1 - (lastAccessDays / 7)) - memory.freshness = (creationFreshness * 0.3) + (accessFreshness * 0.7) + const creationFreshness = Math.max(0, 1 - daysSinceCreation / 30) + const accessFreshness = Math.max(0, 1 - lastAccessDays / 7) + memory.freshness = creationFreshness * 0.3 + accessFreshness * 0.7 }) } // 更新短期记忆鲜度 if (state.shortMemories && state.shortMemories.length > 0) { - state.shortMemories.forEach(memory => { + state.shortMemories.forEach((memory) => { const hoursSinceCreation = (now - new Date(memory.createdAt).getTime()) / (1000 * 60 * 60) const lastAccessHours = memory.lastAccessedAt ? (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60) : hoursSinceCreation - const creationFreshness = Math.max(0, 1 - (hoursSinceCreation / 24)) - const accessFreshness = Math.max(0, 1 - (lastAccessHours / 6)) - memory.freshness = (creationFreshness * 0.2) + (accessFreshness * 0.8) + const creationFreshness = Math.max(0, 1 - hoursSinceCreation / 24) + const accessFreshness = Math.max(0, 1 - lastAccessHours / 6) + memory.freshness = creationFreshness * 0.2 + accessFreshness * 0.8 }) } }, @@ -752,14 +762,14 @@ const memorySlice = createSlice({ if (isShortMemory) { // 更新短期记忆访问信息 - const memory = state.shortMemories?.find(m => m.id === id) + const memory = state.shortMemories?.find((m) => m.id === id) if (memory) { memory.accessCount = (memory.accessCount || 0) + 1 memory.lastAccessedAt = now } } else { // 更新长期记忆访问信息 - const memory = state.memories?.find(m => m.id === id) + const memory = state.memories?.find((m) => m.id === id) if (memory) { memory.accessCount = (memory.accessCount || 0) + 1 memory.lastAccessedAt = now @@ -822,7 +832,7 @@ const memorySlice = createSlice({ console.log('[Memory Reducer] Loaded short memory analyze model:', action.payload.shortMemoryAnalyzeModel) } - log.info('Short-term memory data loaded into state') + console.log('Short-term memory data loaded into state') } }) .addCase(loadLongTermMemoryData.fulfilled, (state, action) => { @@ -840,7 +850,7 @@ const memorySlice = createSlice({ // 自动选择默认的记忆列表 if (!state.currentListId && state.memoryLists && state.memoryLists.length > 0) { // 先尝试找到一个isActive为true的列表 - const activeList = state.memoryLists.find(list => list.isActive) + const activeList = state.memoryLists.find((list) => list.isActive) if (activeList) { state.currentListId = activeList.id console.log('[Memory Reducer] Auto-selected active memory list:', activeList.name) @@ -851,15 +861,21 @@ const memorySlice = createSlice({ } } - log.info('Long-term memory data loaded into state') + console.log('Long-term memory data loaded into state') if (action.payload.historicalContextAnalyzeModel) { state.historicalContextAnalyzeModel = action.payload.historicalContextAnalyzeModel - console.log('[Memory Reducer] Loaded historical context analyze model:', action.payload.historicalContextAnalyzeModel) + console.log( + '[Memory Reducer] Loaded historical context analyze model:', + action.payload.historicalContextAnalyzeModel + ) } else { // 如果文件中没有historicalContextAnalyzeModel,使用shortMemoryAnalyzeModel或analyzeModel作为默认值 state.historicalContextAnalyzeModel = state.shortMemoryAnalyzeModel || state.analyzeModel - console.log('[Memory Reducer] Using default model for historical context:', state.historicalContextAnalyzeModel) + console.log( + '[Memory Reducer] Using default model for historical context:', + state.historicalContextAnalyzeModel + ) } if (action.payload.vectorizeModel) { @@ -867,7 +883,7 @@ const memorySlice = createSlice({ console.log('[Memory Reducer] Loaded vectorize model:', action.payload.vectorizeModel) } - log.info('Memory data loaded into state') + console.log('Memory data loaded into state') } }) } @@ -879,6 +895,7 @@ export const { editMemory, setMemoryActive, setAutoAnalyze, + setFilterSensitiveInfo, setAnalyzeModel, setShortMemoryAnalyzeModel, setHistoricalContextAnalyzeModel, @@ -972,8 +989,11 @@ export const saveMemoryData = createAsyncThunk( const completeData = { // 基本设置 isActive: memoryData.isActive !== undefined ? memoryData.isActive : state.isActive, - shortMemoryActive: memoryData.shortMemoryActive !== undefined ? memoryData.shortMemoryActive : state.shortMemoryActive, + shortMemoryActive: + memoryData.shortMemoryActive !== undefined ? memoryData.shortMemoryActive : state.shortMemoryActive, autoAnalyze: memoryData.autoAnalyze !== undefined ? memoryData.autoAnalyze : state.autoAnalyze, + filterSensitiveInfo: + memoryData.filterSensitiveInfo !== undefined ? memoryData.filterSensitiveInfo : state.filterSensitiveInfo, // 模型选择 analyzeModel: memoryData.analyzeModel || state.analyzeModel, @@ -987,26 +1007,47 @@ export const saveMemoryData = createAsyncThunk( currentListId: memoryData.currentListId || state.currentListId, // 自适应分析相关 - adaptiveAnalysisEnabled: memoryData.adaptiveAnalysisEnabled !== undefined ? memoryData.adaptiveAnalysisEnabled : state.adaptiveAnalysisEnabled, - analysisFrequency: memoryData.analysisFrequency !== undefined ? memoryData.analysisFrequency : state.analysisFrequency, + adaptiveAnalysisEnabled: + memoryData.adaptiveAnalysisEnabled !== undefined + ? memoryData.adaptiveAnalysisEnabled + : state.adaptiveAnalysisEnabled, + analysisFrequency: + memoryData.analysisFrequency !== undefined ? memoryData.analysisFrequency : state.analysisFrequency, analysisDepth: memoryData.analysisDepth || state.analysisDepth, // 用户关注点相关 - interestTrackingEnabled: memoryData.interestTrackingEnabled !== undefined ? memoryData.interestTrackingEnabled : state.interestTrackingEnabled, + interestTrackingEnabled: + memoryData.interestTrackingEnabled !== undefined + ? memoryData.interestTrackingEnabled + : state.interestTrackingEnabled, // 性能监控相关 - monitoringEnabled: memoryData.monitoringEnabled !== undefined ? memoryData.monitoringEnabled : state.monitoringEnabled, + monitoringEnabled: + memoryData.monitoringEnabled !== undefined ? memoryData.monitoringEnabled : state.monitoringEnabled, // 智能优先级与时效性管理相关 - priorityManagementEnabled: memoryData.priorityManagementEnabled !== undefined ? memoryData.priorityManagementEnabled : state.priorityManagementEnabled, + priorityManagementEnabled: + memoryData.priorityManagementEnabled !== undefined + ? memoryData.priorityManagementEnabled + : state.priorityManagementEnabled, decayEnabled: memoryData.decayEnabled !== undefined ? memoryData.decayEnabled : state.decayEnabled, - freshnessEnabled: memoryData.freshnessEnabled !== undefined ? memoryData.freshnessEnabled : state.freshnessEnabled, + freshnessEnabled: + memoryData.freshnessEnabled !== undefined ? memoryData.freshnessEnabled : state.freshnessEnabled, decayRate: memoryData.decayRate !== undefined ? memoryData.decayRate : state.decayRate, // 上下文感知记忆推荐相关 - contextualRecommendationEnabled: memoryData.contextualRecommendationEnabled !== undefined ? memoryData.contextualRecommendationEnabled : state.contextualRecommendationEnabled, - autoRecommendMemories: memoryData.autoRecommendMemories !== undefined ? memoryData.autoRecommendMemories : state.autoRecommendMemories, - recommendationThreshold: memoryData.recommendationThreshold !== undefined ? memoryData.recommendationThreshold : state.recommendationThreshold, + contextualRecommendationEnabled: + memoryData.contextualRecommendationEnabled !== undefined + ? memoryData.contextualRecommendationEnabled + : state.contextualRecommendationEnabled, + autoRecommendMemories: + memoryData.autoRecommendMemories !== undefined + ? memoryData.autoRecommendMemories + : state.autoRecommendMemories, + recommendationThreshold: + memoryData.recommendationThreshold !== undefined + ? memoryData.recommendationThreshold + : state.recommendationThreshold } const result = await window.api.memory.saveData(completeData, forceOverwrite) @@ -1020,20 +1061,17 @@ export const saveMemoryData = createAsyncThunk( ) // 加载长期记忆数据的异步 thunk -export const loadLongTermMemoryData = createAsyncThunk( - 'memory/loadLongTermData', - async () => { - try { - console.log('[Long-term Memory] Loading long-term memory data from file...') - const data = await window.api.memory.loadLongTermData() - console.log('[Long-term Memory] Long-term memory data loaded successfully') - return data - } catch (error) { - console.error('[Long-term Memory] Failed to load long-term memory data:', error) - return null - } +export const loadLongTermMemoryData = createAsyncThunk('memory/loadLongTermData', async () => { + try { + console.log('[Long-term Memory] Loading long-term memory data from file...') + const data = await window.api.memory.loadLongTermData() + console.log('[Long-term Memory] Long-term memory data loaded successfully') + return data + } catch (error) { + console.error('[Long-term Memory] Failed to load long-term memory data:', error) + return null } -) +}) // 保存长期记忆数据的异步 thunk export const saveLongTermMemoryData = createAsyncThunk( @@ -1060,6 +1098,8 @@ export const saveLongTermMemoryData = createAsyncThunk( // 基本设置 isActive: memoryData.isActive !== undefined ? memoryData.isActive : state.isActive, autoAnalyze: memoryData.autoAnalyze !== undefined ? memoryData.autoAnalyze : state.autoAnalyze, + filterSensitiveInfo: + memoryData.filterSensitiveInfo !== undefined ? memoryData.filterSensitiveInfo : state.filterSensitiveInfo, // 模型选择 analyzeModel: memoryData.analyzeModel || state.analyzeModel, @@ -1070,26 +1110,47 @@ export const saveLongTermMemoryData = createAsyncThunk( currentListId: memoryData.currentListId || state.currentListId, // 自适应分析相关 - adaptiveAnalysisEnabled: memoryData.adaptiveAnalysisEnabled !== undefined ? memoryData.adaptiveAnalysisEnabled : state.adaptiveAnalysisEnabled, - analysisFrequency: memoryData.analysisFrequency !== undefined ? memoryData.analysisFrequency : state.analysisFrequency, + adaptiveAnalysisEnabled: + memoryData.adaptiveAnalysisEnabled !== undefined + ? memoryData.adaptiveAnalysisEnabled + : state.adaptiveAnalysisEnabled, + analysisFrequency: + memoryData.analysisFrequency !== undefined ? memoryData.analysisFrequency : state.analysisFrequency, analysisDepth: memoryData.analysisDepth || state.analysisDepth, // 用户关注点相关 - interestTrackingEnabled: memoryData.interestTrackingEnabled !== undefined ? memoryData.interestTrackingEnabled : state.interestTrackingEnabled, + interestTrackingEnabled: + memoryData.interestTrackingEnabled !== undefined + ? memoryData.interestTrackingEnabled + : state.interestTrackingEnabled, // 性能监控相关 - monitoringEnabled: memoryData.monitoringEnabled !== undefined ? memoryData.monitoringEnabled : state.monitoringEnabled, + monitoringEnabled: + memoryData.monitoringEnabled !== undefined ? memoryData.monitoringEnabled : state.monitoringEnabled, // 智能优先级与时效性管理相关 - priorityManagementEnabled: memoryData.priorityManagementEnabled !== undefined ? memoryData.priorityManagementEnabled : state.priorityManagementEnabled, + priorityManagementEnabled: + memoryData.priorityManagementEnabled !== undefined + ? memoryData.priorityManagementEnabled + : state.priorityManagementEnabled, decayEnabled: memoryData.decayEnabled !== undefined ? memoryData.decayEnabled : state.decayEnabled, - freshnessEnabled: memoryData.freshnessEnabled !== undefined ? memoryData.freshnessEnabled : state.freshnessEnabled, + freshnessEnabled: + memoryData.freshnessEnabled !== undefined ? memoryData.freshnessEnabled : state.freshnessEnabled, decayRate: memoryData.decayRate !== undefined ? memoryData.decayRate : state.decayRate, // 上下文感知记忆推荐相关 - contextualRecommendationEnabled: memoryData.contextualRecommendationEnabled !== undefined ? memoryData.contextualRecommendationEnabled : state.contextualRecommendationEnabled, - autoRecommendMemories: memoryData.autoRecommendMemories !== undefined ? memoryData.autoRecommendMemories : state.autoRecommendMemories, - recommendationThreshold: memoryData.recommendationThreshold !== undefined ? memoryData.recommendationThreshold : state.recommendationThreshold, + contextualRecommendationEnabled: + memoryData.contextualRecommendationEnabled !== undefined + ? memoryData.contextualRecommendationEnabled + : state.contextualRecommendationEnabled, + autoRecommendMemories: + memoryData.autoRecommendMemories !== undefined + ? memoryData.autoRecommendMemories + : state.autoRecommendMemories, + recommendationThreshold: + memoryData.recommendationThreshold !== undefined + ? memoryData.recommendationThreshold + : state.recommendationThreshold } const result = await window.api.memory.saveLongTermData(completeData, forceOverwrite) @@ -1103,57 +1164,54 @@ export const saveLongTermMemoryData = createAsyncThunk( ) // 保存所有记忆设置的函数 -export const saveAllMemorySettings = createAsyncThunk( - 'memory/saveAllSettings', - async (_, { dispatch, getState }) => { - try { - const state = (getState() as RootState).memory +export const saveAllMemorySettings = createAsyncThunk('memory/saveAllSettings', async (_, { dispatch, getState }) => { + try { + const state = (getState() as RootState).memory - // 创建一个包含所有设置的对象,但不包含记忆内容和记忆列表 - const settings = { - // 基本设置 - isActive: state.isActive, - shortMemoryActive: state.shortMemoryActive, - autoAnalyze: state.autoAnalyze, + // 创建一个包含所有设置的对象,但不包含记忆内容和记忆列表 + const settings = { + // 基本设置 + isActive: state.isActive, + shortMemoryActive: state.shortMemoryActive, + autoAnalyze: state.autoAnalyze, - // 模型选择 - analyzeModel: state.analyzeModel, - shortMemoryAnalyzeModel: state.shortMemoryAnalyzeModel, - historicalContextAnalyzeModel: state.historicalContextAnalyzeModel, - vectorizeModel: state.vectorizeModel, + // 模型选择 + analyzeModel: state.analyzeModel, + shortMemoryAnalyzeModel: state.shortMemoryAnalyzeModel, + historicalContextAnalyzeModel: state.historicalContextAnalyzeModel, + vectorizeModel: state.vectorizeModel, - // 自适应分析相关 - adaptiveAnalysisEnabled: state.adaptiveAnalysisEnabled, - analysisFrequency: state.analysisFrequency, - analysisDepth: state.analysisDepth, + // 自适应分析相关 + adaptiveAnalysisEnabled: state.adaptiveAnalysisEnabled, + analysisFrequency: state.analysisFrequency, + analysisDepth: state.analysisDepth, - // 用户关注点相关 - interestTrackingEnabled: state.interestTrackingEnabled, + // 用户关注点相关 + interestTrackingEnabled: state.interestTrackingEnabled, - // 性能监控相关 - monitoringEnabled: state.monitoringEnabled, + // 性能监控相关 + monitoringEnabled: state.monitoringEnabled, - // 智能优先级与时效性管理相关 - priorityManagementEnabled: state.priorityManagementEnabled, - decayEnabled: state.decayEnabled, - freshnessEnabled: state.freshnessEnabled, - decayRate: state.decayRate, + // 智能优先级与时效性管理相关 + priorityManagementEnabled: state.priorityManagementEnabled, + decayEnabled: state.decayEnabled, + freshnessEnabled: state.freshnessEnabled, + decayRate: state.decayRate, - // 上下文感知记忆推荐相关 - contextualRecommendationEnabled: state.contextualRecommendationEnabled, - autoRecommendMemories: state.autoRecommendMemories, - recommendationThreshold: state.recommendationThreshold, - } - - const result = await dispatch(saveMemoryData(settings)).unwrap() - console.log('[Memory] All memory settings saved successfully') - return result - } catch (error) { - console.error('[Memory] Failed to save all memory settings:', error) - throw error + // 上下文感知记忆推荐相关 + contextualRecommendationEnabled: state.contextualRecommendationEnabled, + autoRecommendMemories: state.autoRecommendMemories, + recommendationThreshold: state.recommendationThreshold } + + const result = await dispatch(saveMemoryData(settings)).unwrap() + console.log('[Memory] All memory settings saved successfully') + return result + } catch (error) { + console.error('[Memory] Failed to save all memory settings:', error) + throw error } -) +}) // Middleware removed to prevent duplicate saves triggered by batch additions. // Explicit saves should be handled where needed, e.g., at the end of analysis functions. diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 49e06b4de3..ccd86625d0 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -71,6 +71,13 @@ export type Message = { useful?: boolean error?: Record enabledMCPs?: MCPServer[] + // 引用消息 + referencedMessages?: { + id: string + content: string + role: 'user' | 'assistant' + createdAt: string + }[] metadata?: { // Gemini groundingMetadata?: GroundingMetadata diff --git a/敏感信息过滤功能实现方案.txt b/敏感信息过滤功能实现方案.txt new file mode 100644 index 0000000000..277c4cb802 --- /dev/null +++ b/敏感信息过滤功能实现方案.txt @@ -0,0 +1,280 @@ +# 敏感信息过滤功能实现方案(修改版) + +## 需求分析 + +用户希望增加一个按钮,控制记忆功能是否过滤密钥等安全敏感信息。当开启过滤功能时,分析模型会过滤掉密钥等敏感信息;关闭则不过滤。此功能对于保护用户隐私和敏感数据至关重要。 + +## 实现思路 + +1. 在Redux状态中添加一个新的状态属性`filterSensitiveInfo` +2. 在设置界面中添加一个开关按钮,默认为开启状态 +3. 修改分析函数,根据`filterSensitiveInfo`状态添加过滤指令 +4. 添加日志记录,跟踪过滤状态的变化 + +## 修改文件 + +### 1. 修改 src/renderer/src/store/memory.ts + +```typescript +// 在 MemoryState 接口中添加 +export interface MemoryState { + // 其他属性... + filterSensitiveInfo: boolean // 是否过滤敏感信息 +} + +// 在 initialState 中添加 +const initialState: MemoryState = { + // 其他属性... + filterSensitiveInfo: true, // 默认启用敏感信息过滤 +} + +// 添加新的 action creator +setFilterSensitiveInfo: (state, action: PayloadAction) => { + state.filterSensitiveInfo = action.payload +}, + +// 导出 action +export const { + // 其他 actions... + setFilterSensitiveInfo, +} = memorySlice.actions + +// 修改 saveMemoryData 函数,确保 filterSensitiveInfo 设置也被保存 +const completeData = { + // 基本设置 + isActive: memoryData.isActive !== undefined ? memoryData.isActive : state.isActive, + shortMemoryActive: memoryData.shortMemoryActive !== undefined ? memoryData.shortMemoryActive : state.shortMemoryActive, + autoAnalyze: memoryData.autoAnalyze !== undefined ? memoryData.autoAnalyze : state.autoAnalyze, + filterSensitiveInfo: memoryData.filterSensitiveInfo !== undefined ? memoryData.filterSensitiveInfo : state.filterSensitiveInfo, + + // 其他属性... +} + +// 同样修改 saveLongTermMemoryData 函数 +const completeData = { + // 基本设置 + isActive: memoryData.isActive !== undefined ? memoryData.isActive : state.isActive, + autoAnalyze: memoryData.autoAnalyze !== undefined ? memoryData.autoAnalyze : state.autoAnalyze, + filterSensitiveInfo: memoryData.filterSensitiveInfo !== undefined ? memoryData.filterSensitiveInfo : state.filterSensitiveInfo, + + // 其他属性... +} +``` + +### 2. 修改 src/renderer/src/pages/settings/MemorySettings/index.tsx + +```typescript +// 导入 InfoCircleOutlined 图标 +import { + AppstoreOutlined, + DeleteOutlined, + EditOutlined, + InfoCircleOutlined, + PlusOutlined, + SearchOutlined, + UnorderedListOutlined +} from '@ant-design/icons' + +// 导入 setFilterSensitiveInfo action +import { + addMemory, + clearMemories, + deleteMemory, + editMemory, + setAnalyzeModel, + setAnalyzing, + setAutoAnalyze, + setFilterSensitiveInfo, + setMemoryActive, + setShortMemoryAnalyzeModel, + saveMemoryData, + saveLongTermMemoryData, + saveAllMemorySettings +} from '@renderer/store/memory' + +// 从 Redux 获取 filterSensitiveInfo 状态 +const filterSensitiveInfo = useAppSelector((state) => state.memory?.filterSensitiveInfo ?? true) // 默认启用敏感信息过滤 + +// 添加处理切换敏感信息过滤的函数 +const handleToggleFilterSensitiveInfo = async (checked: boolean) => { + dispatch(setFilterSensitiveInfo(checked)) + console.log('[Memory Settings] Filter sensitive info set:', checked) + + // 使用Redux Thunk保存到JSON文件 + try { + await dispatch(saveMemoryData({ filterSensitiveInfo: checked })).unwrap() + console.log('[Memory Settings] Filter sensitive info saved to file successfully:', checked) + } catch (error) { + console.error('[Memory Settings] Failed to save filter sensitive info to file:', error) + } +} + +// 在短期记忆设置中添加开关按钮 + + + {t('settings.memory.filterSensitiveInfo') || '过滤敏感信息'} + + + + + + + +// 在长期记忆设置中也添加相同的开关按钮 + + + {t('settings.memory.filterSensitiveInfo') || '过滤敏感信息'} + + + + + + +``` + +### 3. 修改 src/renderer/src/services/MemoryService.ts + +```typescript +// 修改 analyzeConversation 函数 +const analyzeConversation = async ( + conversation: string, + modelId: string, + customPrompt?: string +): Promise> => { + try { + // 获取当前的过滤敏感信息设置 + const filterSensitiveInfo = store.getState().memory?.filterSensitiveInfo ?? true + + // 使用自定义提示词或默认提示词 + let basePrompt = + customPrompt || + ` +请分析对话内容,提取出重要的用户偏好、习惯、需求和背景信息,这些信息在未来的对话中可能有用。 + +将每条信息分类并按以下格式返回: +类别: 信息内容 + +类别应该是以下几种之一: +- 用户偏好:用户喜好、喜欢的事物、风格等 +- 技术需求:用户的技术相关需求、开发偏好等 +- 个人信息:用户的背景、经历等个人信息 +- 交互偏好:用户喜欢的交流方式、沟通风格等 +- 其他:不属于以上类别的重要信息 + +请确保每条信息都是简洁、准确的。如果没有找到重要信息,请返回空字符串。 +` + + // 如果启用了敏感信息过滤,添加相关指令 + if (filterSensitiveInfo) { + basePrompt += ` +## 安全提示: +请注意不要提取任何敏感信息,包括但不限于: +- API密钥、访问令牌或其他凭证 +- 密码或密码提示 +- 私人联系方式(如电话号码、邮箱地址) +- 个人身份信息(如身份证号、社保号) +- 银行账户或支付信息 +- 私密的个人或商业信息 + +如果发现此类信息,请完全忽略,不要以任何形式记录或提取。 +` + } + + // 其余代码保持不变... + } +} + +// 修改 analyzeAndAddShortMemories 函数 +export const analyzeAndAddShortMemories = async (topicId: string) => { + // 其他代码... + + try { + // 获取当前的过滤敏感信息设置 + const filterSensitiveInfo = store.getState().memory?.filterSensitiveInfo ?? true + + // 构建短期记忆分析提示词 + let prompt = ` +请对以下对话内容进行非常详细的分析和总结,提取对当前对话至关重要的上下文信息。请注意,这个分析将用于生成短期记忆,帮助AI理解当前对话的完整上下文。 + +分析要求: +1. 非常详细地总结用户的每一句话中表达的关键信息、需求和意图 +2. 全面分析AI回复中的重要内容和对用户问题的解决方案 +3. 详细记录对话中的重要事实、数据、代码示例和具体细节 +4. 清晰捕捉对话的逻辑发展、转折点和关键决策 +5. 提取对理解当前对话上下文必不可少的信息 +6. 记录用户提出的具体问题和关注点 +7. 捕捉用户在对话中表达的偏好、困惑和反馈 +8. 记录对话中提到的文件、路径、变量名等具体技术细节 +` + + // 如果启用了敏感信息过滤,添加相关指令 + if (filterSensitiveInfo) { + prompt += ` +9. 请注意不要提取任何敏感信息,包括但不限于: + - API密钥、访问令牌或其他凭证 + - 密码或密码提示 + - 私人联系方式(如电话号码、邮箱地址) + - 个人身份信息(如身份证号、社保号) + - 银行账户或支付信息 + - 私密的个人或商业信息 + 如果发现此类信息,请完全忽略,不要以任何形式记录或提取。 +` + } + + // 其余代码保持不变... + } +} +``` + +### 4. 修改 src/renderer/src/i18n/locales/zh-cn.json 和 en-us.json + +```json +{ + "settings": { + "memory": { + "filterSensitiveInfo": "过滤敏感信息", + "filterSensitiveInfoTip": "启用后,记忆功能将不会提取API密钥、密码等敏感信息" + } + } +} +``` + +```json +{ + "settings": { + "memory": { + "filterSensitiveInfo": "Filter Sensitive Information", + "filterSensitiveInfoTip": "When enabled, memory function will not extract API keys, passwords, or other sensitive information" + } + } +} +``` + +## 实现效果 + +这些修改后,用户将能够通过开关按钮控制记忆功能是否过滤敏感信息: + +1. 当开启过滤功能时(默认状态),分析模型会被明确指示不要提取API密钥、密码等敏感信息 +2. 当关闭过滤功能时,分析模型会正常提取所有信息,包括可能的敏感信息 + +开关按钮会出现在短期记忆和长期记忆设置中,用户可以根据需要随时切换。设置会被保存到配置文件中,确保应用重启后设置仍然生效。 + +## 思考过程 + +1. **状态管理**:首先考虑如何在Redux中添加新的状态属性,并确保它能够被正确保存和加载。 + +2. **UI设计**:在设置界面中添加开关按钮,并提供提示信息,帮助用户理解这个功能的作用。 + +3. **提示词修改**:根据开关状态修改分析提示词,添加不要提取敏感信息的指令。这是实现过滤功能的核心部分。 + +4. **国际化支持**:添加相关的翻译键值对,确保功能在不同语言环境下都能正常使用。 + +5. **持久化**:确保设置能够被正确保存到配置文件中,并在应用重启后加载。 + +## 注意事项 + +1. 这个功能只能在一定程度上防止敏感信息被提取,但不能完全保证。如果用户在对话中明确提到了敏感信息,AI模型可能仍然会提取部分内容。 + +2. 建议用户在讨论敏感信息时,最好暂时关闭记忆功能,或者在对话中避免提及敏感信息。 + +3. 这个功能只影响新分析的对话内容,已经存储的记忆不会受到影响。如果用户想要清除可能包含敏感信息的记忆,需要手动删除这些记忆。