diff --git a/package.json b/package.json index 36c43d9038..5435dbb887 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "adm-zip": "^0.5.16", "async-mutex": "^0.5.0", "color": "^5.0.0", + "d3": "^7.9.0", "diff": "^7.0.0", "docx": "^9.0.2", "electron-log": "^5.1.5", @@ -121,6 +122,7 @@ "@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch", "@tryfabric/martian": "^1.2.4", "@types/adm-zip": "^0", + "@types/d3": "^7", "@types/diff": "^7", "@types/fs-extra": "^11", "@types/js-yaml": "^4", diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 29433a2d8f..eed0bfa1ad 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -156,5 +156,14 @@ export enum IpcChannel { // Search Window SearchWindow_Open = 'search-window:open', SearchWindow_Close = 'search-window:close', - SearchWindow_OpenUrl = 'search-window:open-url' + SearchWindow_OpenUrl = 'search-window:open-url', + + // Memory File Storage + Memory_LoadData = 'memory:load-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/index.ts b/src/main/index.ts index 5102225781..ddd41fceb0 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,3 +1,5 @@ +import './services/MemoryFileService' + import { electronApp, optimizer } from '@electron-toolkit/utils' import { replaceDevtoolsFont } from '@main/utils/windowUtil' import { IpcChannel } from '@shared/IpcChannel' diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 1a328114a7..7d6968ad51 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -1,3 +1,5 @@ +import './services/MemoryFileService' + import fs from 'node:fs' import { arch } from 'node:os' @@ -19,6 +21,7 @@ import FileStorage from './services/FileStorage' import { GeminiService } from './services/GeminiService' import KnowledgeService from './services/KnowledgeService' import mcpService from './services/MCPService' +import { memoryFileService } from './services/MemoryFileService' import * as NutstoreService from './services/NutstoreService' import ObsidianVaultService from './services/ObsidianVaultService' import { ProxyConfig, proxyManager } from './services/ProxyManager' @@ -311,4 +314,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/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 new file mode 100644 index 0000000000..cf692eb7b3 --- /dev/null +++ b/src/main/mcpServers/simpleremember.ts @@ -0,0 +1,321 @@ +// src/main/mcpServers/simpleremember.ts +import { getConfigDir } from '@main/utils/file' +import { Server } from '@modelcontextprotocol/sdk/server/index.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' + +// 定义记忆文件路径 +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/main/services/BackupManager.ts b/src/main/services/BackupManager.ts index f29c6920e0..419bab8750 100644 --- a/src/main/services/BackupManager.ts +++ b/src/main/services/BackupManager.ts @@ -8,6 +8,7 @@ import * as fs from 'fs-extra' import * as path from 'path' import { createClient, CreateDirectoryOptions, FileStat } from 'webdav' +import { getConfigDir } from '../utils/file' import WebDav from './WebDav' import { windowService } from './WindowService' @@ -112,10 +113,29 @@ class BackupManager { // 使用流式复制 await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => { copiedSize += size - const progress = Math.min(80, 20 + Math.floor((copiedSize / totalSize) * 60)) + const progress = Math.min(70, 20 + Math.floor((copiedSize / totalSize) * 50)) onProgress({ stage: 'copying_files', progress, total: 100 }) }) + // 复制记忆数据文件 + const configDir = getConfigDir() + const memoryDataPath = path.join(configDir, 'memory-data.json') + const tempConfigDir = path.join(this.tempDir, 'Config') + const tempMemoryDataPath = path.join(tempConfigDir, 'memory-data.json') + + // 确保目录存在 + await fs.ensureDir(tempConfigDir) + + // 如果记忆数据文件存在,则复制 + if (await fs.pathExists(memoryDataPath)) { + await fs.copy(memoryDataPath, tempMemoryDataPath) + Logger.log('[BackupManager] Memory data file copied') + onProgress({ stage: 'copying_memory_data', progress: 75, total: 100 }) + } else { + Logger.log('[BackupManager] Memory data file not found, skipping') + onProgress({ stage: 'copying_memory_data', progress: 75, total: 100 }) + } + await this.setWritableRecursive(tempDataDir) onProgress({ stage: 'compressing', progress: 80, total: 100 }) @@ -177,11 +197,32 @@ class BackupManager { // 使用流式复制 await this.copyDirWithProgress(sourcePath, destPath, (size) => { copiedSize += size - const progress = Math.min(90, 40 + Math.floor((copiedSize / totalSize) * 50)) + const progress = Math.min(80, 40 + Math.floor((copiedSize / totalSize) * 40)) onProgress({ stage: 'copying_files', progress, total: 100 }) }) - Logger.log('[backup] step 4: clean up temp directory') + // 恢复记忆数据文件 + Logger.log('[backup] step 4: restore memory data file') + const tempConfigDir = path.join(this.tempDir, 'Config') + const tempMemoryDataPath = path.join(tempConfigDir, 'memory-data.json') + + if (await fs.pathExists(tempMemoryDataPath)) { + const configDir = getConfigDir() + const memoryDataPath = path.join(configDir, 'memory-data.json') + + // 确保目录存在 + await fs.ensureDir(configDir) + + // 复制记忆数据文件 + await fs.copy(tempMemoryDataPath, memoryDataPath) + Logger.log('[backup] Memory data file restored') + onProgress({ stage: 'restoring_memory_data', progress: 90, total: 100 }) + } else { + Logger.log('[backup] Memory data file not found in backup, skipping') + onProgress({ stage: 'restoring_memory_data', progress: 90, total: 100 }) + } + + Logger.log('[backup] step 5: clean up temp directory') // 清理临时目录 await this.setWritableRecursive(this.tempDir) await fs.remove(this.tempDir) diff --git a/src/main/services/MCPStreamableHttpClient.ts b/src/main/services/MCPStreamableHttpClient.ts index 1e080d2a71..bdb8ddadee 100644 --- a/src/main/services/MCPStreamableHttpClient.ts +++ b/src/main/services/MCPStreamableHttpClient.ts @@ -111,6 +111,20 @@ export class StreamableHTTPClientTransport implements Transport { headers.set('last-event-id', this._lastEventId) } + // 删除可能存在的HTTP/2伪头部 + if (headers.has(':path')) { + headers.delete(':path') + } + if (headers.has(':method')) { + headers.delete(':method') + } + if (headers.has(':authority')) { + headers.delete(':authority') + } + if (headers.has(':scheme')) { + headers.delete(':scheme') + } + const response = await fetch(this._url, { method: 'GET', headers, @@ -216,6 +230,21 @@ export class StreamableHTTPClientTransport implements Transport { headers.set('content-type', 'application/json') headers.set('accept', 'application/json, text/event-stream') + // 添加错误处理,确保不使用HTTP/2伪头部 + // 删除可能存在的HTTP/2伪头部 + if (headers.has(':path')) { + headers.delete(':path') + } + if (headers.has(':method')) { + headers.delete(':method') + } + if (headers.has(':authority')) { + headers.delete(':authority') + } + if (headers.has(':scheme')) { + headers.delete(':scheme') + } + const init = { ...this._requestInit, method: 'POST', diff --git a/src/main/services/MemoryFileService.ts b/src/main/services/MemoryFileService.ts new file mode 100644 index 0000000000..75d83a1850 --- /dev/null +++ b/src/main/services/MemoryFileService.ts @@ -0,0 +1,310 @@ +import log from 'electron-log' +import { promises as fs } from 'fs' +import path from 'path' + +import { getConfigDir } from '../utils/file' + +// 定义记忆文件路径 +const memoryDataPath = path.join(getConfigDir(), 'memory-data.json') +// 定义长期记忆文件路径 +const longTermMemoryDataPath = path.join(getConfigDir(), 'long-term-memory-data.json') + +export class MemoryFileService { + constructor() { + this.registerIpcHandlers() + } + + async loadData() { + 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 + } + ], + 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: '' + } + + // 合并默认数据和传入的数据,确保数据结构完整 + const completeData = { ...defaultData, ...data } + + // 保存数据 + await fs.writeFile(memoryDataPath, JSON.stringify(completeData, null, 2)) + log.info('Memory data saved successfully (force overwrite)') + return true + } + + // 尝试读取现有数据并合并 + 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 } + + // 处理每个属性 + 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') + 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 + } + + // 尝试读取现有数据并合并 + 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/main/services/ProxyManager.ts b/src/main/services/ProxyManager.ts index 24c741b5c3..64df333337 100644 --- a/src/main/services/ProxyManager.ts +++ b/src/main/services/ProxyManager.ts @@ -127,7 +127,15 @@ export class ProxyManager { const [protocol, address] = proxyUrl.split('://') const [host, port] = address.split(':') if (!protocol.includes('socks')) { - setGlobalDispatcher(new ProxyAgent(proxyUrl)) + // 使用标准方式创建ProxyAgent,但添加错误处理 + try { + // 尝试使用代理 + const agent = new ProxyAgent(proxyUrl) + setGlobalDispatcher(agent) + console.log('[Proxy] Successfully set HTTP proxy:', proxyUrl) + } catch (error) { + console.error('[Proxy] Failed to set proxy:', error) + } } else { const dispatcher = socksDispatcher({ port: parseInt(port), diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 163780e582..bc3d483e34 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -201,6 +201,13 @@ declare global { closeSearchWindow: (uid: string) => Promise openUrlInSearchWindow: (uid: string, url: string) => Promise } + memory: { + loadData: () => 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 882a15b76b..e12b3d55c4 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -182,6 +182,14 @@ const api = { openSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Open, uid), closeSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Close, uid), openUrlInSearchWindow: (uid: string, url: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_OpenUrl, uid, url) + }, + memory: { + loadData: () => ipcRenderer.invoke(IpcChannel.Memory_LoadData), + 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/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/assets/styles/markdown.scss b/src/renderer/src/assets/styles/markdown.scss index e24569b0f2..ee1e6da4ea 100644 --- a/src/renderer/src/assets/styles/markdown.scss +++ b/src/renderer/src/assets/styles/markdown.scss @@ -1,6 +1,7 @@ .markdown { color: var(--color-text); line-height: 1.6; + -webkit-user-select: text; user-select: text; word-break: break-word; diff --git a/src/renderer/src/components/MemoryProvider.tsx b/src/renderer/src/components/MemoryProvider.tsx new file mode 100644 index 0000000000..54b3e6103d --- /dev/null +++ b/src/renderer/src/components/MemoryProvider.tsx @@ -0,0 +1,230 @@ +import { useMemoryService } from '@renderer/services/MemoryService' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import store from '@renderer/store' +import { + clearShortMemories, + loadLongTermMemoryData, + loadMemoryData, + setAdaptiveAnalysisEnabled, + setAnalysisDepth, + setAnalysisFrequency, + setAutoAnalyze, + setAutoRecommendMemories, + setContextualRecommendationEnabled, + setCurrentMemoryList, + setDecayEnabled, + setDecayRate, + setFreshnessEnabled, + setInterestTrackingEnabled, + setMemoryActive, + setMonitoringEnabled, + setPriorityManagementEnabled, + setRecommendationThreshold, + setShortMemoryActive +} 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(() => { + console.log('[MemoryProvider] Loading memory data from file') + // 使用Redux Thunk加载短期记忆数据 + dispatch(loadMemoryData()) + .then((result) => { + if (result.payload) { + 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 short-term memory data loaded or loading failed') + } + }) + .catch((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]) + + // 当对话更新时,触发记忆分析 + useEffect(() => { + if (isActive && autoAnalyze && analyzeModel && messages.length > 0) { + // 获取当前的分析频率 + const memoryState = store.getState().memory || {} + const analysisFrequency = memoryState.analysisFrequency || 5 + const adaptiveAnalysisEnabled = memoryState.adaptiveAnalysisEnabled || false + + // 检查是否有新消息需要分析 + const newMessagesCount = messages.length - lastAnalyzedCountRef.current + + // 使用自适应分析频率 + if ( + newMessagesCount >= analysisFrequency || + (messages.length % analysisFrequency === 0 && lastAnalyzedCountRef.current === 0) + ) { + console.log( + `[Memory Analysis] Triggering analysis with ${newMessagesCount} new messages (frequency: ${analysisFrequency})` + ) + + // 将当前话题ID传递给分析函数 + analyzeAndAddMemories(currentTopic) + lastAnalyzedCountRef.current = messages.length + + // 性能监控:记录当前分析触发时的消息数量 + if (adaptiveAnalysisEnabled) { + console.log(`[Memory Analysis] Adaptive analysis enabled, current frequency: ${analysisFrequency}`) + } + } + } + }, [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 || 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} +} + +export default MemoryProvider diff --git a/src/renderer/src/components/Popups/ShortMemoryPopup.tsx b/src/renderer/src/components/Popups/ShortMemoryPopup.tsx new file mode 100644 index 0000000000..61e1be7dbb --- /dev/null +++ b/src/renderer/src/components/Popups/ShortMemoryPopup.tsx @@ -0,0 +1,273 @@ +import { DeleteOutlined, InfoCircleOutlined } from '@ant-design/icons' +import { Box } from '@renderer/components/Layout' +import { TopView } from '@renderer/components/TopView' +import { addShortMemoryItem, analyzeAndAddShortMemories } from '@renderer/services/MemoryService' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import store from '@renderer/store' +import { deleteShortMemory } from '@renderer/store/memory' +import { Button, Card, Col, Empty, Input, List, message, Modal, Row, Statistic, Tooltip } from 'antd' +import _ from 'lodash' +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +// 不再需要确认对话框 + +const ButtonGroup = styled.div` + display: flex; + gap: 8px; + margin-top: 8px; +` + +const MemoryContent = styled.div` + word-break: break-word; +` + +interface ShowParams { + topicId: string +} + +interface Props extends ShowParams { + resolve: (data: any) => void +} + +const PopupContainer: React.FC = ({ topicId, resolve }) => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const [open, setOpen] = useState(true) + + // 获取短记忆状态 + const shortMemoryActive = useAppSelector((state) => state.memory?.shortMemoryActive || false) + const shortMemories = useAppSelector((state) => { + const allShortMemories = state.memory?.shortMemories || [] + // 只显示当前话题的短记忆 + return topicId ? allShortMemories.filter((memory) => memory.topicId === topicId) : [] + }) + + // 添加短记忆的状态 + const [newMemoryContent, setNewMemoryContent] = useState('') + const [isAnalyzing, setIsAnalyzing] = useState(false) + + // 添加新的短记忆 - 使用防抖减少频繁更新 + const handleAddMemory = useCallback( + _.debounce(() => { + if (newMemoryContent.trim() && topicId) { + addShortMemoryItem(newMemoryContent.trim(), topicId) + setNewMemoryContent('') // 清空输入框 + } + }, 300), + [newMemoryContent, topicId] + ) + + // 手动分析对话内容 - 使用节流避免频繁分析操作 + const handleAnalyzeConversation = useCallback( + _.throttle(async () => { + if (!topicId || !shortMemoryActive) return + + setIsAnalyzing(true) + try { + const result = await analyzeAndAddShortMemories(topicId) + if (result) { + // 如果有新的短期记忆被添加 + Modal.success({ + title: t('settings.memory.shortMemoryAnalysisSuccess') || '分析成功', + content: t('settings.memory.shortMemoryAnalysisSuccessContent') || '已成功提取并添加重要信息到短期记忆' + }) + } else { + // 如果没有新的短期记忆被添加 + Modal.info({ + title: t('settings.memory.shortMemoryAnalysisNoNew') || '无新信息', + content: t('settings.memory.shortMemoryAnalysisNoNewContent') || '未发现新的重要信息或所有信息已存在' + }) + } + } catch (error) { + console.error('Failed to analyze conversation:', error) + Modal.error({ + title: t('settings.memory.shortMemoryAnalysisError') || '分析失败', + content: t('settings.memory.shortMemoryAnalysisErrorContent') || '分析对话内容时出错' + }) + } finally { + setIsAnalyzing(false) + } + }, 1000), + [topicId, shortMemoryActive, t] + ) + + // 删除短记忆 - 直接删除无需确认,使用节流避免频繁删除操作 + 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) + } + + const afterClose = () => { + resolve({}) + } + + ShortMemoryPopup.hide = onClose + + return ( + + + setNewMemoryContent(e.target.value)} + placeholder={t('settings.memory.addShortMemoryPlaceholder')} + autoSize={{ minRows: 2, maxRows: 4 }} + disabled={!shortMemoryActive || !topicId} + /> + + + + + + + {/* 性能监控统计信息 */} + + }> + + + + + + + + + + + + + + + + {shortMemories.length > 0 ? ( + ( + + + + + ) +} + +export default ContextualRecommendationSettings diff --git a/src/renderer/src/pages/settings/MemorySettings/HistoricalContextSettings.tsx b/src/renderer/src/pages/settings/MemorySettings/HistoricalContextSettings.tsx new file mode 100644 index 0000000000..91b9d3b4c5 --- /dev/null +++ b/src/renderer/src/pages/settings/MemorySettings/HistoricalContextSettings.tsx @@ -0,0 +1,115 @@ +import { InfoCircleOutlined } from '@ant-design/icons' +import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' +import { useProviders } from '@renderer/hooks/useProvider' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import { saveMemoryData, setHistoricalContextAnalyzeModel } from '@renderer/store/memory' +import { setEnableHistoricalContext } from '@renderer/store/settings' +import { Button, Switch, Tooltip } from 'antd' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' + +import { SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..' + +const HistoricalContextSettings: FC = () => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const { providers } = useProviders() + + // 获取相关状态 + const enableHistoricalContext = useAppSelector((state) => state.settings.enableHistoricalContext) + const historicalContextAnalyzeModel = useAppSelector((state) => state.memory.historicalContextAnalyzeModel) + + // 处理开关状态变化 + const handleHistoricalContextToggle = (checked: boolean) => { + dispatch(setEnableHistoricalContext(checked)) + } + + // 处理模型选择变化 + const handleModelChange = async (modelId: string) => { + dispatch(setHistoricalContextAnalyzeModel(modelId)) + console.log('[HistoricalContextSettings] Historical context analyze model set:', modelId) + + // 使用Redux Thunk保存到JSON文件 + try { + await dispatch(saveMemoryData({ historicalContextAnalyzeModel: modelId })).unwrap() + console.log('[HistoricalContextSettings] Historical context analyze model saved to file successfully:', modelId) + } catch (error) { + console.error('[HistoricalContextSettings] Failed to save historical context analyze model to file:', error) + } + } + + // 获取当前选中模型的名称 + const getSelectedModelName = () => { + if (!historicalContextAnalyzeModel) return '' + + // 遍历所有服务商的模型找到匹配的模型 + for (const provider of Object.values(providers)) { + const model = provider.models.find((m) => m.id === historicalContextAnalyzeModel) + if (model) { + return `${model.name} | ${provider.name}` + } + } + + return historicalContextAnalyzeModel + } + + return ( + + {t('settings.memory.historicalContext.title') || '历史对话上下文'} + + {t('settings.memory.historicalContext.description') || '允许AI在需要时自动引用历史对话,以提供更连贯的回答。'} + + + + + {t('settings.memory.historicalContext.enable') || '启用历史对话上下文'} + + + + + + + + + + {t('settings.memory.analyzeModel') || '分析模型'} + + + + + + + + ) +} + +export default HistoricalContextSettings diff --git a/src/renderer/src/pages/settings/MemorySettings/MemoryDeduplicationPanel.tsx b/src/renderer/src/pages/settings/MemorySettings/MemoryDeduplicationPanel.tsx new file mode 100644 index 0000000000..f966ca8802 --- /dev/null +++ b/src/renderer/src/pages/settings/MemorySettings/MemoryDeduplicationPanel.tsx @@ -0,0 +1,448 @@ +import { + CheckCircleOutlined, + DownOutlined, + MergeCellsOutlined, + QuestionCircleOutlined, + RightOutlined +} from '@ant-design/icons' +import { TopicManager } from '@renderer/hooks/useTopic' +import { + applyDeduplicationResult, + deduplicateAndMergeMemories, + DeduplicationResult +} from '@renderer/services/MemoryDeduplicationService' +import { useAppSelector } from '@renderer/store' +import store from '@renderer/store' +import { Topic } from '@renderer/types' +import { Button, Card, Collapse, Empty, List, Modal, Slider, Space, Spin, Tag, Typography } from 'antd' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +// 使用items属性,不再需要Panel组件 +const { Title, Text, Paragraph } = Typography + +interface MemoryDeduplicationPanelProps { + title?: string + description?: string + translationPrefix?: string + applyResults?: (result: DeduplicationResult) => void + isShortMemory?: boolean +} + +const MemoryDeduplicationPanel: React.FC = ({ + title, + description, + translationPrefix = 'settings.memory.deduplication', + applyResults, + isShortMemory = false +}) => { + const { t } = useTranslation() + const [isExpanded, setIsExpanded] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [deduplicationResult, setDeduplicationResult] = useState(null) + const [threshold, setThreshold] = useState(0.75) // 降低默认阈值以捕获更多相似记忆 + const [selectedListId, setSelectedListId] = useState(undefined) + const [selectedTopicId, setSelectedTopicId] = useState(undefined) + const [topicsList, setTopicsList] = useState([]) + const [loadingTopics, setLoadingTopics] = useState(false) + + // 获取记忆列表 + const memoryLists = useAppSelector((state) => state.memory?.memoryLists || []) + const memories = useAppSelector((state) => + isShortMemory ? state.memory?.shortMemories || [] : state.memory?.memories || [] + ) + + // 加载有短期记忆的话题 + useEffect(() => { + const loadTopics = async () => { + try { + setLoadingTopics(true) + + // 获取短期记忆 + const shortMemories = store.getState().memory?.shortMemories || [] + + // 获取所有有短期记忆的话题ID + const topicIds = Array.from(new Set(shortMemories.map((memory) => memory.topicId))) + + if (topicIds.length > 0) { + // 获取所有助手及其话题,确保我们使用与左侧列表相同的话题名称 + const assistants = store.getState().assistants?.assistants || [] + const allAssistantTopics = assistants.flatMap((assistant) => assistant.topics || []) + + // 创建完整的话题列表 + const fullTopics: Topic[] = [] + + for (const topicId of topicIds) { + // 首先尝试从助手的话题列表中找到完整的话题信息 + let topicInfo = allAssistantTopics.find((topic) => topic.id === topicId) + + // 如果在助手话题中找不到,则尝试从数据库获取 + if (!topicInfo) { + try { + const dbTopic = await TopicManager.getTopic(topicId) + if (dbTopic) { + topicInfo = { + id: dbTopic.id, + assistantId: '', + name: `话题 ${dbTopic.id.substring(0, 8)}`, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + messages: [] + } + } + } catch (error) { + console.error(`Failed to get topic name for ${topicId}:`, error) + } + } + + // 如果找到了话题信息,添加到列表中 + if (topicInfo) { + fullTopics.push(topicInfo) + } + } + + // 按更新时间排序,最新的在前 + const sortedTopics = fullTopics.sort((a, b) => { + return new Date(b.updatedAt || b.createdAt).getTime() - new Date(a.updatedAt || a.createdAt).getTime() + }) + + setTopicsList(sortedTopics) + } + } catch (error) { + console.error('Failed to load topics:', error) + } finally { + setLoadingTopics(false) + } + } + + loadTopics() + }, []) + + // 开始去重分析 + const handleDeduplication = async () => { + setIsLoading(true) + try { + if (isShortMemory) { + // 短期记忆去重 + const result = await deduplicateAndMergeMemories(undefined, true, selectedTopicId) + setDeduplicationResult(result) + } else { + // 长期记忆去重 + const result = await deduplicateAndMergeMemories(selectedListId, false) + setDeduplicationResult(result) + } + } finally { + setIsLoading(false) + } + } + + // 应用去重结果 + const handleApplyResult = () => { + if (!deduplicationResult) return + + Modal.confirm({ + title: t(`${translationPrefix}.confirmApply`), + content: t(`${translationPrefix}.confirmApplyContent`), + 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`) || '应用去重结果时发生错误,请重试' + }) + } + } + }) + } + + // 获取记忆内容 - 这个函数在renderItem中使用,确保没有删除错误 + const getMemoryContent = (index: string) => { + const memoryIndex = parseInt(index) - 1 + if (memoryIndex >= 0 && memoryIndex < memories.length) { + const memory = memories[memoryIndex] + return { + content: memory.content, + category: 'category' in memory ? memory.category || '其他' : '其他' + } + } + return { content: '', category: '' } + } + // 函数 getMemories 在第38行报错未使用,不是 getMemoryContent + // 将删除报错的 getMemories 函数 (实际检查代码发现没有 getMemories 函数,可能之前已删除或误报,先跳过此文件) + + // 渲染结果 + const renderResult = () => { + if (!deduplicationResult) return null + + if (deduplicationResult.similarGroups.length === 0) { + return ( + + ) + } + + return ( +
+ {t('settings.memory.deduplication.similarGroups')} + ({ + key: group.groupId, + label: ( + + + {t('settings.memory.deduplication.group')} {group.groupId} + + + ({group.memoryIds.length} {t('settings.memory.deduplication.items')}) + + {group.category && {group.category}} + + ), + children: ( + <> + + { + const memory = getMemoryContent(id) + return ( + + {id}} + description={ + <> + {memory.category} + {memory.content} + + } + /> + + ) + }} + /> + + + + + {group.category || t('settings.memory.deduplication.other')} + {group.mergedContent} + + + + ) + }))} + /> + +
+ +
+
+ ) + } + + // 切换折叠状态 + 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/MemoryListManager.tsx b/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx new file mode 100644 index 0000000000..7a7f83b441 --- /dev/null +++ b/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx @@ -0,0 +1,319 @@ +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, + saveLongTermMemoryData, + 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 = async () => { + if (!newListName.trim()) { + return // 名称不能为空 + } + + if (editingList) { + // 编辑现有列表 + await dispatch( + editMemoryList({ + id: editingList.id, + name: newListName, + description: newListDescription + }) + ) + } else { + // 添加新列表 + await dispatch( + addMemoryList({ + name: newListName, + description: newListDescription, + isActive: false + }) + ) + } + + // 保存到长期记忆文件 + 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('') + 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'), + 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 = 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 = 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 ( + +
+ {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" + /> + + + + + + ) +} + +export default PriorityManagementSettings diff --git a/src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx b/src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx new file mode 100644 index 0000000000..3a439b2b42 --- /dev/null +++ b/src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx @@ -0,0 +1,145 @@ +import { DeleteOutlined } from '@ant-design/icons' +import { addShortMemoryItem } from '@renderer/services/MemoryService' +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 _ from 'lodash' +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' + +const { Title } = Typography +// 不再需要确认对话框 + +const ShortMemoryManager = () => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + + // 获取当前话题ID + const currentTopicId = useAppSelector((state) => state.messages?.currentTopic?.id) + + // 获取短记忆状态 + const shortMemoryActive = useAppSelector((state) => state.memory?.shortMemoryActive || false) + const shortMemories = useAppSelector((state) => { + const allShortMemories = state.memory?.shortMemories || [] + // 只显示当前话题的短记忆 + return currentTopicId ? allShortMemories.filter((memory) => memory.topicId === currentTopicId) : [] + }) + + // 添加短记忆的状态 + const [newMemoryContent, setNewMemoryContent] = useState('') + + // 切换短记忆功能激活状态 + const handleToggleActive = (checked: boolean) => { + dispatch(setShortMemoryActive(checked)) + } + + // 添加新的短记忆 - 使用防抖减少频繁更新 + 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) + + // 执行删除操作 + 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(`[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] + ) + + return ( +
+
+ {t('settings.memory.shortMemory')} + + + +
+ +
+ setNewMemoryContent(e.target.value)} + placeholder={t('settings.memory.addShortMemoryPlaceholder')} + autoSize={{ minRows: 2, maxRows: 4 }} + disabled={!shortMemoryActive || !currentTopicId} + /> + +
+ +
+ {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..487126c898 --- /dev/null +++ b/src/renderer/src/pages/settings/MemorySettings/index.tsx @@ -0,0 +1,1327 @@ +import { + AppstoreOutlined, + DeleteOutlined, + EditOutlined, + InfoCircleOutlined, + PlusOutlined, + SearchOutlined, + UnorderedListOutlined +} from '@ant-design/icons' +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 { useAppDispatch, useAppSelector } from '@renderer/store' +import store from '@renderer/store' // Import store for direct access +import { + addMemory, + clearMemories, + deleteMemory, + editMemory, + saveAllMemorySettings, + saveLongTermMemoryData, + saveMemoryData, + setAnalyzeModel, + setAnalyzing, + setAutoAnalyze, + setFilterSensitiveInfo, + setMemoryActive, + 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' +import { FC, useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { + SettingContainer, + SettingDivider, + SettingGroup, + SettingHelpText, + SettingRow, + SettingRowTitle, + 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' + +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 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) + + // 从 Redux 获取所有模型,不仅仅是可用的模型 + const providers = useAppSelector((state) => state.llm?.providers || []) + + // 使用 useMemo 缓存模型数组,避免不必要的重新渲染 + const models = useMemo(() => { + // 只获取已启用的提供商的模型 + return providers + .filter((provider) => provider.enabled) // 只保留已启用的提供商 + .flatMap((provider) => provider.models || []) + }, [providers]) + + // 我们不再使用modelOptions,因为我们现在使用SelectModelPopup组件 + + // 如果没有模型,添加一个默认模型 + 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 [currentPage, setCurrentPage] = useState(1) + const pageSize = 15 // 每页显示15条记忆 + + // 处理添加记忆 + 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 = async (id: string) => { + // 先从当前状态中获取要删除的记忆之外的所有记忆 + const state = store.getState().memory + 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() + console.log('[Memory Settings] Long-term memories saved to file after deletion (force overwrite)') + + message.success(t('settings.memory.deleteSuccess')) + } catch (error) { + console.error('[Memory Settings] Failed to save long-term memory data after deletion:', error) + } + } + + // 保存所有设置 + const handleSaveAllSettings = async () => { + try { + const result = await dispatch(saveAllMemorySettings()) + if (result.meta.requestStatus === 'fulfilled') { + message.success(t('settings.memory.saveAllSettingsSuccess') || '所有设置已成功保存') + console.log('[Memory Settings] All memory settings saved successfully') + } else { + message.error(t('settings.memory.saveAllSettingsError') || '保存设置失败') + console.error('[Memory Settings] Failed to save all memory settings:', result.payload) + } + } catch (error) { + console.error('[Memory Settings] Failed to save all memory settings:', error) + message.error(t('settings.memory.saveAllSettingsError') || '保存设置失败') + } + } + + // 处理清空记忆 + const handleClearMemories = async () => { + dispatch(clearMemories(currentListId || undefined)) + setIsClearModalVisible(false) + + // 将清空后的状态保存到长期记忆文件,并强制覆盖 + try { + // 直接传递空数组作为 memories,确保完全清空 + const state = store.getState().memory + 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) + } + + message.success(t('settings.memory.clearSuccess')) + } + + // 处理切换记忆功能 + const handleToggleMemory = (checked: boolean) => { + dispatch(setMemoryActive(checked)) + } + + // 处理切换自动分析 + const handleToggleAutoAnalyze = (checked: boolean) => { + 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)) + console.log('[Memory Settings] Analyze model set:', modelId) + + // 使用Redux Thunk保存到JSON文件 + try { + 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) + } + } + + // 处理选择短期记忆分析模型 + const handleSelectShortMemoryModel = async (modelId: string) => { + dispatch(setShortMemoryAnalyzeModel(modelId)) + console.log('[Memory Settings] Short memory analyze model set:', modelId) + + // 使用Redux Thunk保存到JSON文件 + try { + 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) + } + } + + // 手动触发分析 + 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 getSelectedModelName = () => { + if (!analyzeModel) return '' + + // 遍历所有服务商的模型找到匹配的模型 + for (const provider of Object.values(providers)) { + const model = provider.models.find((m) => m.id === analyzeModel) + if (model) { + return `${model.name} | ${provider.name}` + } + } + + return analyzeModel + } + + // 获取当前选中短期记忆模型的名称 + const getSelectedShortMemoryModelName = () => { + if (!shortMemoryAnalyzeModel) return '' + + // 遍历所有服务商的模型找到匹配的模型 + for (const provider of Object.values(providers)) { + const model = provider.models.find((m) => m.id === shortMemoryAnalyzeModel) + if (model) { + return `${model.name} | ${provider.name}` + } + } + + return shortMemoryAnalyzeModel + } + + // 重置长期记忆分析标记 + const handleResetLongTermMemoryAnalyzedMessageIds = async () => { + if (!selectedTopicId) { + message.warning(t('settings.memory.selectTopicFirst') || '请先选择要重置的话题') + return + } + + try { + const result = await resetLongTermMemoryAnalyzedMessageIds(selectedTopicId) + if (result) { + message.success(t('settings.memory.resetLongTermMemorySuccess') || '长期记忆分析标记已重置') + + // 重置成功后,自动触发分析 + message.info(t('settings.memory.startingAnalysis') || '开始分析...') + setTimeout(() => { + // 使用延时确保重置操作已完成 + analyzeAndAddMemories(selectedTopicId) + }, 500) + } else { + message.info(t('settings.memory.resetLongTermMemoryNoChange') || '没有需要重置的分析标记') + } + } catch (error) { + console.error('Failed to reset long-term memory analyzed message IDs:', error) + message.error(t('settings.memory.resetLongTermMemoryError') || '重置长期记忆分析标记失败') + } + } + + // 添加滚动检测 + 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 ( + + {/* 1. 将 TabsContainer 移到 SettingContainer 顶部 */} + + + + {t('settings.memory.shortMemory') || '短期记忆'} + + ), + children: ( + // 将原来...中的内容放在这里 + + {t('settings.memory.title')} + {t('settings.memory.description')} + + + {t('settings.memory.shortMemorySettings')} + {t('settings.memory.shortMemoryDescription')} + + + {/* 保留原有的短期记忆设置 */} + + {t('settings.memory.enableMemory')} + + + + {t('settings.memory.enableAutoAnalyze')} + + + + + {t('settings.memory.filterSensitiveInfo') || '过滤敏感信息'} + + + + + + + + {/* 短期记忆分析模型选择 */} + + + {t('settings.memory.shortMemoryAnalyzeModel') || '短期记忆分析模型'} + + + + + {/* 话题选择 */} + {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') || '手动分析'} + + + + {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) + .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).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} + /> + + )} + + ) : ( + + ) + ) : isActive ? ( + + + currentListId ? memory.listId === currentListId : true + )} + onEditMemory={(id) => { + const memory = memories.find((m) => m.id === id) + if (memory) { + setEditingMemory({ id: memory.id, content: memory.content }) + setIsEditModalVisible(true) + } + }} + onDeleteMemory={handleDeleteMemory} + /> + + ) : ( + + )} + + + ) + } + ]} + /> + + {/* 8. 移除外部的 SettingGroup 包裹,Modal 等保持在 SettingContainer 内 */} + {/* 添加记忆对话框 (保持不变) */} + setIsAddModalVisible(false)} + okButtonProps={{ disabled: !newMemory.trim() }}> + setNewMemory(e.target.value)} + placeholder={t('settings.memory.memoryPlaceholder')} + /> + + + {/* 编辑记忆对话框 */} + setIsEditModalVisible(false)} + okButtonProps={{ disabled: !editingMemory?.content.trim() }}> + setEditingMemory((prev) => (prev ? { ...prev, content: e.target.value } : null))} + placeholder={t('settings.memory.memoryPlaceholder')} + /> + + + {/* 清空记忆确认对话框 */} + setIsClearModalVisible(false)} + okButtonProps={{ danger: true }} + okText={t('common.confirm')} + cancelText={t('common.cancel')}> +

+ {currentListId + ? t('settings.memory.clearConfirmContentList', { + name: memoryLists.find((list) => list.id === currentListId)?.name || '' + }) + : t('settings.memory.clearConfirmContent')} +

+
+ + ) +} + +const MemoryListHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +` + +const ButtonGroup = styled.div` + display: flex; + gap: 8px; +` + +const MemoryListContainer = styled.div` + max-height: calc(60vh - 100px); + min-height: 400px; + overflow-y: auto; + border: 1px solid var(--color-border); + border-radius: 4px; + padding: 8px; + position: relative; /* 为滚动指示器添加定位上下文 */ + + /* 确保容器高度可以自适应 */ + &:has(.ant-list) { + height: auto; + } + + /* 添加媒体查询以适应不同屏幕尺寸 */ + @media (min-height: 900px) { + max-height: calc(70vh - 100px); + } + + @media (max-height: 700px) { + max-height: calc(50vh - 80px); + } + + /* 自定义滚动条样式 */ + &::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + &::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 4px; + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--color-primary); + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + /* 滚动指示器 */ + &::after { + content: ''; + position: absolute; + bottom: 10px; + right: 10px; + width: 24px; + height: 24px; + border-radius: 50%; + background-color: var(--color-primary); + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + background-image: url('data:image/svg+xml;utf8,'); + background-repeat: no-repeat; + background-position: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + transform: rotate(180deg); + } + + &.scrollable::after { + opacity: 0.7; + } +` + +const MemoryItemMeta = styled.div` + display: flex; + justify-content: space-between; + color: var(--color-text-3); + font-size: 12px; +` + +const TabLabelContainer = styled.span` + display: flex; + align-items: center; + gap: 8px; +` + +const TabDot = styled.span<{ color: string }>` + font-size: 18px; + color: ${(props) => props.color}; +` + +const ButtonsContainer = styled.div` + display: flex; + gap: 8px; +` + +const TagWithCursor = styled(Tag)` + cursor: pointer; + margin-right: 8px; +` + +const StyledRadioGroup = styled(Radio.Group)` + margin-right: 16px; +` + +const TabPaneSettingGroup = styled(SettingGroup)` + border-top-left-radius: 0; + border-top-right-radius: 0; + margin-top: 0; +` + +const MemoryMindMapContainer = styled.div` + width: 100%; + height: calc(60vh - 100px); + min-height: 400px; + margin-bottom: 20px; + display: flex; + flex-direction: column; + + /* 添加媒体查询以适应不同屏幕尺寸 */ + @media (min-height: 900px) { + height: calc(70vh - 100px); + } + + @media (max-height: 700px) { + height: calc(50vh - 80px); + } +` + +const PaginationContainer = styled.div` + display: flex; + justify-content: center; + padding: 12px 0; + border-top: 1px solid var(--color-border); +` + +const CategoryFilterContainer = styled.div` + display: flex; + align-items: center; + margin-bottom: 16px; + flex-wrap: wrap; + gap: 8px; + + > span { + margin-right: 8px; + font-weight: 500; + } + + > div { + display: flex; + flex-wrap: wrap; + gap: 4px; + } +` + +const TabsContainer = styled.div` + margin: -20px -20px 0 -20px; /* 负边距使选项卡扩展到容器边缘 */ + + .ant-tabs { + width: 100%; + } + + .ant-tabs-nav { + margin-bottom: 0; + background: var(--color-background-soft); + padding: 0; + border-radius: 0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + position: relative; + overflow: hidden; + border-bottom: 1px solid var(--color-border); + } + + .ant-tabs-nav::before { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background-color: var(--color-border); + opacity: 0.5; + } + + .ant-tabs-nav-wrap { + padding: 0 8px; + } + + .ant-tabs-tab { + font-weight: 500; + padding: 14px 24px; + margin: 0 4px; + transition: all 0.3s; + border-radius: 8px 8px 0 0; + position: relative; + top: 1px; + } + + .ant-tabs-tab:first-child { + margin-left: 8px; + } + + .ant-tabs-tab:hover { + color: var(--color-primary); + background-color: rgba(0, 0, 0, 0.02); + } + + .ant-tabs-tab-active { + background-color: var(--color-background-soft); + border-top-left-radius: 8px; + border-top-right-radius: 8px; + } + + .ant-tabs-tab-active::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background-color: var(--color-primary); + border-radius: 3px 3px 0 0; + } + + .ant-tabs-tab-active .ant-tabs-tab-btn { + color: var(--color-primary) !important; + font-weight: 600; + } + + .ant-tabs-ink-bar { + display: none; + } + + .ant-tabs-content-holder { + padding: 0; + } + + .ant-tabs-nav-operations { + display: none !important; + } +` + +export default MemorySettings diff --git a/src/renderer/src/pages/settings/SettingsPage.tsx b/src/renderer/src/pages/settings/SettingsPage.tsx index 3f8f1efdca..dc4780082e 100644 --- a/src/renderer/src/pages/settings/SettingsPage.tsx +++ b/src/renderer/src/pages/settings/SettingsPage.tsx @@ -1,3 +1,17 @@ +import { + AppstoreOutlined, + CloudOutlined, + CodeOutlined, + ExperimentOutlined, + GlobalOutlined, + InfoCircleOutlined, + LayoutOutlined, + MacCommandOutlined, + RocketOutlined, + SaveOutlined, + SettingOutlined, + ThunderboltOutlined +} from '@ant-design/icons' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon' import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings' @@ -27,6 +41,7 @@ import DisplaySettings from './DisplaySettings/DisplaySettings' import GeneralSettings from './GeneralSettings' import MCPSettings from './MCPSettings' import { McpSettingsNavbar } from './MCPSettings/McpSettingsNavbar' +import MemorySettings from './MemorySettings' import MiniAppSettings from './MiniappSettings/MiniAppSettings' import ProvidersList from './ProviderSettings' import QuickAssistantSettings from './QuickAssistantSettings' @@ -74,6 +89,12 @@ const SettingsPage: FC = () => { {t('settings.mcp.title')} + + + + {t('settings.memory.title')} + + @@ -131,6 +152,7 @@ const SettingsPage: FC = () => { } /> } /> } /> + } /> } /> } /> {showMiniAppSettings && } />} @@ -156,6 +178,8 @@ const ContentContainer = styled.div` display: flex; flex: 1; flex-direction: row; + height: calc(100vh - var(--navbar-height)); /* 设置高度为视口高度减去导航栏高度 */ + overflow: hidden; /* 防止内容溢出 */ ` const SettingMenus = styled.ul` @@ -165,6 +189,26 @@ const SettingMenus = styled.ul` border-right: 0.5px solid var(--color-border); padding: 10px; user-select: none; + overflow-y: auto; /* 允许菜单滚动 */ + + /* 添加滚动条样式 */ + &::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + &::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 3px; + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--color-primary); + } + + &::-webkit-scrollbar-track { + background: transparent; + } ` const MenuItemLink = styled(Link)` @@ -209,6 +253,26 @@ const SettingContent = styled.div` height: 100%; flex: 1; border-right: 0.5px solid var(--color-border); + overflow-y: auto; /* 添加滚动属性,允许内容滚动 */ + + /* 添加滚动条样式 */ + &::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + &::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 4px; + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--color-primary); + } + + &::-webkit-scrollbar-track { + background: transparent; + } ` export default SettingsPage diff --git a/src/renderer/src/pages/settings/index.tsx b/src/renderer/src/pages/settings/index.tsx index 072e5ead90..9d8b3984c8 100644 --- a/src/renderer/src/pages/settings/index.tsx +++ b/src/renderer/src/pages/settings/index.tsx @@ -7,16 +7,58 @@ export const SettingContainer = styled.div<{ theme?: ThemeMode }>` display: flex; flex-direction: column; flex: 1; - height: calc(100vh - var(--navbar-height)); + min-height: calc(100vh - var(--navbar-height)); + height: auto; padding: 20px; padding-top: 15px; padding-bottom: 75px; - overflow-y: scroll; + overflow-y: auto; /* 改为auto,只在需要时显示滚动条 */ font-family: Ubuntu; background: ${(props) => (props.theme === 'dark' ? 'transparent' : 'var(--color-background-soft)')}; + /* 添加滚动指示器 */ + &::after { + content: ''; + position: fixed; + bottom: 20px; + right: 20px; + width: 40px; + height: 40px; + border-radius: 50%; + background-color: var(--color-primary); + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + background-image: url('data:image/svg+xml;utf8,'); + background-repeat: no-repeat; + background-position: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + } + + &.scrollable::after { + opacity: 0.7; + } + &::-webkit-scrollbar { - display: none; + width: 10px; + height: 10px; + } + + &::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 5px; + border: 2px solid transparent; + background-clip: content-box; + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--color-primary); + border: 2px solid transparent; + background-clip: content-box; + } + + &::-webkit-scrollbar-track { + background: transparent; } ` diff --git a/src/renderer/src/providers/AiProvider/AnthropicProvider.ts b/src/renderer/src/providers/AiProvider/AnthropicProvider.ts index 0c936d6951..bbb228b998 100644 --- a/src/renderer/src/providers/AiProvider/AnthropicProvider.ts +++ b/src/renderer/src/providers/AiProvider/AnthropicProvider.ts @@ -10,6 +10,8 @@ import { filterEmptyMessages, filterUserRoleStartMessages } from '@renderer/services/MessagesService' +import store from '@renderer/store' +import { getActiveServers } from '@renderer/store/mcp' import { Assistant, FileTypes, MCPToolResponse, Message, Model, Provider, Suggestion } from '@renderer/types' import { removeSpecialCharactersForTopicName } from '@renderer/utils' import { mcpToolCallResponseToAnthropicMessage, parseAndCallTools } from '@renderer/utils/mcp-tools' @@ -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 = { @@ -478,14 +480,42 @@ export default class AnthropicProvider extends BaseProvider { * Generate text * @param prompt - The prompt * @param content - The content + * @param modelId - Optional model ID to use * @returns The generated text */ - public async generateText({ prompt, content }: { prompt: string; content: string }): Promise { - const model = getDefaultModel() + public async generateText({ + prompt, + content, + modelId + }: { + prompt: string + content: string + modelId?: string + }): Promise { + // 使用指定的模型或默认模型 + const model = modelId + ? store + .getState() + .llm.providers.flatMap((provider) => provider.models) + .find((m) => m.id === modelId) + : getDefaultModel() + + if (!model) { + console.error(`Model ${modelId} not found, using default model`) + return '' + } + + // 应用记忆功能到系统提示词 + const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService') + const enhancedPrompt = await applyMemoriesToPrompt(prompt) + console.log( + '[AnthropicProvider] Applied memories to prompt, length difference:', + enhancedPrompt.length - prompt.length + ) const message = await this.sdk.messages.create({ model: model.id, - system: prompt, + system: enhancedPrompt, stream: false, max_tokens: 4096, messages: [ diff --git a/src/renderer/src/providers/AiProvider/BaseProvider.ts b/src/renderer/src/providers/AiProvider/BaseProvider.ts index 6f7dc9f02c..2b21ec460f 100644 --- a/src/renderer/src/providers/AiProvider/BaseProvider.ts +++ b/src/renderer/src/providers/AiProvider/BaseProvider.ts @@ -37,7 +37,15 @@ export default abstract class BaseProvider { abstract summaries(messages: Message[], assistant: Assistant): Promise abstract summaryForSearch(messages: Message[], assistant: Assistant): Promise abstract suggestions(messages: Message[], assistant: Assistant): Promise - abstract generateText({ prompt, content }: { prompt: string; content: string }): Promise + abstract generateText({ + prompt, + content, + modelId + }: { + prompt: string + content: string + modelId?: string + }): Promise abstract check(model: Model): Promise<{ valid: boolean; error: Error | null }> abstract models(): Promise abstract generateImage(params: GenerateImageParams): Promise @@ -94,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 f337f80221..08b10142b5 100644 --- a/src/renderer/src/providers/AiProvider/GeminiProvider.ts +++ b/src/renderer/src/providers/AiProvider/GeminiProvider.ts @@ -30,6 +30,8 @@ import { filterUserRoleStartMessages } from '@renderer/services/MessagesService' import WebSearchService from '@renderer/services/WebSearchService' +import store from '@renderer/store' +import { getActiveServers } from '@renderer/store/mcp' import { Assistant, FileType, FileTypes, MCPToolResponse, Message, Model, Provider, Suggestion } from '@renderer/types' import { removeSpecialCharactersForTopicName } from '@renderer/utils' import { mcpToolCallResponseToGeminiMessage, parseAndCallTools } from '@renderer/utils/mcp-tools' @@ -228,7 +230,11 @@ 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) @@ -467,11 +473,40 @@ export default class GeminiProvider extends BaseProvider { * Generate text * @param prompt - The prompt * @param content - The content + * @param modelId - Optional model ID to use * @returns The generated text */ - public async generateText({ prompt, content }: { prompt: string; content: string }): Promise { - const model = getDefaultModel() - const systemMessage = { role: 'system', content: prompt } + public async generateText({ + prompt, + content, + modelId + }: { + prompt: string + content: string + modelId?: string + }): Promise { + // 使用指定的模型或默认模型 + const model = modelId + ? store + .getState() + .llm.providers.flatMap((provider) => provider.models) + .find((m) => m.id === modelId) + : getDefaultModel() + + if (!model) { + console.error(`Model ${modelId} not found, using default model`) + return '' + } + + // 应用记忆功能到系统提示词 + const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService') + const enhancedPrompt = await applyMemoriesToPrompt(prompt) + console.log( + '[GeminiProvider] Applied memories to prompt, length difference:', + enhancedPrompt.length - prompt.length + ) + + const systemMessage = { role: 'system', content: enhancedPrompt } const geminiModel = this.sdk.getGenerativeModel( { @@ -483,7 +518,7 @@ export default class GeminiProvider extends BaseProvider { const chat = await geminiModel.startChat() const messageContent = isGemmaModel(model) - ? `user\n${prompt}\nuser\n${content}` + ? `user\n${enhancedPrompt}\nuser\n${content}` : content const { response } = await chat.sendMessage(messageContent) diff --git a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts index b78bf2c1e5..a64e173f4b 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, @@ -310,7 +311,17 @@ export default class OpenAIProvider extends BaseProvider { const model = assistant.model || defaultModel const { contextCount, maxTokens, streamOutput } = getAssistantSettings(assistant) messages = addImageFileToContents(messages) - let systemMessage = { role: 'system', content: assistant.prompt || '' } + // 应用记忆功能到系统提示词 + const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService') + // 获取当前话题ID + const currentTopicId = messages.length > 0 ? messages[0].topicId : undefined + const enhancedPrompt = await applyMemoriesToPrompt(assistant.prompt || '', currentTopicId) + console.log( + '[OpenAIProvider.completions] Applied memories to prompt, length difference:', + enhancedPrompt.length - (assistant.prompt || '').length + ) + + let systemMessage = { role: 'system', content: enhancedPrompt } if (isOpenAIoSeries(model)) { systemMessage = { role: 'developer', @@ -318,7 +329,11 @@ 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[] = [] @@ -544,12 +559,23 @@ export default class OpenAIProvider extends BaseProvider { async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void) { const defaultModel = getDefaultModel() const model = assistant.model || defaultModel + + // 应用记忆功能到系统提示词 + const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService') + // 获取当前话题ID + const currentTopicId = message.topicId + const enhancedPrompt = await applyMemoriesToPrompt(assistant.prompt || '', currentTopicId) + console.log( + '[OpenAIProvider.translate] Applied memories to prompt, length difference:', + enhancedPrompt.length - (assistant.prompt || '').length + ) + const messages = message.content ? [ - { role: 'system', content: assistant.prompt }, + { role: 'system', content: enhancedPrompt }, { role: 'user', content: message.content } ] - : [{ role: 'user', content: assistant.prompt }] + : [{ role: 'user', content: enhancedPrompt }] const isOpenAIReasoning = this.isOpenAIReasoning(model) @@ -630,9 +656,25 @@ export default class OpenAIProvider extends BaseProvider { return prev + (prev ? '\n' : '') + content }, '') + // 获取原始提示词 + const originalPrompt = getStoreSetting('topicNamingPrompt') || i18n.t('prompts.title') + + // 应用记忆功能到系统提示词 + const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService') + // 获取当前话题ID + const currentTopicId = messages.length > 0 ? messages[0].topicId : undefined + // 使用双重类型断言强制转换类型 + const enhancedPrompt = (await applyMemoriesToPrompt(originalPrompt as string, currentTopicId)) as unknown as string + // 存储原始提示词长度 + const originalPromptLength = (originalPrompt as string).length + console.log( + '[OpenAIProvider.summaries] Applied memories to prompt, length difference:', + enhancedPrompt.length - originalPromptLength + ) + const systemMessage = { role: 'system', - content: getStoreSetting('topicNamingPrompt') || i18n.t('prompts.title') + content: enhancedPrompt } const userMessage = { @@ -701,18 +743,46 @@ export default class OpenAIProvider extends BaseProvider { * Generate text * @param prompt - The prompt * @param content - The content + * @param modelId - Optional model ID to use * @returns The generated text */ - public async generateText({ prompt, content }: { prompt: string; content: string }): Promise { - const model = getDefaultModel() + public async generateText({ + prompt, + content, + modelId + }: { + prompt: string + content: string + modelId?: string + }): Promise { + // 使用指定的模型或默认模型 + const model = modelId + ? store + .getState() + .llm.providers.flatMap((provider) => provider.models) + .find((m) => m.id === modelId) + : getDefaultModel() + + if (!model) { + console.error(`Model ${modelId} not found, using default model`) + return '' + } await this.checkIsCopilot() + // 应用记忆功能到系统提示词 + const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService') + // 使用双重类型断言强制转换类型 + 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) + const response = await this.sdk.chat.completions.create({ model: model.id, stream: false, messages: [ - { role: 'system', content: prompt }, + { role: 'system', content: enhancedPrompt }, { role: 'user', content } ] }) @@ -794,7 +864,7 @@ export default class OpenAIProvider extends BaseProvider { if (this.provider.id === 'github') { // @ts-ignore key is not typed return response.body - .map((model) => ({ + .map((model: any) => ({ id: model.name, description: model.summary, object: 'model', diff --git a/src/renderer/src/providers/AiProvider/index.ts b/src/renderer/src/providers/AiProvider/index.ts index 5a377e3084..ca10ba109d 100644 --- a/src/renderer/src/providers/AiProvider/index.ts +++ b/src/renderer/src/providers/AiProvider/index.ts @@ -88,8 +88,16 @@ export default class AiProvider { return this.sdk.suggestions(messages, assistant) } - public async generateText({ prompt, content }: { prompt: string; content: string }): Promise { - return this.sdk.generateText({ prompt, content }) + public async generateText({ + prompt, + content, + modelId + }: { + prompt: string + content: string + modelId?: string + }): Promise { + return this.sdk.generateText({ prompt, content, modelId }) } public async check(model: Model): Promise<{ valid: boolean; error: Error | null }> { diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index c587bc18fd..5d9280d119 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -355,8 +355,28 @@ export async function fetchSearchSummary({ messages, assistant }: { messages: Me } } -export async function fetchGenerate({ prompt, content }: { prompt: string; content: string }): Promise { - const model = getDefaultModel() +export async function fetchGenerate({ + prompt, + content, + modelId +}: { + prompt: string + content: string + modelId?: string +}): Promise { + // 使用指定的模型或默认模型 + const model = modelId + ? store + .getState() + .llm.providers.flatMap((provider) => provider.models) + .find((m) => m.id === modelId) + : getDefaultModel() + + if (!model) { + console.error(`Model ${modelId} not found, using default model`) + return '' + } + const provider = getProviderByModel(model) if (!hasApiKey(provider)) { @@ -366,8 +386,9 @@ export async function fetchGenerate({ prompt, content }: { prompt: string; conte const AI = new AiProvider(provider) try { - return await AI.generateText({ prompt, content }) + return await AI.generateText({ prompt, content, modelId }) } catch (error: any) { + console.error('Error generating text:', error) return '' } } diff --git a/src/renderer/src/services/ContextualMemoryService.ts b/src/renderer/src/services/ContextualMemoryService.ts new file mode 100644 index 0000000000..18509bdcc4 --- /dev/null +++ b/src/renderer/src/services/ContextualMemoryService.ts @@ -0,0 +1,525 @@ +// src/renderer/src/services/ContextualMemoryService.ts + +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 { + 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) + ]) + + // 合并并排序推荐结果 + const 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) + ]) + + // 合并并排序推荐结果 + const 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) + ]) + + // 合并并排序推荐结果 + const 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/HistoricalContextService.ts b/src/renderer/src/services/HistoricalContextService.ts new file mode 100644 index 0000000000..3df3817e1b --- /dev/null +++ b/src/renderer/src/services/HistoricalContextService.ts @@ -0,0 +1,239 @@ +// src/renderer/src/services/HistoricalContextService.ts +import { TopicManager } from '@renderer/hooks/useTopic' +import { fetchGenerate } from '@renderer/services/ApiService' +import store from '@renderer/store' +import { ShortMemory } from '@renderer/store/memory' +import { Message } from '@renderer/types' + +/** + * 分析当前对话并决定是否需要调用历史对话 + * @param topicId 当前话题ID + * @param recentMessageCount 要分析的最近消息数量 + * @param returnIdOnly 是否只返回话题ID而不获取完整内容(用于调试) + * @returns 如果需要历史上下文,返回历史对话内容;否则返回null + */ +export const analyzeAndSelectHistoricalContext = async ( + topicId: string, + recentMessageCount: number = 8, + returnIdOnly: boolean = false +): Promise<{ content: string; sourceTopicId: string } | null> => { + try { + // 1. 获取设置,检查功能是否启用 + const state = store.getState() + const isEnabled = state.settings?.enableHistoricalContext ?? false + + if (!isEnabled) { + console.log('[HistoricalContext] Feature is disabled') + return null + } + + // 2. 获取最近的消息 + const recentMessages = await getRecentMessages(topicId, recentMessageCount) + if (!recentMessages || recentMessages.length === 0) { + console.log('[HistoricalContext] No recent messages found') + return null + } + + // 3. 获取所有短期记忆(已分析的对话) + const shortMemories = state.memory?.shortMemories || [] + if (shortMemories.length === 0) { + console.log('[HistoricalContext] No short memories available') + return null + } + + // 4. 使用快速模型分析是否需要历史上下文 + const analysisResult = await analyzeNeedForHistoricalContext(recentMessages, shortMemories) + if (!analysisResult.needsHistoricalContext) { + console.log('[HistoricalContext] Analysis indicates no need for historical context') + return null + } + + // 5. 如果需要历史上下文,获取原始对话内容 + if (analysisResult.selectedTopicId) { + // 如果只需要返回ID,则不获取完整内容(用于调试) + if (returnIdOnly) { + return { + content: `话题ID: ${analysisResult.selectedTopicId}\n原因: ${analysisResult.reason || '相关历史对话'}`, + sourceTopicId: analysisResult.selectedTopicId + } + } + + // 正常情况下,获取完整对话内容 + const dialogContent = await getOriginalDialogContent(analysisResult.selectedTopicId) + if (dialogContent) { + return { + content: dialogContent, + sourceTopicId: analysisResult.selectedTopicId + } + } + } + + return null + } catch (error) { + console.error('[HistoricalContext] Error analyzing and selecting historical context:', error) + return null + } +} + +/** + * 获取指定话题的最近消息 + */ +const getRecentMessages = async (topicId: string, count: number): Promise => { + try { + // 先尝试从Redux store获取 + const state = store.getState() + let messages: Message[] = [] + + if (state.messages?.messagesByTopic && state.messages.messagesByTopic[topicId]) { + messages = state.messages.messagesByTopic[topicId] + } else { + // 如果Redux store中没有,从数据库获取 + const topicMessages = await TopicManager.getTopicMessages(topicId) + if (topicMessages && topicMessages.length > 0) { + messages = topicMessages + } + } + + // 返回最近的count条消息 + return messages.slice(-count) + } catch (error) { + console.error('[HistoricalContext] Error getting recent messages:', error) + return [] + } +} + +/** + * 分析是否需要历史上下文 + */ +const analyzeNeedForHistoricalContext = async ( + recentMessages: Message[], + shortMemories: ShortMemory[] +): Promise<{ needsHistoricalContext: boolean; selectedTopicId?: string; reason?: string }> => { + try { + // 准备分析提示词 + const messagesContent = recentMessages + .map((msg) => `${msg.role === 'user' ? '用户' : 'AI'}: ${msg.content}`) + .join('\n') + + const memoriesContent = shortMemories + .map((memory) => `话题ID: ${memory.topicId}\n内容: ${memory.content}`) + .join('\n\n') + + const prompt = ` +你是一个专门分析对话上下文的助手,你的任务是判断当前对话是否需要引用历史对话来提供更完整、更连贯的回答。 + +最近的对话内容: +${messagesContent} + +可用的历史对话摘要: +${memoriesContent} + +请仔细分析用户的问题和可用的历史对话摘要。考虑以下因素: + +1. 用户当前问题是否与历史对话中的任何主题相关 +2. 历史对话中是否包含可能对当前问题有帮助的信息 +3. 引用历史对话是否能使回答更全面、更个性化 +4. 即使用户没有直接提及历史内容,但如果历史对话中有相关信息,也应考虑引用 + +请积极地寻找可能的联系,即使联系不是非常明显的。如果有任何可能相关的历史对话,请倾向于引用它。 + +请回答以下问题: +1. 是否需要引用历史对话来更好地回答用户的问题?(是/否) +2. 如果需要,请指出最相关的历史对话的话题ID。 +3. 详细解释为什么需要引用这个历史对话,以及它如何与当前问题相关。 + +请按以下JSON格式回答,不要添加任何其他文本: +{ + "needsHistoricalContext": true/false, + "selectedTopicId": "话题ID或null", + "reason": "详细解释为什么需要或不需要引用历史对话" +} +` + + // 获取分析模型 + const state = store.getState() + // 优先使用历史对话上下文分析模型,如果没有设置,则使用短期记忆分析模型或长期记忆分析模型 + const analyzeModel = + state.memory?.historicalContextAnalyzeModel || state.memory?.shortMemoryAnalyzeModel || state.memory?.analyzeModel + + if (!analyzeModel) { + console.log('[HistoricalContext] No analyze model set') + return { needsHistoricalContext: false } + } + + // 调用模型进行分析 + console.log('[HistoricalContext] Calling AI model for analysis...') + const result = await fetchGenerate({ + prompt, + content: '', + modelId: analyzeModel + }) + + if (!result) { + console.log('[HistoricalContext] No result from AI analysis') + return { needsHistoricalContext: false } + } + + // 解析结果 + try { + // 尝试直接解析JSON + const parsedResult = JSON.parse(result) + return { + needsHistoricalContext: parsedResult.needsHistoricalContext === true, + selectedTopicId: parsedResult.selectedTopicId || undefined, + reason: parsedResult.reason + } + } catch (parseError) { + // 如果直接解析失败,尝试从文本中提取JSON + const jsonMatch = result.match(/\{[\s\S]*\}/) + if (jsonMatch) { + try { + const extractedJson = JSON.parse(jsonMatch[0]) + return { + needsHistoricalContext: extractedJson.needsHistoricalContext === true, + selectedTopicId: extractedJson.selectedTopicId || undefined, + reason: extractedJson.reason + } + } catch (extractError) { + console.error('[HistoricalContext] Failed to extract JSON from result:', extractError) + } + } + + // 如果都失败了,尝试简单的文本分析 + 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]+)/) + + return { + needsHistoricalContext: needsContext, + selectedTopicId: topicIdMatch ? topicIdMatch[1] : undefined, + reason: reasonMatch ? reasonMatch[1] : undefined + } + } + } catch (error) { + console.error('[HistoricalContext] Error analyzing need for historical context:', error) + return { needsHistoricalContext: false } + } +} + +/** + * 获取原始对话内容 + */ +const getOriginalDialogContent = async (topicId: string): Promise => { + try { + // 获取话题的原始消息 + const messages = await TopicManager.getTopicMessages(topicId) + if (!messages || messages.length === 0) { + console.log(`[HistoricalContext] No messages found for topic ${topicId}`) + return null + } + + // 格式化对话内容 + const dialogContent = messages.map((msg) => `${msg.role === 'user' ? '用户' : 'AI'}: ${msg.content}`).join('\n\n') + + return dialogContent + } catch (error) { + console.error('[HistoricalContext] Error getting original dialog content:', error) + return null + } +} diff --git a/src/renderer/src/services/MemoryDeduplicationService.ts b/src/renderer/src/services/MemoryDeduplicationService.ts new file mode 100644 index 0000000000..f72bd61359 --- /dev/null +++ b/src/renderer/src/services/MemoryDeduplicationService.ts @@ -0,0 +1,341 @@ +// 记忆去重与合并服务 +import { fetchGenerate } from '@renderer/services/ApiService' +import store from '@renderer/store' +import { + addMemory, + addShortMemory, + deleteMemory, + deleteShortMemory, + saveLongTermMemoryData, + saveMemoryData +} from '@renderer/store/memory' + +// 记忆去重与合并的结果接口 +export interface DeduplicationResult { + similarGroups: { + groupId: string + memoryIds: string[] + mergedContent: string + category?: string + importance?: number // 新增重要性评分 + keywords?: string[] // 新增关键词 + }[] + independentMemories: string[] + rawResponse: string +} + +/** + * 分析记忆库中的相似记忆,提供智能合并建议 + * @param listId 可选的列表ID,如果不提供则处理所有列表 + * @param isShortMemory 是否处理短期记忆 + * @param topicId 当处理短期记忆时,可选的话题ID + * @returns 去重分析结果 + */ +export const deduplicateAndMergeMemories = async ( + listId?: string, + isShortMemory: boolean = false, + topicId?: string +): Promise => { + // 获取需要处理的记忆 + const state = store.getState() + + let targetMemories: any[] = [] + + if (isShortMemory) { + // 处理短期记忆 + const shortMemories = state.memory?.shortMemories || [] + targetMemories = topicId ? shortMemories.filter((memory) => memory.topicId === topicId) : shortMemories + } else { + // 处理长期记忆 + const memories = state.memory?.memories || [] + targetMemories = listId ? memories.filter((memory) => memory.listId === listId) : memories + } + + if (targetMemories.length < 2) { + console.log('[Memory Deduplication] Not enough memories to deduplicate') + return null + } + + const memoryType = isShortMemory ? 'short memories' : 'memories' + console.log(`[Memory Deduplication] Starting deduplication for ${targetMemories.length} ${memoryType}`) + + // 构建去重提示词 + const memoriesToCheck = targetMemories + .map((memory, index) => { + if (isShortMemory) { + return `${index + 1}. 短期记忆: ${memory.content}` + } else { + return `${index + 1}. ${memory.category || '其他'}: ${memory.content}` + } + }) + .join('\n') + + const prompt = ` +请仔细分析以下记忆项,识别语义相似或包含重复信息的条目,并提供智能合并建议。 + +相似度判断标准: +1. 语义相似:即使表述不同,但表达相同或非常相似的意思 +2. 内容重叠:一个记忆项包含另一个记忆项的大部分信息 +3. 主题相同:描述同一个主题或事件的不同方面 + +记忆项列表: +${memoriesToCheck} + +例如,以下记忆应被视为相似: +- "用户喜欢简洁的界面设计"和"用户偏好简单直观的UI" +- "用户正在开发一个网站项目"和"用户在进行网站开发工作" +- "用户正在准备完成一个项目"和"用户正在进行一个项目的工作" + +请按以下格式返回结果: +1. 识别出的相似组: + - 组1: [记忆项编号,如"1,5,8"] - 合并建议: "合并后的内容" - 分类: "最合适的分类" + - 组2: [记忆项编号] - 合并建议: "合并后的内容" - 分类: "最合适的分类" + ... + +2. 独立记忆项: [不需要合并的记忆项编号] + +合并建议要求: +- 保留所有非重复的有价值信息 +- 使用简洁清晰的语言 +- 确保合并后的内容比原始记忆更加全面和准确 +- 如果记忆项之间有细微差异,请在合并内容中保留这些差异 + +如果没有发现相似记忆,请返回"未发现相似记忆"。 +` + + try { + // 使用AI模型进行去重分析 + const analyzeModel = state.memory?.analyzeModel + if (!analyzeModel) { + console.log('[Memory Deduplication] No analyze model set') + return null + } + + console.log('[Memory Deduplication] Calling AI model for analysis...') + const result = await fetchGenerate({ + prompt: prompt, + content: memoriesToCheck, + modelId: analyzeModel + }) + + if (!result) { + console.log('[Memory Deduplication] No result from AI analysis') + return null + } + + console.log('[Memory Deduplication] Analysis result:', result) + + // 解析结果 + const similarGroups: DeduplicationResult['similarGroups'] = [] + const independentMemories: string[] = [] + + // 检查是否没有发现相似记忆 + if (result.includes('未发现相似记忆')) { + console.log('[Memory Deduplication] No similar memories found') + return { + similarGroups: [], + independentMemories: targetMemories.map((_, index) => String(index + 1)), + rawResponse: result + } + } + + // 解析相似组 + const similarGroupsMatch = result.match(/1\.\s*识别出的相似组:([\s\S]*?)(?=2\.\s*独立记忆项:|$)/i) + 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 + + let match: RegExpExecArray | null + while ((match = groupRegex.exec(groupsText)) !== null) { + const groupId = match[1] || String(similarGroups.length + 1) + const memoryIndices = match[2].split(',').map((s: string) => s.trim()) + 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 + + similarGroups.push({ + groupId, + memoryIds: memoryIndices, + mergedContent, + category, + importance, + keywords + }) + } + } + + // 解析独立记忆项 + const independentMatch = result.match(/2\.\s*独立记忆项:\s*\[([\d,\s]+)\]/i) + if (independentMatch && independentMatch[1]) { + independentMemories.push(...independentMatch[1].split(',').map((s: string) => s.trim())) + } + + console.log('[Memory Deduplication] Parsed result:', { similarGroups, independentMemories }) + + return { + similarGroups, + independentMemories, + rawResponse: result + } + } catch (error) { + console.error('[Memory Deduplication] Error during deduplication:', error) + return null + } +} + +// 已在顶部导入saveMemoryData和saveLongTermMemoryData + +/** + * 应用去重结果,合并相似记忆 + * @param result 去重分析结果 + * @param autoApply 是否自动应用合并结果 + * @param isShortMemory 是否处理短期记忆 + */ +export const applyDeduplicationResult = async ( + result: DeduplicationResult, + autoApply: boolean = false, + isShortMemory: boolean = false +) => { + if (!result || !result.similarGroups || result.similarGroups.length === 0) { + console.log('[Memory Deduplication] No similar groups to apply') + return + } + + const state = store.getState() + const memories = isShortMemory ? state.memory?.shortMemories || [] : state.memory?.memories || [] + + // 处理每个相似组 + for (const group of result.similarGroups) { + // 获取组中的记忆 + const memoryIndices = group.memoryIds.map((id) => parseInt(id) - 1) + const groupMemories = memoryIndices.map((index) => memories[index]).filter(Boolean) + + if (groupMemories.length < 2) continue + + // 获取第一个记忆的列表ID和其他属性 + const firstMemory = groupMemories[0] + + // 收集所有已分析过的消息ID + const allAnalyzedMessageIds = new Set() + groupMemories.forEach((memory) => { + if (memory.analyzedMessageIds) { + memory.analyzedMessageIds.forEach((id) => allAnalyzedMessageIds.add(id)) + } + }) + + // 找出最新的lastMessageId + let lastMessageId: string | undefined + groupMemories.forEach((memory) => { + if (memory.lastMessageId) { + if (!lastMessageId || new Date(memory.createdAt) > new Date(lastMessageId)) { + lastMessageId = memory.lastMessageId + } + } + }) + + // 找出所有关联的话题ID + const topicIds = new Set() + groupMemories.forEach((memory) => { + if (memory.topicId) { + topicIds.add(memory.topicId) + } + }) + + // 如果自动应用,则添加合并后的记忆并删除原记忆 + if (autoApply) { + if (isShortMemory) { + // 处理短期记忆 + // 添加合并后的短期记忆 + const topicId = topicIds.size === 1 ? Array.from(topicIds)[0] : undefined + if (topicId) { + store.dispatch( + addShortMemory({ + content: group.mergedContent, + topicId: topicId, + analyzedMessageIds: Array.from(allAnalyzedMessageIds), + lastMessageId: lastMessageId, + importance: group.importance, // 添加重要性评分 + keywords: group.keywords // 添加关键词 + }) + ) + + // 删除原短期记忆 + for (const memory of groupMemories) { + store.dispatch(deleteShortMemory(memory.id)) + } + } + } else { + // 处理长期记忆 + // 安全地获取 listId 和 category,因为它们只存在于 Memory 类型 + const listId = 'listId' in firstMemory ? firstMemory.listId : undefined + const memoryCategory = 'category' in firstMemory ? firstMemory.category : undefined + + // 添加合并后的记忆 + store.dispatch( + addMemory({ + content: group.mergedContent, + source: '自动合并', + category: group.category || memoryCategory || '其他', // 使用安全获取的 category + listId: listId, // 使用安全获取的 listId + analyzedMessageIds: Array.from(allAnalyzedMessageIds), + lastMessageId: lastMessageId, + topicId: topicIds.size === 1 ? Array.from(topicIds)[0] : undefined, + importance: group.importance, // 添加重要性评分 + keywords: group.keywords // 添加关键词 + }) + ) + + // 删除原记忆 + for (const memory of groupMemories) { + store.dispatch(deleteMemory(memory.id)) + } + } + + console.log(`[Memory Deduplication] Applied group ${group.groupId}: merged ${groupMemories.length} memories`) + } + } + + // 合并完成后,将更改保存到文件 + if (autoApply) { + try { + // 获取最新的状态 + const currentState = store.getState().memory + + // 保存到文件 + if (isShortMemory) { + // 短期记忆使用saveMemoryData + 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() + console.log('[Memory Deduplication] Long-term memories saved to file after merging') + } + } catch (error) { + console.error('[Memory Deduplication] Failed to save memory data after merging:', error) + } + } +} diff --git a/src/renderer/src/services/MemoryService.ts b/src/renderer/src/services/MemoryService.ts new file mode 100644 index 0000000000..4821931c2f --- /dev/null +++ b/src/renderer/src/services/MemoryService.ts @@ -0,0 +1,1600 @@ +// Import database for topic access +import { TopicManager } from '@renderer/hooks/useTopic' // Import TopicManager +import { fetchGenerate } from '@renderer/services/ApiService' // Import fetchGenerate instead of AiProvider +// Import getProviderByModel +import { useAppDispatch, useAppSelector } from '@renderer/store' +// Removed duplicate import: import store from '@renderer/store'; +import store from '@renderer/store' // Import store +// AiProvider no longer needed as we're using fetchGenerate +import { + accessMemory, + addAnalysisLatency, + addMemory, + addShortMemory, + clearCurrentRecommendations, + Memory, + MemoryRecommendation, + saveLongTermMemoryData, + saveMemoryData, + setAnalyzing, + setRecommending, + updateAnalysisStats, + updateCurrentRecommendations, + updateMemoryPriorities, + updatePerformanceMetrics, + updateUserInterest +} from '@renderer/store/memory' +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' => { + 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(/\u4ed4\u7ec6\u5206\u6790/g, '\u7b80\u8981\u5206\u6790') + .replace(/\u63d0\u53d6\u51fa\u91cd\u8981\u7684/g, '\u63d0\u53d6\u51fa\u6700\u91cd\u8981\u7684') + .replace(/## \u5206\u6790\u8981\u6c42\uff1a([\s\S]*?)## \u8f93\u51fa\u683c\u5f0f\uff1a/g, '## \u8f93\u51fa\u683c\u5f0f\uff1a') + case 'high': + // 增强提示词,要求更深入的分析 + return basePrompt.replace(/## \u5206\u6790\u8981\u6c42\uff1a([\s\S]*?)## \u8f93\u51fa\u683c\u5f0f\uff1a/g, + `## \u5206\u6790\u8981\u6c42\uff1a +1. \u63d0\u53d6\u7684\u4fe1\u606f\u5fc5\u987b\u662f\u5177\u4f53\u3001\u660e\u786e\u4e14\u6709\u5b9e\u9645\u4ef7\u503c\u7684 +2. \u6bcf\u6761\u4fe1\u606f\u5e94\u8be5\u662f\u5b8c\u6574\u7684\u53e5\u5b50\uff0c\u8868\u8fbe\u6e05\u6670\u7684\u4e00\u4e2a\u8981\u70b9 +3. \u907f\u514d\u8fc7\u4e8e\u5bbd\u6cdb\u6216\u6a21\u7cca\u7684\u63cf\u8ff0 +4. \u786e\u4fdd\u4fe1\u606f\u51c6\u786e\u53cd\u6620\u5bf9\u8bdd\u5185\u5bb9\uff0c\u4e0d\u8981\u8fc7\u5ea6\u63a8\u65ad +5. \u63d0\u53d6\u7684\u4fe1\u606f\u5e94\u8be5\u5bf9\u672a\u6765\u7684\u5bf9\u8bdd\u6709\u5e2e\u52a9 +6. \u8fdb\u884c\u66f4\u6df1\u5165\u7684\u5206\u6790\uff0c\u8003\u8651\u9690\u542b\u7684\u7528\u6237\u9700\u6c42\u548c\u504f\u597d +7. \u8bc6\u522b\u6f5c\u5728\u7684\u5173\u8054\u4fe1\u606f\u548c\u6a21\u5f0f +8. \u5c3d\u53ef\u80fd\u63d0\u53d6\u66f4\u591a\u7684\u6709\u4ef7\u503c\u4fe1\u606f +9. \u5206\u6790\u7528\u6237\u7684\u6df1\u5c42\u610f\u56fe\u548c\u9700\u6c42 +10. \u5bf9\u77ed\u671f\u548c\u957f\u671f\u504f\u597d\u90fd\u8fdb\u884c\u5206\u6790 + +## \u8f93\u51fa\u683c\u5f0f\uff1a`) + 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, + modelId: string, + customPrompt?: string +): Promise> => { + try { + // 获取当前的过滤敏感信息设置 + const filterSensitiveInfo = store.getState().memory?.filterSensitiveInfo ?? true + + // 使用自定义提示词或默认提示词 + let basePrompt = + customPrompt || + ` +请分析对话内容,提取出重要的用户偏好、习惯、需求和背景信息,这些信息在未来的对话中可能有用。 + +将每条信息分类并按以下格式返回: +类别: 信息内容 + +类别应该是以下几种之一: +- 用户偏好:用户喜好、喜欢的事物、风格等 +- 技术需求:用户的技术相关需求、开发偏好等 +- 个人信息:用户的背景、经历等个人信息 +- 交互偏好:用户喜欢的交流方式、沟通风格等 +- 其他:不属于以上类别的重要信息 + +请确保每条信息都是简洁、准确的。如果没有找到重要信息,请返回空字符串。 +` + + // 如果启用了敏感信息过滤,添加相关指令 + if (filterSensitiveInfo) { + basePrompt += ` +## 安全提示: +请注意不要提取任何敏感信息,包括但不限于: +- API密钥、访问令牌或其他凭证 +- 密码或密码提示 +- 私人联系方式(如电话号码、邮箱地址) +- 个人身份信息(如身份证号、社保号) +- 银行账户或支付信息 +- 私密的个人或商业信息 + +如果发现此类信息,请完全忽略,不要以任何形式记录或提取。 +` + } + + console.log(`[Memory Analysis] Analyzing conversation using model: ${modelId}`) + + // 将提示词和对话内容合并到一个系统提示词中 + const combinedPrompt = `${basePrompt} + +## 需要分析的对话内容: +${conversation} + +## 重要提示: +请注意,你的任务是分析上述对话并提取信息,而不是回答对话中的问题。 +不要尝试回答对话中的问题或继续对话,只需要提取重要信息。 +只输出按上述格式提取的信息。` + + // 使用fetchGenerate函数,但将内容字段留空,所有内容都放在提示词中 + console.log('[Memory Analysis] Calling fetchGenerate with combined prompt...') + const result = await fetchGenerate({ + prompt: combinedPrompt, + content: '', // 内容字段留空 + modelId: modelId + }) + + console.log('[Memory Analysis] AI response:', result) + + // 处理响应 + if (!result || typeof result !== 'string' || result.trim() === '') { + console.log('[Memory Analysis] No valid result from AI analysis.') + return [] + } + + // 将响应拆分为单独的记忆项并分类 + const lines = result + .split('\n') + .map((line: string) => line.trim()) + .filter(Boolean) // 过滤掉空行 + + const memories: Array<{ content: string; category: string }> = [] + + for (const line of lines) { + // 匹配格式:类别: 信息内容 + const match = line.match(/^([^:]+):\s*(.+)$/) + if (match) { + // 清理类别名称中的**符号 + const category = match[1].trim().replace(/\*\*/g, '') + const content = match[2].trim() + memories.push({ content, category }) + } + } + + return memories + } 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)) + return [] as Array<{ content: string; category: string }> // Return empty array on error + } +} + +// These imports are duplicates, removing them. +// Removed duplicate import: import store from '@renderer/store'; + +// 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() + // 获取设置状态 + 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 参数,允许分析指定的话题 + const analyzeAndAddMemories = useCallback( + async (topicId?: string) => { + // 如果没有提供话题ID,则使用当前话题 + // 在函数执行时获取最新状态 + const currentState = store.getState() // Use imported store + const memoryState = currentState.memory || {} + const messagesState = currentState.messages || {} + + // 检查isAnalyzing状态是否卡住(超过1分钟) + if (memoryState.isAnalyzing && memoryState.lastAnalyzeTime) { + const now = Date.now() + const analyzeTime = memoryState.lastAnalyzeTime + if (now - analyzeTime > 1 * 60 * 1000) { + // 1分钟超时 + console.log('[Memory Analysis] Analysis state stuck, resetting...') + dispatch(setAnalyzing(false)) + } + } + + // 重新检查条件 + if (!memoryState.isActive || !memoryState.autoAnalyze || !memoryState.analyzeModel || memoryState.isAnalyzing) { + console.log('[Memory Analysis] Conditions not met or already analyzing at time of call:', { + isActive: memoryState.isActive, + autoAnalyze: memoryState.autoAnalyze, + analyzeModel: memoryState.analyzeModel, + isAnalyzing: memoryState.isAnalyzing + }) + return + } + + // 获取对话内容 + let messages: any[] = [] + const targetTopicId = topicId || messagesState.currentTopic?.id + + if (targetTopicId) { + // 如果提供了话题ID,先尝试从 Redux store 中获取 + if (messagesState.messagesByTopic && messagesState.messagesByTopic[targetTopicId]) { + messages = messagesState.messagesByTopic[targetTopicId] || [] + } else { + // 如果 Redux store 中没有,则从数据库中获取 + try { + const topicMessages = await TopicManager.getTopicMessages(targetTopicId) + if (topicMessages && topicMessages.length > 0) { + messages = topicMessages + } + } catch (error) { + console.error(`[Memory Analysis] Failed to get messages for topic ${targetTopicId}:`, error) + } + } + } + + if (!messages || messages.length === 0) { + console.log('[Memory Analysis] No messages to analyze.') + return + } + + // 获取现有的长期记忆 + const existingMemories = store.getState().memory?.memories || [] + const topicMemories = existingMemories.filter((memory) => memory.topicId === targetTopicId) + + // 收集所有已分析过的消息ID + const analyzedMessageIds = new Set() + topicMemories.forEach((memory) => { + if (memory.analyzedMessageIds) { + memory.analyzedMessageIds.forEach((id) => analyzedMessageIds.add(id)) + } + }) + + // 找出未分析过的新消息 + const newMessages = messages.filter((msg) => !analyzedMessageIds.has(msg.id)) + + if (newMessages.length === 0) { + console.log('[Memory Analysis] No new messages to analyze.') + return + } + + console.log(`[Memory Analysis] Found ${newMessages.length} new messages to analyze.`) + + // 构建新消息的对话内容 + const newConversation = newMessages.map((msg) => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n') + + // 获取已有的长期记忆内容 + const existingMemoriesContent = topicMemories + .map((memory) => `${memory.category || '其他'}: ${memory.content}`) + .join('\n') + + if (!newConversation) { + console.log('[Memory Analysis] No conversation content to analyze.') + return + } + + 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 basePrompt = ` +你是一个专业的对话分析专家,负责从对话中提取关键信息,形成精准的长期记忆。 + +## 重要提示: +请注意,你的任务是分析对话并提取信息,而不是回答对话中的问题。不要尝试回答对话中的问题或继续对话,只需要提取重要信息。 + +## 分析要求: +请仔细分析对话内容,提取出重要的用户偏好、习惯、需求和背景信息,这些信息在未来的对话中可能有用。 + +1. 提取的信息必须是具体、明确且有实际价值的 +2. 每条信息应该是完整的句子,表达清晰的一个要点 +3. 避免过于宽泛或模糊的描述 +4. 确保信息准确反映对话内容,不要过度推断 +5. 提取的信息应该对未来的对话有帮助 + +## 输出格式: +将每条信息分类并严格按以下格式返回: +类别: 信息内容 + +例如: +用户偏好: 用户喜欢简洁直接的代码修改方式。 +技术需求: 用户需要修复长期记忆分析功能中的问题。 + +## 信息类别: +- 用户偏好:用户喜好、喜欢的事物、风格、审美倾向等 +- 技术需求:用户的技术相关需求、开发偏好、编程习惯等 +- 个人信息:用户的背景、经历、身份等个人信息 +- 交互偏好:用户喜欢的交流方式、沟通风格、反馈方式等 +- 其他:不属于以上类别的重要信息 + +## 需要分析的对话内容: +${newConversation} + +## 注意事项: +- 不要回答对话中的问题 +- 不要继续对话或生成新的对话 +- 只输出按上述格式提取的信息 +- 如果没有找到重要信息,请返回空字符串 + +${ + existingMemoriesContent + ? `## 已提取的信息: +${existingMemoriesContent} + +请分析新的对话内容,提取出新的重要信息,避免重复已有信息。只关注新增的、有价值的信息。如果发现与已有信息相矛盾的内容,请提取最新的信息并标注这是更新。` + : '请确保每条信息都是简洁、准确且有价值的。如果没有找到重要信息,请返回空字符串。' +}` + + // 根据分析深度调整提示词 + // 注意:现在我们直接使用basePrompt,不再调整提示词 + + // 调用分析函数,传递自定义提示词和对话内容 + // 将对话内容直接放在提示词中,不再单独传递 + const memories = await analyzeConversation('', memoryState.analyzeModel!, basePrompt) + + // 用户关注点学习 + 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 || [] + + // 首先进行简单的字符串匹配去重 + const newMemories = memories.filter((memory) => { + return !existingMemories.some((m) => m.content === memory.content) + }) + + // 记录是否添加了新记忆 + let addedNewMemories = false + + console.log(`[Memory Analysis] Found ${memories.length} memories, ${newMemories.length} are new`) + + // 添加新记忆 + for (const memory of newMemories) { + // 获取当前选中的列表ID + const currentListId = store.getState().memory?.currentListId || store.getState().memory?.memoryLists[0]?.id + + // 收集新分析的消息ID + const newMessageIds = messages.map((msg) => msg.id) + + // 获取最后一条消息的ID,用于跟踪分析进度 + const lastMessageId = messages[messages.length - 1]?.id + + dispatch( + addMemory({ + content: memory.content, + source: '自动分析', + category: memory.category, + listId: currentListId, + analyzedMessageIds: newMessageIds, + lastMessageId: lastMessageId, + topicId: targetTopicId + }) + ) + console.log( + `[Memory Analysis] Added new memory: "${memory.content}" (${memory.category}) to list ${currentListId}` + ) + addedNewMemories = true + } + + console.log(`[Memory Analysis] Processed ${memories.length} potential memories, added ${newMemories.length}.`) + + // 如果添加了新记忆,将其保存到长期记忆文件 + 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() + 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) + } + } + + // 自适应分析:根据分析结果调整分析频率 + 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) + } finally { + dispatch(setAnalyzing(false)) + console.log('[Memory Analysis] Analysis finished.') + } + // 依赖项只需要 dispatch,因为其他所有状态都在函数内部重新获取 + }, + [dispatch] + ) + + // Ref 来存储最新的 analyzeAndAddMemories 函数 + const analyzeAndAddMemoriesRef = useRef(analyzeAndAddMemories) + + // Effect 来保持 ref 是最新的 + useEffect(() => { + 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:', { + isActive, + autoAnalyze, + analyzeModel + }) + return // 清理函数不需要显式返回 undefined + } + + console.log('[Memory Analysis Timer] Setting up interval timer (1 minute)...') // 更新日志说明时间 + // 设置 1 分钟间隔用于测试 + const intervalId = setInterval( + () => { + console.log('[Memory Analysis Timer] Interval triggered. Calling analyze function from ref...') + // 定时器触发时不指定话题ID,使用当前活动话题 + analyzeAndAddMemoriesRef.current() // 调用 ref 中的函数 + }, + 1 * 60 * 1000 + ) // 1 分钟 + + // 清理函数 + return () => { + console.log('[Memory Analysis Timer] Clearing interval timer...') + clearInterval(intervalId) + } + // 依赖项只包含决定是否启动定时器的设置 + }, [isActive, autoAnalyze, analyzeModel]) + + // 获取上下文感知记忆推荐 + 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 + } +} + +// 手动添加短记忆 +export const addShortMemoryItem = async ( + content: string, + topicId: string, + analyzedMessageIds?: string[], + lastMessageId?: string +) => { + // Use imported store directly + store.dispatch( + addShortMemory({ + content, + topicId, + analyzedMessageIds, + lastMessageId + }) + ) + + // 保存到文件,并强制覆盖 + try { + const state = store.getState().memory + 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) + } +} + +// 手动添加长期记忆 +export const addMemoryItem = async ( + content: string, + category?: string, + source?: string, + listId?: string, + topicId?: string +) => { + // Use imported store directly + store.dispatch( + addMemory({ + content, + category, + source: source || '手动添加', + listId, + topicId + }) + ) + + // 保存到长期记忆文件 + try { + const state = store.getState().memory + 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) + } +} + +/** + * 重置指定话题的长期记忆分析标记 + * @param topicId 要重置的话题ID + * @returns 是否重置成功 + */ +export const resetLongTermMemoryAnalyzedMessageIds = async (topicId: string): Promise => { + if (!topicId) { + console.log('[Memory Reset] No topic ID provided') + return false + } + + try { + // 获取当前记忆状态 + const state = store.getState().memory + + // 找到指定话题的所有长期记忆 + const memories = state.memories || [] + const topicMemories = memories.filter((memory) => memory.topicId === topicId) + + if (topicMemories.length === 0) { + console.log(`[Memory Reset] No long-term memories found for topic ${topicId}`) + return false + } + + console.log(`[Memory Reset] Found ${topicMemories.length} long-term memories for topic ${topicId}`) + + // 重置每个记忆的已分析消息ID + let hasChanges = false + + // 创建更新后的记忆数组 + const updatedMemories = state.memories.map((memory) => { + // 只更新指定话题的记忆 + if (memory.topicId === topicId && memory.analyzedMessageIds && memory.analyzedMessageIds.length > 0) { + hasChanges = true + // 创建新对象,而不是修改原对象 + return { + ...memory, + analyzedMessageIds: [] + } + } + return memory + }) + + if (!hasChanges) { + console.log(`[Memory Reset] No analyzed message IDs to reset for topic ${topicId}`) + return false + } + + // 更新Redux状态中的memories数组 + store.dispatch({ + type: 'memory/setMemories', + payload: updatedMemories + }) + + // 保存更改到文件 + await store + .dispatch( + saveMemoryData({ + memories: updatedMemories + }) + ) + .unwrap() + + // 尝试获取话题的消息,以确保分析时能找到消息 + try { + // 获取当前话题的消息 + const messagesState = store.getState().messages || {} + let messages: any[] = [] + + // 先尝试从 Redux store 中获取 + if (messagesState.messagesByTopic && messagesState.messagesByTopic[topicId]) { + messages = messagesState.messagesByTopic[topicId] || [] + } else { + // 如果 Redux store 中没有,则从数据库中获取 + try { + const topicMessages = await TopicManager.getTopicMessages(topicId) + if (topicMessages && topicMessages.length > 0) { + messages = topicMessages + } + } catch (error) { + console.error(`[Memory Reset] Failed to get messages for topic ${topicId}:`, error) + } + } + + console.log(`[Memory Reset] Found ${messages.length} messages for topic ${topicId}`) + + if (messages.length === 0) { + console.log(`[Memory Reset] Warning: No messages found for topic ${topicId}, analysis may not work`) + } + } catch (error) { + console.error(`[Memory Reset] Error checking messages for topic ${topicId}:`, error) + } + + console.log(`[Memory Reset] Successfully reset analyzed message IDs for topic ${topicId}`) + return true + } catch (error) { + console.error('[Memory Reset] Failed to reset analyzed message IDs:', error) + return false + } +} + +// 分析对话内容并提取重要信息添加到短期记忆 +// 分析对话内容并提取重要信息添加到短期记忆 +export const analyzeAndAddShortMemories = async (topicId: string) => { + if (!topicId) { + console.log('[Short Memory Analysis] No topic ID provided') + return false + } + + // 获取当前记忆状态 + const memoryState = store.getState().memory || {} + const messagesState = store.getState().messages || {} + const shortMemoryAnalyzeModel = memoryState.shortMemoryAnalyzeModel + + if (!shortMemoryAnalyzeModel) { + console.log('[Short Memory Analysis] No short memory analyze model set') + return false + } + + // 获取对话内容 + let messages: any[] = [] + + // 从 Redux store 中获取话题消息 + if (messagesState.messagesByTopic && messagesState.messagesByTopic[topicId]) { + messages = messagesState.messagesByTopic[topicId] || [] + } else { + // 如果 Redux store 中没有,则从数据库中获取 + try { + const topicMessages = await TopicManager.getTopicMessages(topicId) + if (topicMessages && topicMessages.length > 0) { + messages = topicMessages + } + } catch (error) { + console.error(`[Short Memory Analysis] Failed to get messages for topic ${topicId}:`, error) + return false + } + } + + if (!messages || messages.length === 0) { + console.log('[Short Memory Analysis] No messages to analyze.') + return false + } + + // 获取现有的短期记忆 + const existingShortMemories = store.getState().memory?.shortMemories || [] + const topicShortMemories = existingShortMemories.filter((memory) => memory.topicId === topicId) + + // 收集所有已分析过的消息ID + const analyzedMessageIds = new Set() + topicShortMemories.forEach((memory) => { + if (memory.analyzedMessageIds) { + memory.analyzedMessageIds.forEach((id) => analyzedMessageIds.add(id)) + } + }) + + // 找出未分析过的新消息 + const newMessages = messages.filter((msg) => !analyzedMessageIds.has(msg.id)) + + if (newMessages.length === 0) { + console.log('[Short Memory Analysis] No new messages to analyze.') + return false + } + + console.log(`[Short Memory Analysis] Found ${newMessages.length} new messages to analyze.`) + + // 构建新消息的对话内容 + const newConversation = newMessages.map((msg) => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n') + + // 获取已有的短期记忆内容 + const existingMemoriesContent = topicShortMemories.map((memory) => memory.content).join('\n') + + try { + console.log('[Short Memory Analysis] Starting analysis...') + 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 + + // 构建短期记忆分析提示词,包含已有记忆和新对话 + let prompt = ` +请对以下对话内容进行非常详细的分析和总结,提取对当前对话至关重要的上下文信息。请注意,这个分析将用于生成短期记忆,帮助AI理解当前对话的完整上下文。 + +分析要求: +1. 非常详细地总结用户的每一句话中表达的关键信息、需求和意图 +2. 全面分析AI回复中的重要内容和对用户问题的解决方案 +3. 详细记录对话中的重要事实、数据、代码示例和具体细节 +4. 清晰捕捉对话的逻辑发展、转折点和关键决策 +5. 提取对理解当前对话上下文必不可少的信息 +6. 记录用户提出的具体问题和关注点 +7. 捕捉用户在对话中表达的偏好、困惑和反馈 +8. 记录对话中提到的文件、路径、变量名等具体技术细节` + + // 如果启用了敏感信息过滤,添加相关指令 + if (filterSensitiveInfo) { + prompt += ` +9. 请注意不要提取任何敏感信息,包括但不限于: + - API密钥、访问令牌或其他凭证 + - 密码或密码提示 + - 私人联系方式(如电话号码、邮箱地址) + - 个人身份信息(如身份证号、社保号) + - 银行账户或支付信息 + - 私密的个人或商业信息 + 如果发现此类信息,请完全忽略,不要以任何形式记录或提取。` + } + + prompt += ` + +与长期记忆不同,短期记忆应该非常详细地关注当前对话的具体细节和上下文。每条短期记忆应该是对对话片段的精准总结,确保不遗漏任何重要信息。 + +请注意,对于长对话(超过5万字),您应该生成至少15-20条详细的记忆条目,确保完整捕捉对话的所有重要方面。对于超长对话(超过8万字),应生成至少20-30条记忆条目。 + +${ + existingMemoriesContent + ? `以下是已经提取的重要信息: +${existingMemoriesContent} + +请分析新的对话内容,提取出新的重要信息,避免重复已有信息。确保新提取的信息与已有信息形成连贯的上下文理解。对于新的对话内容,请提供非常详细的分析。` + : '请对对话进行非常全面和详细的分析,确保不遗漏任何重要细节。每条总结应该是完整的句子,清晰表达一个重要的上下文信息。请确保总结足够详细,以便在没有原始对话的情况下也能理解完整的上下文。' +} + +输出格式: +请严格按照以下格式输出每条记忆,每条记忆必须单独成行,并以短横线开头: + +- 记忆条目1 +- 记忆条目2 +- 记忆条目3 +... + +要求: +1. 每条记忆必须以短横线开头(“- ”),不要使用数字编号 +2. 每条记忆必须是一个完整的句子,包含充分的上下文信息 +3. 确保记忆内容精准、具体且与当前对话直接相关 +4. 按重要性排序,最重要的信息放在前面 +5. 对于复杂的对话,必须提供至少15-20条记忆条目 +6. 对于超长对话(超过8万字),必须提供至少20-30条记忆条目 +7. 对于技术内容,请包含具体的文件名、路径、变量名、函数名等技术细节 +8. 对于代码相关的对话,请记录关键的代码片段和实现细节 + +注意:不要在输出中包含任何解释或其他格式的文本,只输出以短横线开头的记忆条目。如果对话内容简单,可以少于15条,但必须确保完整捕捉所有重要信息 + +请记住,您的分析应该非常详细,不要过于简化或概括。对于8万字的对话,100字的总结是远远不够的,应该提供至少500-1000字的详细总结,分成多个条目。 + +如果没有找到新的重要信息,请返回空字符串。 + +新的对话内容: +${newConversation} +` + + // 获取模型 + const model = store + .getState() + .llm.providers.flatMap((provider) => provider.models) + .find((model) => model.id === shortMemoryAnalyzeModel) + + if (!model) { + console.error(`[Short Memory Analysis] Model ${shortMemoryAnalyzeModel} not found`) + return false + } + + // 调用AI生成文本 + console.log('[Short Memory Analysis] Calling AI.generateText...') + const result = await fetchGenerate({ + prompt: prompt, + content: newConversation, + modelId: shortMemoryAnalyzeModel + }) + console.log('[Short Memory Analysis] AI.generateText response:', result) + + if (!result || typeof result !== 'string' || result.trim() === '') { + console.log('[Short Memory Analysis] No valid result from AI analysis.') + return false + } + + // 改进的记忆提取逻辑 + let extractedLines: string[] = [] + + // 首先尝试匹配带有数字或短横线的列表项 + 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()) { + 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 = 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 ${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) + + // 获取最后一条消息的ID,用于跟踪分析进度 + const lastMessageId = messages[messages.length - 1]?.id + + // 添加新的短期记忆 + const addedMemories: string[] = [] // Explicitly type addedMemories + for (const content of newMemories) { + 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) + } + } + + // 显式触发保存操作,确保数据被持久化,并强制覆盖 + try { + const state = store.getState().memory + 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) + // 即使保存失败,我们仍然返回true,因为记忆已经添加到Redux状态中 + } + + return addedMemories.length > 0 + } catch (error) { + console.error('[Short Memory Analysis] Failed to analyze and add short memories:', error) + return false + } +} + +// 将记忆应用到系统提示词 +import { persistor } from '@renderer/store' // Import persistor + +export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: string): Promise => { + // 检查持久化状态是否已加载完成 + if (!persistor.getState().bootstrapped) { + console.warn('[Memory] Persistor not bootstrapped yet. Skipping applying memories.') + return systemPrompt + } + + const state = store.getState() // Use imported store + // 确保 state.memory 存在,如果不存在则提供默认值 + const { + isActive, + memories, + memoryLists, + shortMemoryActive, + shortMemories, + priorityManagementEnabled, + contextualRecommendationEnabled, + currentRecommendations + } = state.memory || { + isActive: false, + memories: [], + memoryLists: [], + shortMemoryActive: false, + shortMemories: [], + priorityManagementEnabled: false, + contextualRecommendationEnabled: false, + currentRecommendations: [] + } + + // 获取当前话题ID + const currentTopicId = state.messages.currentTopic?.id + + console.log('[Memory] Applying memories to prompt:', { + isActive, + memoriesCount: memories?.length, + listsCount: memoryLists?.length, + shortMemoryActive, + shortMemoriesCount: shortMemories?.length, + currentTopicId, + priorityManagementEnabled + }) + + 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) { + // 构建推荐记忆提示词 + // 按重要性排序 + 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 importanceA = memoryA?.importance || 0.5 + const importanceB = memoryB?.importance || 0.5 + return importanceB - importanceA + }) + + // 构建更自然的提示词 + const recommendedMemoryPrompt = `在与用户交流时,请考虑以下关于用户的重要信息:\n\n${recommendedMemories + .map((memory) => `- ${memory.content}`) + .join('\n')}` + + console.log('[Memory] Contextual memory recommendations:', recommendedMemoryPrompt) + + // 添加推荐记忆到提示词 + result = `${result}\n\n${recommendedMemoryPrompt}` + hasContent = true + } + } + + // 处理短记忆 + if (shortMemoryActive && shortMemories && shortMemories.length > 0 && 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) { + // 按重要性排序 + topicShortMemories.sort((a, b) => { + const importanceA = a.importance || 0.5 + const importanceB = b.importance || 0.5 + return importanceB - importanceA + }) + + // 构建更自然的短期记忆提示词 + const shortMemoryPrompt = `关于当前对话,请记住以下重要信息:\n\n${topicShortMemories.map((memory) => `- ${memory.content}`).join('\n')}` + console.log('[Memory] Short memory prompt:', shortMemoryPrompt) + + // 添加短记忆到提示词 + result = `${result}\n\n${shortMemoryPrompt}` + hasContent = true + } + } + + // 处理长记忆 + if (isActive && memories && memories.length > 0 && memoryLists && memoryLists.length > 0) { + // 获取所有激活的记忆列表 + const activeListIds = memoryLists.filter((list) => list.isActive).map((list) => list.id) + + if (activeListIds.length > 0) { + // 只获取激活列表中的记忆 + 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) { + // 按重要性对所有记忆进行排序 + activeMemories.sort((a, b) => { + const importanceA = a.importance || 0.5 + const importanceB = b.importance || 0.5 + return importanceB - importanceA + }) + + // 按列表分组构建记忆提示词 + let memoryPrompt = '' + + // 构建更自然的开头 + memoryPrompt = `请考虑以下关于用户的重要背景信息:\n\n` + + // 如果只有一个激活列表,直接列出记忆 + if (activeListIds.length === 1) { + memoryPrompt += activeMemories.map((memory) => `- ${memory.content}`).join('\n') + } else { + // 如果有多个激活列表,按列表分组 + for (const listId of activeListIds) { + const list = memoryLists.find((l) => l.id === listId) + if (list) { + const listMemories = activeMemories.filter((m) => m.listId === listId) + if (listMemories.length > 0) { + memoryPrompt += `\n${list.name}:\n` + memoryPrompt += listMemories.map((memory) => `- ${memory.content}`).join('\n') + memoryPrompt += '\n' + } + } + } + } + + console.log('[Memory] Long-term memory prompt:', memoryPrompt) + + // 添加到系统提示词 + result = `${result}\n\n${memoryPrompt}` + hasContent = true + } + } + } + + if (hasContent) { + console.log('[Memory] Final prompt with memories applied') + } else { + console.log('[Memory] No memories to apply') + } + + // 添加历史对话上下文 + if (topicId) { + try { + const { analyzeAndSelectHistoricalContext } = await import('./HistoricalContextService') + const historicalContext = await analyzeAndSelectHistoricalContext(topicId) + + if (historicalContext) { + console.log('[Memory] Adding historical context from topic:', historicalContext.sourceTopicId) + result = `${result}\n\n以下是之前的相关对话,可能对回答当前问题有帮助:\n\n${historicalContext.content}` + } + } catch (error) { + console.error('[Memory] Error adding historical context:', error) + } + } + + return result +} 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/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 2a441d291e..7c9029a7d0 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -10,6 +10,7 @@ import copilot from './copilot' import knowledge from './knowledge' import llm from './llm' import mcp from './mcp' +import memory from './memory' // Removed import of memoryPersistenceMiddleware import messagesReducer from './messages' import migrate from './migrate' import minapps from './minapps' @@ -35,6 +36,7 @@ const rootReducer = combineReducers({ websearch, mcp, copilot, + memory, messages: messagesReducer }) @@ -43,7 +45,7 @@ const persistedReducer = persistReducer( key: 'cherry-studio', storage, version: 96, - blacklist: ['runtime', 'messages'], + blacklist: ['runtime', 'messages', 'memory'], migrate }, rootReducer @@ -57,7 +59,7 @@ const store = configureStore({ serializableCheck: { ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER] } - }) + }) // Removed concat of memoryPersistenceMiddleware }, devTools: true }) diff --git a/src/renderer/src/store/mcp.ts b/src/renderer/src/store/mcp.ts index 1d075e70e2..801ab1ab12 100644 --- a/src/renderer/src/store/mcp.ts +++ b/src/renderer/src/store/mcp.ts @@ -97,6 +97,14 @@ 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/store/memory.ts b/src/renderer/src/store/memory.ts new file mode 100644 index 0000000000..6abd9e5c6f --- /dev/null +++ b/src/renderer/src/store/memory.ts @@ -0,0 +1,1218 @@ +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit' +import store, { RootState } from '@renderer/store' +import { nanoid } from 'nanoid' + +// 记忆列表接口 +export interface MemoryList { + id: string + name: string + description?: string + createdAt: string + updatedAt: string + isActive: boolean // 是否在对话中使用该记忆列表 +} + +// 记忆项接口 +export interface Memory { + id: string + content: string + createdAt: string + source?: string // 来源,例如"自动分析"或"手动添加" + category?: string // 分类,例如"用户偏好"、"技术需求"等 + listId: string // 所属的记忆列表ID + 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),基于创建时间和最后访问时间 +} + +// 短记忆项接口 +export interface ShortMemory { + id: string + content: string + createdAt: string + 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 MemoryRecommendation { + memoryId: string + relevanceScore: number + source: 'long-term' | 'short-term' + matchReason?: string +} + +export interface MemoryState { + memoryLists: MemoryList[] // 记忆列表 + memories: Memory[] // 所有记忆项 + shortMemories: ShortMemory[] // 短记忆项 + currentListId: string | null // 当前选中的记忆列表ID + isActive: boolean // 记忆功能是否激活 + shortMemoryActive: boolean // 短记忆功能是否激活 + autoAnalyze: boolean // 是否自动分析 + filterSensitiveInfo: boolean // 是否过滤敏感信息 + analyzeModel: string | null // 用于长期记忆分析的模型ID + shortMemoryAnalyzeModel: string | null // 用于短期记忆分析的模型ID + historicalContextAnalyzeModel: 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 // 上次优先级更新时间 + + // 上下文感知记忆推荐相关 + contextualRecommendationEnabled: boolean // 是否启用上下文感知记忆推荐 + autoRecommendMemories: boolean // 是否自动推荐记忆 + recommendationThreshold: number // 推荐阈值(0-1) + currentRecommendations: MemoryRecommendation[] // 当前的记忆推荐 + isRecommending: boolean // 是否正在推荐记忆 + lastRecommendTime: number | null // 上次推荐时间 +} + +// 创建默认记忆列表 +const defaultList: MemoryList = { + id: nanoid(), + name: '默认记忆', + description: '系统默认的记忆列表', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + isActive: true +} + +const initialState: MemoryState = { + memoryLists: [defaultList], + memories: [], + shortMemories: [], // 初始化空的短记忆数组 + currentListId: defaultList.id, + isActive: true, + shortMemoryActive: true, // 默认启用短记忆功能 + autoAnalyze: true, + filterSensitiveInfo: true, // 默认启用敏感信息过滤 + analyzeModel: 'gpt-3.5-turbo', // 设置默认长期记忆分析模型 + shortMemoryAnalyzeModel: 'gpt-3.5-turbo', // 设置默认短期记忆分析模型 + historicalContextAnalyzeModel: 'gpt-3.5-turbo', // 设置默认历史对话上下文分析模型 + vectorizeModel: 'gpt-3.5-turbo', // 设置默认向量化模型 + lastAnalyzeTime: null, + 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(), // 初始化为当前时间 + + // 上下文感知记忆推荐相关 + contextualRecommendationEnabled: true, // 默认启用上下文感知记忆推荐 + autoRecommendMemories: true, // 默认自动推荐记忆 + recommendationThreshold: 0.7, // 默认推荐阈值 + currentRecommendations: [], // 初始化空的推荐列表 + isRecommending: false, // 初始化为非推荐状态 + lastRecommendTime: null // 初始化为空 +} + +const memorySlice = createSlice({ + name: 'memory', + initialState, + reducers: { + // 添加新记忆 + addMemory: ( + state, + action: PayloadAction<{ + content: string + source?: string + category?: string + listId?: string + analyzedMessageIds?: string[] + lastMessageId?: string + topicId?: string + importance?: number // 新增重要性评分 + keywords?: string[] // 新增关键词 + }> + ) => { + // 确保 memoryLists 存在 + if (!state.memoryLists) { + state.memoryLists = [defaultList] + } + + // 使用指定的列表ID或当前选中的列表ID + const listId = + action.payload.listId || + state.currentListId || + (state.memoryLists.length > 0 ? state.memoryLists[0].id : defaultList.id) + + const newMemory: Memory = { + id: nanoid(), + content: action.payload.content, + createdAt: new Date().toISOString(), + source: action.payload.source || '手动添加', + category: action.payload.category, + listId: listId, + analyzedMessageIds: action.payload.analyzedMessageIds, + lastMessageId: action.payload.lastMessageId, + topicId: action.payload.topicId, + importance: action.payload.importance, // 添加重要性评分 + keywords: action.payload.keywords // 添加关键词 + } + + // 确保 memories 存在 + if (!state.memories) { + state.memories = [] + } + state.memories.push(newMemory) + + // 更新记忆列表的更新时间 + const list = state.memoryLists.find((list) => list.id === listId) + if (list) { + list.updatedAt = new Date().toISOString() + } + }, + + // 删除记忆 + deleteMemory: (state, action: PayloadAction) => { + // 确保 memories 存在 + if (!state.memories) { + state.memories = [] + return + } + state.memories = state.memories.filter((memory) => memory.id !== action.payload) + }, + + // 编辑记忆 + editMemory: (state, action: PayloadAction<{ id: string; content: string }>) => { + // 确保 memories 存在 + if (!state.memories) { + state.memories = [] + return + } + + const memory = state.memories.find((m) => m.id === action.payload.id) + if (memory) { + memory.content = action.payload.content + } + }, + + // 设置记忆功能是否激活 + setMemoryActive: (state, action: PayloadAction) => { + state.isActive = action.payload + }, + + // 设置是否自动分析 + setAutoAnalyze: (state, action: PayloadAction) => { + state.autoAnalyze = action.payload + }, + + // 设置是否过滤敏感信息 + setFilterSensitiveInfo: (state, action: PayloadAction) => { + state.filterSensitiveInfo = action.payload + }, + + // 设置长期记忆分析模型 + setAnalyzeModel: (state, action: PayloadAction) => { + state.analyzeModel = action.payload + }, + + // 设置短期记忆分析模型 + setShortMemoryAnalyzeModel: (state, action: PayloadAction) => { + state.shortMemoryAnalyzeModel = action.payload + }, + + // 设置历史对话上下文分析模型 + setHistoricalContextAnalyzeModel: (state, action: PayloadAction) => { + state.historicalContextAnalyzeModel = action.payload + }, + // 设置向量化模型 + setVectorizeModel: (state, action: PayloadAction) => { + state.vectorizeModel = action.payload + }, + + // 设置分析状态 + setAnalyzing: (state, action: PayloadAction) => { + state.isAnalyzing = action.payload + if (action.payload) { + state.lastAnalyzeTime = Date.now() + } + }, + + // 批量添加记忆(用于导入) + importMemories: (state, action: PayloadAction<{ memories: Memory[]; listId?: string }>) => { + // 确保 memoryLists 存在 + if (!state.memoryLists) { + state.memoryLists = [defaultList] + } + + const listId = + action.payload.listId || + state.currentListId || + (state.memoryLists.length > 0 ? state.memoryLists[0].id : defaultList.id) + + // 确保 memories 存在 + if (!state.memories) { + state.memories = [] + } + + // 合并记忆,避免重复 + const existingContents = new Set(state.memories.map((m) => m.content)) + const newMemories = action.payload.memories + .filter((m) => !existingContents.has(m.content)) + .map((m) => ({ ...m, listId })) // 确保所有导入的记忆都有正确的列表ID + + state.memories = [...state.memories, ...newMemories] + + // 更新记忆列表的更新时间 + const list = state.memoryLists.find((list) => list.id === listId) + if (list) { + list.updatedAt = new Date().toISOString() + } + }, + + // 清空指定列表的记忆 + clearMemories: (state, action: PayloadAction) => { + // 确保 memories 存在 + if (!state.memories) { + state.memories = [] + return + } + + const listId = action.payload || state.currentListId + + if (listId) { + // 清空指定列表的记忆 + state.memories = state.memories.filter((memory) => memory.listId !== listId) + } else { + // 清空所有记忆 + state.memories = [] + } + }, + + // 添加新的记忆列表 + addMemoryList: (state, action: PayloadAction<{ name: string; description?: string; isActive?: boolean }>) => { + const newList: MemoryList = { + id: nanoid(), + name: action.payload.name, + description: action.payload.description, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + isActive: action.payload.isActive ?? false + } + // 确保 memoryLists 存在 + if (!state.memoryLists) { + state.memoryLists = [] + } + state.memoryLists.push(newList) + }, + + // 删除记忆列表 + deleteMemoryList: (state, action: PayloadAction) => { + // 确保 memoryLists 存在 + if (!state.memoryLists) { + state.memoryLists = [] + return + } + + // 删除列表 + state.memoryLists = state.memoryLists.filter((list) => list.id !== action.payload) + + // 删除该列表下的所有记忆 + if (state.memories) { + state.memories = state.memories.filter((memory) => memory.listId !== action.payload) + } + + // 如果删除的是当前选中的列表,则切换到第一个列表 + if (state.currentListId === action.payload) { + state.currentListId = state.memoryLists.length > 0 ? state.memoryLists[0].id : null + } + }, + + // 编辑记忆列表 + editMemoryList: (state, action: PayloadAction<{ id: string; name?: string; description?: string }>) => { + // 确保 memoryLists 存在 + if (!state.memoryLists) { + state.memoryLists = [] + return + } + + const list = state.memoryLists.find((list) => list.id === action.payload.id) + if (list) { + if (action.payload.name) list.name = action.payload.name + if (action.payload.description !== undefined) list.description = action.payload.description + list.updatedAt = new Date().toISOString() + } + }, + + // 设置当前选中的记忆列表 + setCurrentMemoryList: (state, action: PayloadAction) => { + // 确保 memoryLists 存在 + if (!state.memoryLists) { + state.memoryLists = [] + } + state.currentListId = action.payload + }, + + // 切换记忆列表的激活状态 + toggleMemoryListActive: (state, action: PayloadAction<{ id: string; isActive: boolean }>) => { + // 确保 memoryLists 存在 + if (!state.memoryLists) { + state.memoryLists = [] + return + } + + const list = state.memoryLists.find((list) => list.id === action.payload.id) + if (list) { + list.isActive = action.payload.isActive + list.updatedAt = new Date().toISOString() + } + }, + + // 添加短记忆 + addShortMemory: ( + state, + action: PayloadAction<{ + content: string + topicId: string + analyzedMessageIds?: string[] + lastMessageId?: string + importance?: number // 新增重要性评分 + keywords?: string[] // 新增关键词 + }> + ) => { + const newShortMemory: ShortMemory = { + id: nanoid(), + content: action.payload.content, + createdAt: new Date().toISOString(), + topicId: action.payload.topicId, + analyzedMessageIds: action.payload.analyzedMessageIds, + lastMessageId: action.payload.lastMessageId, + importance: action.payload.importance, // 添加重要性评分 + keywords: action.payload.keywords // 添加关键词 + } + + // 确保 shortMemories 存在 + if (!state.shortMemories) { + state.shortMemories = [] + } + + state.shortMemories.push(newShortMemory) + }, + + // 删除短记忆 + deleteShortMemory: (state, action: PayloadAction) => { + // 确保 shortMemories 存在 + if (!state.shortMemories) { + state.shortMemories = [] + return + } + + // 找到要删除的记忆 + const memoryToDelete = state.shortMemories.find((memory) => memory.id === action.payload) + + // 如果找到了要删除的记忆,并且它有分析过的消息ID + if (memoryToDelete && memoryToDelete.analyzedMessageIds && memoryToDelete.analyzedMessageIds.length > 0) { + // 获取要删除的记忆的消息ID + const messageIdsToCheck = new Set(memoryToDelete.analyzedMessageIds) + + // 检查其他记忆是否也引用了这些消息ID + // 创建一个映射,记录每个消息ID被引用的次数 + 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 + messageIdReferences.set(msgId, (messageIdReferences.get(msgId) || 0) + 1) + } + }) + } + }) + + // 找出没有被其他记忆引用的消息ID + 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` + ) + + // 将这些消息ID标记为未分析,以便下次分析时重新分析这些消息 + // 注意:我们不需要显式地清除标记,因为分析逻辑会检查消息ID是否在任何记忆的analyzedMessageIds中 + // 如果消息ID不再被任何记忆引用,它将自动被视为未分析 + } + + // 记录日志,方便调试 + console.log(`[Memory] Deleting short memory with ${messageIdsToCheck.size} analyzed message IDs`) + } + + // 删除记忆 + state.shortMemories = state.shortMemories.filter((memory) => memory.id !== action.payload) + }, + + // 清空指定话题的短记忆 + clearShortMemories: (state, action: PayloadAction) => { + // 确保 shortMemories 存在 + if (!state.shortMemories) { + state.shortMemories = [] + return + } + + const topicId = action.payload + + if (topicId) { + // 清空指定话题的短记忆 + state.shortMemories = state.shortMemories.filter((memory) => memory.topicId !== topicId) + } else { + // 清空所有短记忆 + state.shortMemories = [] + } + }, + + // 设置短记忆功能是否激活 + 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 + } + } + }, + + // 设置上下文感知记忆推荐是否启用 + setContextualRecommendationEnabled: (state, action: PayloadAction) => { + state.contextualRecommendationEnabled = action.payload + }, + + // 直接设置记忆数组(用于重置分析标记等操作) + setMemories: (state, action: PayloadAction) => { + state.memories = 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) => { + builder + .addCase(loadMemoryData.fulfilled, (state, action) => { + if (action.payload) { + // 更新状态中的记忆数据 + state.memoryLists = action.payload.memoryLists || state.memoryLists + 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) + } + + console.log('Short-term memory data loaded into state') + } + }) + .addCase(loadLongTermMemoryData.fulfilled, (state, action) => { + if (action.payload) { + // 更新状态中的长期记忆数据 + state.memoryLists = action.payload.memoryLists || state.memoryLists + state.memories = action.payload.memories || state.memories + + // 更新模型选择 + if (action.payload.analyzeModel) { + state.analyzeModel = action.payload.analyzeModel + console.log('[Memory Reducer] Loaded long-term analyze model:', action.payload.analyzeModel) + } + + // 自动选择默认的记忆列表 + if (!state.currentListId && state.memoryLists && state.memoryLists.length > 0) { + // 先尝试找到一个isActive为true的列表 + 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) + } else { + // 如果没有激活的列表,使用第一个列表 + state.currentListId = state.memoryLists[0].id + console.log('[Memory Reducer] Auto-selected first memory list:', state.memoryLists[0].name) + } + } + + 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 + ) + } else { + // 如果文件中没有historicalContextAnalyzeModel,使用shortMemoryAnalyzeModel或analyzeModel作为默认值 + state.historicalContextAnalyzeModel = state.shortMemoryAnalyzeModel || state.analyzeModel + console.log( + '[Memory Reducer] Using default model for historical context:', + state.historicalContextAnalyzeModel + ) + } + + if (action.payload.vectorizeModel) { + state.vectorizeModel = action.payload.vectorizeModel + console.log('[Memory Reducer] Loaded vectorize model:', action.payload.vectorizeModel) + } + + console.log('Memory data loaded into state') + } + }) + } +}) + +export const { + addMemory, + deleteMemory, + editMemory, + setMemoryActive, + setAutoAnalyze, + setFilterSensitiveInfo, + setAnalyzeModel, + setShortMemoryAnalyzeModel, + setHistoricalContextAnalyzeModel, + setVectorizeModel, + setAnalyzing, + importMemories, + clearMemories, + addMemoryList, + deleteMemoryList, + editMemoryList, + setCurrentMemoryList, + toggleMemoryListActive, + setMemories, + // 短记忆相关的action + addShortMemory, + deleteShortMemory, + clearShortMemories, + setShortMemoryActive, + + // 自适应分析相关的action + setAdaptiveAnalysisEnabled, + setAnalysisFrequency, + setAnalysisDepth, + updateAnalysisStats, + + // 用户关注点相关的action + setInterestTrackingEnabled, + updateUserInterest, + + // 性能监控相关的action + setMonitoringEnabled, + updatePerformanceMetrics, + addAnalysisLatency, + addMemoryRetrievalLatency, + + // 智能优先级与时效性管理相关的action + setPriorityManagementEnabled, + setDecayEnabled, + setFreshnessEnabled, + setDecayRate, + updateMemoryPriorities, + updateMemoryFreshness, + accessMemory, + + // 上下文感知记忆推荐相关的action + setContextualRecommendationEnabled, + setAutoRecommendMemories, + setRecommendationThreshold, + updateCurrentRecommendations, + setRecommending, + clearCurrentRecommendations +} = memorySlice.actions + +// 加载记忆数据的异步 thunk +export const loadMemoryData = createAsyncThunk( + 'memory/loadData', + async () => { + try { + // 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') // Removed direct log call from renderer + return data + } catch (error) { + 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 +export const saveMemoryData = createAsyncThunk( + 'memory/saveData', + async (data: Partial & { forceOverwrite?: boolean }) => { + const { forceOverwrite, ...memoryData } = data + try { + console.log('[Memory] Saving memory data to file...', Object.keys(data)) + + // 如果是强制覆盖模式,直接使用传入的数据,不合并当前状态 + if (forceOverwrite) { + console.log('[Memory] Force overwrite mode enabled, using provided data directly') + const result = await window.api.memory.saveData(memoryData, forceOverwrite) + console.log('[Memory] Memory data saved successfully (force overwrite)') + return result + } + + // 非强制覆盖模式,确保数据完整性 + const state = store.getState().memory + + // 保存所有设置,而不仅仅是特定字段 + // 创建一个包含所有设置的对象 + 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, + + // 模型选择 + analyzeModel: memoryData.analyzeModel || state.analyzeModel, + shortMemoryAnalyzeModel: memoryData.shortMemoryAnalyzeModel || state.shortMemoryAnalyzeModel, + historicalContextAnalyzeModel: memoryData.historicalContextAnalyzeModel || state.historicalContextAnalyzeModel, + vectorizeModel: memoryData.vectorizeModel || state.vectorizeModel, + + // 记忆数据 + memoryLists: memoryData.memoryLists || state.memoryLists, + shortMemories: memoryData.shortMemories || state.shortMemories, + currentListId: memoryData.currentListId || state.currentListId, + + // 自适应分析相关 + 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, + + // 性能监控相关 + monitoringEnabled: + memoryData.monitoringEnabled !== undefined ? memoryData.monitoringEnabled : state.monitoringEnabled, + + // 智能优先级与时效性管理相关 + priorityManagementEnabled: + memoryData.priorityManagementEnabled !== undefined + ? memoryData.priorityManagementEnabled + : state.priorityManagementEnabled, + decayEnabled: memoryData.decayEnabled !== undefined ? memoryData.decayEnabled : state.decayEnabled, + 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 + } + + const result = await window.api.memory.saveData(completeData, forceOverwrite) + console.log('[Memory] Memory data saved successfully') + return result + } catch (error) { + console.error('[Memory] Failed to save memory data:', error) + return false + } + } +) + +// 加载长期记忆数据的异步 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 + } +}) + +// 保存长期记忆数据的异步 thunk +export const saveLongTermMemoryData = createAsyncThunk( + 'memory/saveLongTermData', + async (data: Partial & { forceOverwrite?: boolean }) => { + const { forceOverwrite, ...memoryData } = data + try { + console.log('[Long-term Memory] Saving long-term memory data to file...', Object.keys(data)) + + // 如果是强制覆盖模式,直接使用传入的数据,不合并当前状态 + if (forceOverwrite) { + console.log('[Long-term Memory] Force overwrite mode enabled, using provided data directly') + const result = await window.api.memory.saveLongTermData(memoryData, forceOverwrite) + console.log('[Long-term Memory] Long-term memory data saved successfully (force overwrite)') + return result + } + + // 非强制覆盖模式,确保数据完整性 + const state = store.getState().memory + + // 保存所有设置,而不仅仅是特定字段 + // 创建一个包含所有设置的对象 + 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, + + // 模型选择 + analyzeModel: memoryData.analyzeModel || state.analyzeModel, + + // 记忆数据 + memoryLists: memoryData.memoryLists || state.memoryLists, + memories: memoryData.memories || state.memories, + currentListId: memoryData.currentListId || state.currentListId, + + // 自适应分析相关 + 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, + + // 性能监控相关 + monitoringEnabled: + memoryData.monitoringEnabled !== undefined ? memoryData.monitoringEnabled : state.monitoringEnabled, + + // 智能优先级与时效性管理相关 + priorityManagementEnabled: + memoryData.priorityManagementEnabled !== undefined + ? memoryData.priorityManagementEnabled + : state.priorityManagementEnabled, + decayEnabled: memoryData.decayEnabled !== undefined ? memoryData.decayEnabled : state.decayEnabled, + 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 + } + + const result = await window.api.memory.saveLongTermData(completeData, forceOverwrite) + console.log('[Long-term Memory] Long-term memory data saved successfully') + return result + } catch (error) { + console.error('[Long-term Memory] Failed to save long-term memory data:', error) + return false + } + } +) + +// 保存所有记忆设置的函数 +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, + + // 模型选择 + analyzeModel: state.analyzeModel, + shortMemoryAnalyzeModel: state.shortMemoryAnalyzeModel, + historicalContextAnalyzeModel: state.historicalContextAnalyzeModel, + vectorizeModel: state.vectorizeModel, + + // 自适应分析相关 + adaptiveAnalysisEnabled: state.adaptiveAnalysisEnabled, + analysisFrequency: state.analysisFrequency, + analysisDepth: state.analysisDepth, + + // 用户关注点相关 + interestTrackingEnabled: state.interestTrackingEnabled, + + // 性能监控相关 + monitoringEnabled: state.monitoringEnabled, + + // 智能优先级与时效性管理相关 + 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 + } +}) + +// 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 diff --git a/src/renderer/src/store/messages.ts b/src/renderer/src/store/messages.ts index 6e244b892e..54965fbfe1 100644 --- a/src/renderer/src/store/messages.ts +++ b/src/renderer/src/store/messages.ts @@ -391,14 +391,34 @@ export const sendMessage = }) } catch (error: any) { console.error('Error in chat completion:', error) + // 添加检查,防止意外的错误消息被保存 + const errorMessage = + typeof error?.message === 'string' + ? error.message + : 'An unexpected error occurred during chat completion.' + + // 检查是否是我们不希望保存的特定字符串,如果是,替换为通用错误 + let finalErrorMessage = errorMessage + + // 检查多种可能的 rememberInstructions 错误形式 + if ( + errorMessage === 'rememberInstructions is not defined' || + (typeof errorMessage === 'string' && errorMessage.includes('rememberInstructions')) + ) { + console.warn('Detected and sanitized rememberInstructions error') + finalErrorMessage = 'An unexpected error occurred.' + } + dispatch( updateMessageThunk(topic.id, assistantMessage.id, { status: 'error', - error: { message: error.message } + // 使用处理过的错误消息 + error: { message: finalErrorMessage } }) ) dispatch(clearStreamMessage({ topicId: topic.id, messageId: assistantMessage.id })) - dispatch(setError(error.message)) + // setError 也使用处理过的消息 + dispatch(setError(finalErrorMessage)) } }) } diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index ece6d8cdbe..8aeda166a7 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -50,6 +50,7 @@ export interface SettingsState { clickAssistantToShowTopic: boolean autoCheckUpdate: boolean renderInputMessageAsMarkdown: boolean + enableHistoricalContext: boolean // 是否启用历史对话上下文功能 codeShowLineNumbers: boolean codeCollapsible: boolean codeWrappable: boolean @@ -156,6 +157,7 @@ export const initialState: SettingsState = { clickAssistantToShowTopic: true, autoCheckUpdate: true, renderInputMessageAsMarkdown: false, + enableHistoricalContext: false, // 默认禁用历史对话上下文功能 codeShowLineNumbers: false, codeCollapsible: false, codeWrappable: false, @@ -310,6 +312,10 @@ const settingsSlice = createSlice({ setRenderInputMessageAsMarkdown: (state, action: PayloadAction) => { state.renderInputMessageAsMarkdown = action.payload }, + + setEnableHistoricalContext: (state, action: PayloadAction) => { + state.enableHistoricalContext = action.payload + }, setClickAssistantToShowTopic: (state, action: PayloadAction) => { state.clickAssistantToShowTopic = action.payload }, @@ -519,6 +525,7 @@ export const { setPasteLongTextAsFile, setAutoCheckUpdate, setRenderInputMessageAsMarkdown, + setEnableHistoricalContext, setClickAssistantToShowTopic, setWebdavHost, setWebdavUser, diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 61c5a6d6c7..fc5f5c7642 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -73,6 +73,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/src/renderer/src/utils/error.ts b/src/renderer/src/utils/error.ts index a53c9e5923..1f47e03e01 100644 --- a/src/renderer/src/utils/error.ts +++ b/src/renderer/src/utils/error.ts @@ -28,17 +28,59 @@ export function getErrorDetails(err: any, seen = new WeakSet()): any { export function formatErrorMessage(error: any): string { console.error('Original error:', error) + // 检查已知的问题错误对象 + if (typeof error === 'object' && error !== null) { + // 特别检查 rememberInstructions 错误 + if (error.message === 'rememberInstructions is not defined') { + console.warn('Formatting known corrupted error message from storage.') + // 返回安全的通用错误消息 + return '```\nError: A previously recorded error message could not be displayed.\n```' + } + + // 检查错误对象中是否包含 rememberInstructions 字符串 + if (JSON.stringify(error).includes('rememberInstructions')) { + console.warn('Detected potential rememberInstructions issue in error object') + return '```\nError: An error occurred while processing the message.\n```' + } + + // 处理网络错误 + if (error.message === 'network error') { + console.warn('Network error detected') + return '```\nError: 网络连接错误,请检查您的网络连接并重试\n```' + } + + // 处理其他网络相关错误 + if ( + typeof error.message === 'string' && + (error.message.includes('network') || + error.message.includes('timeout') || + error.message.includes('connection') || + error.message.includes('ECONNREFUSED')) + ) { + console.warn('Network-related error detected:', error.message) + return '```\nError: 网络连接问题\n```' + } + } + try { const detailedError = getErrorDetails(error) delete detailedError?.headers delete detailedError?.stack delete detailedError?.request_id - return '```json\n' + JSON.stringify(detailedError, null, 2) + '\n```' - } catch (e) { + // Ensure stringification is safe + try { + return '```json\n' + JSON.stringify(detailedError, null, 2) + '\n```' + } catch (stringifyError) { + console.error('Error stringifying detailed error:', stringifyError) + return '```\nError: Unable to stringify detailed error message.\n```' + } + } catch (getDetailsError) { + console.error('Error getting error details:', getDetailsError) + // Fallback to simple string conversion if getErrorDetails fails try { return '```\n' + String(error) + '\n```' } catch { - return 'Error: Unable to format error message' + return '```\nError: Unable to format error message.\n```' } } } diff --git a/src/renderer/src/utils/prompt.ts b/src/renderer/src/utils/prompt.ts index 698fdb1186..0da18bc5f3 100644 --- a/src/renderer/src/utils/prompt.ts +++ b/src/renderer/src/utils/prompt.ts @@ -147,12 +147,55 @@ ${availableTools} ` } -export const buildSystemPrompt = (userSystemPrompt: string, tools: MCPTool[]): string => { - if (tools && tools.length > 0) { - return SYSTEM_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', userSystemPrompt) - .replace('{{ TOOL_USE_EXAMPLES }}', ToolUseExamples) - .replace('{{ AVAILABLE_TOOLS }}', AvailableTools(tools)) +import { applyMemoriesToPrompt } from '@renderer/services/MemoryService' +import { MCPServer } from '@renderer/types' + +import { getRememberedMemories } from './remember-utils' +export const buildSystemPrompt = async ( + userSystemPrompt: string, + tools: MCPTool[], + mcpServers: MCPServer[] = [] +): Promise => { + // 获取MCP记忆 + let mcpMemoriesPrompt = '' + try { + mcpMemoriesPrompt = await getRememberedMemories(mcpServers) + } catch (error) { + console.error('Error getting MCP memories:', error) } - return userSystemPrompt -} + // 获取内置记忆 + let appMemoriesPrompt = '' + try { + // 应用内置记忆功能 + console.log('[Prompt] Applying app memories to prompt') + // 直接将用户系统提示词传递给 applyMemoriesToPrompt,让它添加记忆 + appMemoriesPrompt = await applyMemoriesToPrompt(userSystemPrompt) + console.log('[Prompt] App memories prompt length:', appMemoriesPrompt.length - userSystemPrompt.length) + } catch (error) { + console.error('Error applying app memories:', error) + // 如果应用 Redux 记忆失败,至少保留原始用户提示 + appMemoriesPrompt = userSystemPrompt + } + + // 添加记忆工具的使用说明 + // 合并所有提示词 + // 注意:appMemoriesPrompt 已经包含 userSystemPrompt,所以不需要再次添加 + // 合并 app 记忆(已包含 user prompt)和 mcp 记忆 + const enhancedPrompt = appMemoriesPrompt + (mcpMemoriesPrompt ? `\n\n${mcpMemoriesPrompt}` : '') + + let finalPrompt: string + if (tools && tools.length > 0) { + console.log('[Prompt] Final prompt with tools:', { promptLength: enhancedPrompt.length }) + // Break down the chained replace calls to potentially help the parser + const availableToolsString = AvailableTools(tools) + let tempPrompt = SYSTEM_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', enhancedPrompt) + tempPrompt = tempPrompt.replace('{{ TOOL_USE_EXAMPLES }}', ToolUseExamples) + finalPrompt = tempPrompt.replace('{{ AVAILABLE_TOOLS }}', availableToolsString) + } else { + console.log('[Prompt] Final prompt without tools:', { promptLength: enhancedPrompt.length }) + finalPrompt = enhancedPrompt // Assign enhancedPrompt when no tools are present + } + // Single return point for the function + return finalPrompt +} // Closing brace for the buildSystemPrompt function moved here diff --git a/src/renderer/src/utils/remember-utils.ts b/src/renderer/src/utils/remember-utils.ts new file mode 100644 index 0000000000..f7fda8a650 --- /dev/null +++ b/src/renderer/src/utils/remember-utils.ts @@ -0,0 +1,68 @@ +// 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 '' + } + + // 构建记忆提示词 + // Add explicit type for memory item in map function + const memoryPrompt = memories.map((memory: { content: string }) => `- ${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 '' + } +} diff --git a/yarn.lock b/yarn.lock index d79730c0a5..e401a78e10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3364,6 +3364,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" @@ -3371,7 +3403,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: @@ -3380,6 +3436,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" @@ -3389,6 +3498,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" @@ -3396,7 +3549,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: @@ -3405,7 +3588,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: @@ -3415,6 +3598,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" @@ -3466,6 +3687,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" @@ -3992,6 +4220,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/js-yaml": "npm:^4" @@ -4015,6 +4244,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" @@ -5482,6 +5712,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" @@ -5765,21 +6002,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: @@ -5789,14 +6082,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: @@ -5805,6 +6162,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" @@ -5812,14 +6220,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: @@ -5834,7 +6269,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: @@ -5847,6 +6282,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" @@ -6138,6 +6611,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" @@ -9049,7 +9531,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: @@ -9191,6 +9673,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" @@ -14612,6 +15101,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" @@ -14733,6 +15229,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" 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. 这个功能只影响新分析的对话内容,已经存储的记忆不会受到影响。如果用户想要清除可能包含敏感信息的记忆,需要手动删除这些记忆。