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] =?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) => ( + +