记忆功能

This commit is contained in:
1600822305 2025-04-13 03:51:11 +08:00
parent 54a8f31422
commit 8aea052bd6
33 changed files with 3067 additions and 510 deletions

View File

@ -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<string, string> = {}): 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}`)
}

View File

@ -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<Entity[]> {
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<Relation[]> {
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<string>()
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<void> {
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<void> {
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<KnowledgeGraph> {
// 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<KnowledgeGraph> {
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<KnowledgeGraph> {
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<void>; // To track initialization
private knowledgeGraphManager: KnowledgeGraphManager | null = null
private initializationPromise: Promise<void> // 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<void> {
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<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;
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)}`
)
}
})
}

View File

@ -1,99 +1,114 @@
// src/main/mcpServers/simpleremember.ts
import { getConfigDir } from '@main/utils/file'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'
import {
CallToolRequestSchema,
ErrorCode,
ListPromptsRequestSchema,
ListToolsRequestSchema,
McpError
} from '@modelcontextprotocol/sdk/types.js'
import { Mutex } from 'async-mutex'
import { promises as fs } from 'fs'
import path from 'path'
import { Mutex } from 'async-mutex'
// 定义记忆文件路径
const defaultMemoryPath = path.join(getConfigDir(), 'simpleremember.json')
// 记忆项接口
interface Memory {
content: string;
createdAt: string;
content: string
createdAt: string
}
// 记忆存储结构
interface MemoryStorage {
memories: Memory[];
memories: Memory[]
}
class SimpleRememberManager {
private memoryPath: string;
private memories: Memory[] = [];
private fileMutex: Mutex = new Mutex();
private memoryPath: string
private memories: Memory[] = []
private fileMutex: Mutex = new Mutex()
constructor(memoryPath: string) {
this.memoryPath = memoryPath;
this.memoryPath = memoryPath
}
// 静态工厂方法用于初始化
public static async create(memoryPath: string): Promise<SimpleRememberManager> {
const manager = new SimpleRememberManager(memoryPath);
await manager._ensureMemoryPathExists();
await manager._loadMemoriesFromDisk();
return manager;
const manager = new SimpleRememberManager(memoryPath)
await manager._ensureMemoryPathExists()
await manager._loadMemoriesFromDisk()
return manager
}
// 确保记忆文件存在
private async _ensureMemoryPathExists(): Promise<void> {
try {
const directory = path.dirname(this.memoryPath);
await fs.mkdir(directory, { recursive: true });
const directory = path.dirname(this.memoryPath)
await fs.mkdir(directory, { recursive: true })
try {
await fs.access(this.memoryPath);
await fs.access(this.memoryPath)
} catch (error) {
// 文件不存在,创建一个空文件
await fs.writeFile(this.memoryPath, JSON.stringify({ memories: [] }, null, 2));
await fs.writeFile(this.memoryPath, JSON.stringify({ memories: [] }, null, 2))
}
} catch (error) {
console.error('Failed to ensure memory path exists:', error);
throw new McpError(ErrorCode.InternalError, `Failed to ensure memory path: ${error instanceof Error ? error.message : String(error)}`);
console.error('Failed to ensure memory path exists:', error)
throw new McpError(
ErrorCode.InternalError,
`Failed to ensure memory path: ${error instanceof Error ? error.message : String(error)}`
)
}
}
// 从磁盘加载记忆
private async _loadMemoriesFromDisk(): Promise<void> {
try {
const data = await fs.readFile(this.memoryPath, 'utf-8');
const data = await fs.readFile(this.memoryPath, 'utf-8')
// 处理空文件情况
if (data.trim() === '') {
this.memories = [];
await this._persistMemories();
return;
this.memories = []
await this._persistMemories()
return
}
const storage: MemoryStorage = JSON.parse(data);
this.memories = storage.memories || [];
const storage: MemoryStorage = JSON.parse(data)
this.memories = storage.memories || []
} catch (error) {
if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') {
this.memories = [];
await this._persistMemories();
this.memories = []
await this._persistMemories()
} else if (error instanceof SyntaxError) {
console.error('Failed to parse simpleremember.json, initializing with empty memories:', error);
this.memories = [];
await this._persistMemories();
console.error('Failed to parse simpleremember.json, initializing with empty memories:', error)
this.memories = []
await this._persistMemories()
} else {
console.error('Unexpected error loading memories:', error);
throw new McpError(ErrorCode.InternalError, `Failed to load memories: ${error instanceof Error ? error.message : String(error)}`);
console.error('Unexpected error loading memories:', error)
throw new McpError(
ErrorCode.InternalError,
`Failed to load memories: ${error instanceof Error ? error.message : String(error)}`
)
}
}
}
// 将记忆持久化到磁盘
private async _persistMemories(): Promise<void> {
const release = await this.fileMutex.acquire();
const release = await this.fileMutex.acquire()
try {
const storage: MemoryStorage = {
memories: this.memories
};
await fs.writeFile(this.memoryPath, JSON.stringify(storage, null, 2));
}
await fs.writeFile(this.memoryPath, JSON.stringify(storage, null, 2))
} catch (error) {
console.error('Failed to save memories:', error);
throw new McpError(ErrorCode.InternalError, `Failed to save memories: ${error instanceof Error ? error.message : String(error)}`);
console.error('Failed to save memories:', error)
throw new McpError(
ErrorCode.InternalError,
`Failed to save memories: ${error instanceof Error ? error.message : String(error)}`
)
} finally {
release();
release()
}
}
@ -102,27 +117,28 @@ class SimpleRememberManager {
const newMemory: Memory = {
content: memory,
createdAt: new Date().toISOString()
};
this.memories.push(newMemory);
await this._persistMemories();
return newMemory;
}
this.memories.push(newMemory)
await this._persistMemories()
return newMemory
}
// 获取所有记忆
async getAllMemories(): Promise<Memory[]> {
return [...this.memories];
return [...this.memories]
}
// 获取记忆 - 这个方法会被get_memories工具调用
async get_memories(): Promise<Memory[]> {
return this.getAllMemories();
return this.getAllMemories()
}
}
// 定义工具 - 按照MCP规范定义工具
const REMEMBER_TOOL = {
name: 'remember',
description: '用于记忆长期有用信息的工具。这个工具会自动应用记忆,无需显式调用。只用于存储长期有用的信息,不适合临时信息。',
description:
'用于记忆长期有用信息的工具。这个工具会自动应用记忆,无需显式调用。只用于存储长期有用的信息,不适合临时信息。',
inputSchema: {
type: 'object',
properties: {
@ -133,7 +149,7 @@ const REMEMBER_TOOL = {
},
required: ['memory']
}
};
}
const GET_MEMORIES_TOOL = {
name: 'get_memories',
@ -142,24 +158,20 @@ const GET_MEMORIES_TOOL = {
type: 'object',
properties: {}
}
};
}
// 添加日志以便调试
console.log("[SimpleRemember] Defined tools:", { REMEMBER_TOOL, GET_MEMORIES_TOOL });
console.log('[SimpleRemember] Defined tools:', { REMEMBER_TOOL, GET_MEMORIES_TOOL })
class SimpleRememberServer {
public server: Server;
private simpleRememberManager: SimpleRememberManager | null = null;
private initializationPromise: Promise<void>;
public server: Server
private simpleRememberManager: SimpleRememberManager | null = null
private initializationPromise: Promise<void>
constructor(envPath: string = '') {
const memoryPath = envPath
? path.isAbsolute(envPath)
? envPath
: path.resolve(envPath)
: defaultMemoryPath;
const memoryPath = envPath ? (path.isAbsolute(envPath) ? envPath : path.resolve(envPath)) : defaultMemoryPath
console.log("[SimpleRemember] Creating server with memory path:", memoryPath);
console.log('[SimpleRemember] Creating server with memory path:', memoryPath)
// 初始化服务器
this.server = new Server(
@ -177,127 +189,133 @@ class SimpleRememberServer {
prompts: {}
}
}
);
)
console.log("[SimpleRemember] Server initialized with tools capability");
console.log('[SimpleRemember] Server initialized with tools capability')
// 手动添加工具到服务器的工具列表中
console.log("[SimpleRemember] Adding tools to server");
console.log('[SimpleRemember] Adding tools to server')
// 先设置请求处理程序,再初始化管理器
this.setupRequestHandlers();
this.initializationPromise = this._initializeManager(memoryPath);
this.setupRequestHandlers()
this.initializationPromise = this._initializeManager(memoryPath)
console.log("[SimpleRemember] Server initialization complete");
console.log('[SimpleRemember] Server initialization complete')
// 打印工具信息以确认它们已注册
console.log("[SimpleRemember] Tools registered:", [REMEMBER_TOOL.name, GET_MEMORIES_TOOL.name]);
console.log('[SimpleRemember] Tools registered:', [REMEMBER_TOOL.name, GET_MEMORIES_TOOL.name])
}
private async _initializeManager(memoryPath: string): Promise<void> {
try {
this.simpleRememberManager = await SimpleRememberManager.create(memoryPath);
console.log("SimpleRememberManager initialized successfully.");
this.simpleRememberManager = await SimpleRememberManager.create(memoryPath)
console.log('SimpleRememberManager initialized successfully.')
} catch (error) {
console.error("Failed to initialize SimpleRememberManager:", error);
this.simpleRememberManager = null;
console.error('Failed to initialize SimpleRememberManager:', error)
this.simpleRememberManager = null
}
}
private async _getManager(): Promise<SimpleRememberManager> {
if (!this.simpleRememberManager) {
await this.initializationPromise;
await this.initializationPromise
if (!this.simpleRememberManager) {
throw new McpError(ErrorCode.InternalError, "SimpleRememberManager is not initialized");
throw new McpError(ErrorCode.InternalError, 'SimpleRememberManager is not initialized')
}
}
return this.simpleRememberManager;
return this.simpleRememberManager
}
setupRequestHandlers() {
// 添加对prompts/list请求的处理
this.server.setRequestHandler(ListPromptsRequestSchema, async (request) => {
console.log("[SimpleRemember] Listing prompts request received", request);
console.log('[SimpleRemember] Listing prompts request received', request)
// 返回空的提示词列表
return {
prompts: []
};
});
}
})
this.server.setRequestHandler(ListToolsRequestSchema, async (request) => {
// 直接返回工具列表,不需要等待管理器初始化
console.log("[SimpleRemember] Listing tools request received", request);
console.log('[SimpleRemember] Listing tools request received', request)
// 打印工具定义以确保它们存在
console.log("[SimpleRemember] REMEMBER_TOOL:", JSON.stringify(REMEMBER_TOOL));
console.log("[SimpleRemember] GET_MEMORIES_TOOL:", JSON.stringify(GET_MEMORIES_TOOL));
console.log('[SimpleRemember] REMEMBER_TOOL:', JSON.stringify(REMEMBER_TOOL))
console.log('[SimpleRemember] GET_MEMORIES_TOOL:', JSON.stringify(GET_MEMORIES_TOOL))
const toolsList = [REMEMBER_TOOL, GET_MEMORIES_TOOL];
console.log("[SimpleRemember] Returning tools:", JSON.stringify(toolsList));
const toolsList = [REMEMBER_TOOL, GET_MEMORIES_TOOL]
console.log('[SimpleRemember] Returning tools:', JSON.stringify(toolsList))
// 按照MCP规范返回工具列表
return {
tools: toolsList,
tools: toolsList
// 如果有分页可以添加nextCursor
// nextCursor: "next-page-cursor"
};
});
}
})
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const { name, arguments: args } = request.params
console.log(`[SimpleRemember] Received tool call: ${name}`, args);
console.log(`[SimpleRemember] Received tool call: ${name}`, args)
try {
const manager = await this._getManager();
const manager = await this._getManager()
if (name === 'remember') {
if (!args || typeof args.memory !== 'string') {
console.error(`[SimpleRemember] Invalid arguments for ${name}:`, args);
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'memory' string is required.`);
console.error(`[SimpleRemember] Invalid arguments for ${name}:`, args)
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'memory' string is required.`)
}
console.log(`[SimpleRemember] Remembering: "${args.memory}"`);
const result = await manager.remember(args.memory);
console.log(`[SimpleRemember] Memory saved successfully:`, result);
console.log(`[SimpleRemember] Remembering: "${args.memory}"`)
const result = await manager.remember(args.memory)
console.log(`[SimpleRemember] Memory saved successfully:`, result)
// 按照MCP规范返回工具调用结果
return {
content: [{
type: 'text',
text: `记忆已保存: "${args.memory}"`
}],
content: [
{
type: 'text',
text: `记忆已保存: "${args.memory}"`
}
],
isError: false
};
}
}
if (name === 'get_memories') {
console.log(`[SimpleRemember] Getting all memories`);
const memories = await manager.get_memories();
console.log(`[SimpleRemember] Retrieved ${memories.length} memories`);
console.log(`[SimpleRemember] Getting all memories`)
const memories = await manager.get_memories()
console.log(`[SimpleRemember] Retrieved ${memories.length} memories`)
// 按照MCP规范返回工具调用结果
return {
content: [{
type: 'text',
text: JSON.stringify(memories, null, 2)
}],
content: [
{
type: 'text',
text: JSON.stringify(memories, null, 2)
}
],
isError: false
};
}
}
console.error(`[SimpleRemember] Unknown tool: ${name}`);
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
console.error(`[SimpleRemember] Unknown tool: ${name}`)
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`)
} catch (error) {
console.error(`[SimpleRemember] Error handling tool call ${name}:`, error);
console.error(`[SimpleRemember] Error handling tool call ${name}:`, error)
// 按照MCP规范返回工具调用错误
return {
content: [{
type: 'text',
text: error instanceof Error ? error.message : String(error)
}],
content: [
{
type: 'text',
text: error instanceof Error ? error.message : String(error)
}
],
isError: true
};
}
}
});
})
}
}
export default SimpleRememberServer;
export default SimpleRememberServer

View File

@ -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 {
<AntdProvider>
<SyntaxHighlighterProvider>
<PersistGate loading={null} persistor={persistor}>
<TopViewContainer>
<HashRouter>
<NavigationHandler />
<Sidebar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings" element={<PaintingsPage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</HashRouter>
</TopViewContainer>
<MemoryProvider>
<TopViewContainer>
<HashRouter>
<NavigationHandler />
<Sidebar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings" element={<PaintingsPage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</HashRouter>
</TopViewContainer>
</MemoryProvider>
</PersistGate>
</SyntaxHighlighterProvider>
</AntdProvider>

View File

@ -0,0 +1,72 @@
import { useMemoryService } from '@renderer/services/MemoryService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { clearShortMemories } from '@renderer/store/memory'
import { FC, ReactNode, useEffect, useRef } from 'react'
interface MemoryProviderProps {
children: ReactNode
}
/**
*
*
*/
const MemoryProvider: FC<MemoryProviderProps> = ({ 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<string | null>(null)
// 添加一个 ref 来存储上次分析时的消息数量
const lastAnalyzedCountRef = useRef(0)
// 当对话更新时,触发记忆分析
useEffect(() => {
if (isActive && autoAnalyze && analyzeModel && messages.length > 0) {
// 检查是否有新消息需要分析
const newMessagesCount = messages.length - lastAnalyzedCountRef.current
// 当有 5 条或更多新消息,或者消息数量是 5 的倍数且从未分析过时触发分析
if (newMessagesCount >= 5 || (messages.length % 5 === 0 && lastAnalyzedCountRef.current === 0)) {
console.log(`[Memory Analysis] Triggering analysis with ${newMessagesCount} new messages`)
// 将当前话题ID传递给分析函数
analyzeAndAddMemories(currentTopic)
lastAnalyzedCountRef.current = messages.length
}
}
}, [isActive, autoAnalyze, analyzeModel, messages.length, analyzeAndAddMemories, currentTopic])
// 当对话话题切换时,清除上一个话题的短记忆
useEffect(() => {
// 如果短记忆功能激活且当前话题发生变化
if (shortMemoryActive && currentTopic !== previousTopicRef.current && previousTopicRef.current) {
console.log(`[Memory] Topic changed from ${previousTopicRef.current} to ${currentTopic}, clearing short memories`)
// 清除上一个话题的短记忆
dispatch(clearShortMemories(previousTopicRef.current))
}
// 更新上一次的话题ID
previousTopicRef.current = currentTopic
}, [currentTopic, shortMemoryActive, dispatch])
return <>{children}</>
}
export default MemoryProvider

View File

@ -3,11 +3,11 @@ import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
// Original translation
import enUS from './locales/en-us.json'
import jaJP from './locales/ja-jp.json'
import ruRU from './locales/ru-ru.json'
import zhCN from './locales/zh-cn.json'
import zhTW from './locales/zh-tw.json'
import enUS from './locales/en-US.json'
import jaJP from './locales/ja-JP.json'
import ruRU from './locales/ru-RU.json'
import zhCN from './locales/zh-CN.json'
import zhTW from './locales/zh-TW.json'
// Machine translation
import elGR from './translate/el-gr.json'
import esES from './translate/es-es.json'

View File

@ -1024,6 +1024,44 @@
"launch.onboot": "Start Automatically on Boot",
"launch.title": "Launch",
"launch.totray": "Minimize to Tray on Launch",
"memory": {
"title": "Memory Function",
"description": "Manage AI assistant's long-term memory, automatically analyze conversations and extract important information",
"enableMemory": "Enable Memory Function",
"enableAutoAnalyze": "Enable Auto Analysis",
"analyzeModel": "Analysis Model",
"selectModel": "Select Model",
"memoriesList": "Memory List",
"addMemory": "Add Memory",
"editMemory": "Edit Memory",
"clearAll": "Clear All",
"noMemories": "No memories yet",
"memoryPlaceholder": "Enter content to remember",
"addSuccess": "Memory added successfully",
"editSuccess": "Memory edited successfully",
"deleteSuccess": "Memory deleted successfully",
"clearSuccess": "Memories cleared successfully",
"clearConfirmTitle": "Confirm Clear",
"clearConfirmContent": "Are you sure you want to clear all memories? This action cannot be undone.",
"manualAnalyze": "Manual Analysis",
"analyzeNow": "Analyze Now",
"startingAnalysis": "Starting analysis...",
"cannotAnalyze": "Cannot analyze, please check settings",
"selectTopic": "Select Topic",
"selectTopicPlaceholder": "Select a topic to analyze",
"filterByCategory": "Filter by Category",
"allCategories": "All",
"uncategorized": "Uncategorized",
"shortMemory": "Short-term Memory",
"toggleShortMemoryActive": "Toggle Short-term Memory",
"addShortMemory": "Add Short-term Memory",
"addShortMemoryPlaceholder": "Enter short-term memory content, only valid in current conversation",
"noShortMemories": "No short-term memories",
"noCurrentTopic": "Please select a conversation topic first",
"confirmDelete": "Confirm Delete",
"confirmDeleteContent": "Are you sure you want to delete this short-term memory?",
"delete": "Delete"
},
"mcp": {
"actions": "Actions",
"active": "Active",

View File

@ -1024,6 +1024,59 @@
"launch.onboot": "开机自动启动",
"launch.title": "启动",
"launch.totray": "启动时最小化到托盘",
"memory": {
"title": "记忆功能",
"description": "管理AI助手的长期记忆自动分析对话并提取重要信息",
"enableMemory": "启用记忆功能",
"enableAutoAnalyze": "启用自动分析",
"analyzeModel": "分析模型",
"selectModel": "选择模型",
"memoriesList": "记忆列表",
"memoryLists": "记忆角色",
"addMemory": "添加记忆",
"editMemory": "编辑记忆",
"clearAll": "清空全部",
"noMemories": "暂无记忆",
"memoryPlaceholder": "输入要记住的内容",
"addSuccess": "记忆添加成功",
"editSuccess": "记忆编辑成功",
"deleteSuccess": "记忆删除成功",
"clearSuccess": "记忆清空成功",
"clearConfirmTitle": "确认清空",
"clearConfirmContent": "确定要清空所有记忆吗?此操作无法撤销。",
"listView": "列表视图",
"mindmapView": "思维导图",
"centerNodeLabel": "用户记忆",
"manualAnalyze": "手动分析",
"analyzeNow": "立即分析",
"startingAnalysis": "开始分析...",
"cannotAnalyze": "无法分析,请检查设置",
"selectTopic": "选择话题",
"selectTopicPlaceholder": "选择要分析的话题",
"filterByCategory": "按分类筛选",
"allCategories": "全部",
"uncategorized": "未分类",
"addList": "添加记忆列表",
"editList": "编辑记忆列表",
"listName": "列表名称",
"listNamePlaceholder": "输入列表名称",
"listDescription": "列表描述",
"listDescriptionPlaceholder": "输入列表描述(可选)",
"noLists": "暂无记忆列表",
"confirmDeleteList": "确认删除列表",
"confirmDeleteListContent": "确定要删除 {{name}} 列表吗?此操作将同时删除列表中的所有记忆,且不可恢复。",
"toggleActive": "切换激活状态",
"clearConfirmContentList": "确定要清空 {{name}} 中的所有记忆吗?此操作不可恢复。",
"shortMemory": "短期记忆",
"toggleShortMemoryActive": "切换短期记忆功能",
"addShortMemory": "添加短期记忆",
"addShortMemoryPlaceholder": "输入短期记忆内容,只在当前对话中有效",
"noShortMemories": "暂无短期记忆",
"noCurrentTopic": "请先选择一个对话话题",
"confirmDelete": "确认删除",
"confirmDeleteContent": "确定要删除这条短期记忆吗?",
"delete": "删除"
},
"mcp": {
"actions": "操作",
"active": "启用",

View File

@ -7,6 +7,31 @@ import styled from 'styled-components'
import Markdown from '../Markdown/Markdown'
const MessageError: FC<{ message: Message }> = ({ message }) => {
const { t } = useTranslation()
// 首先检查是否存在已知的问题错误
if (message.error && typeof message.error === 'object') {
// 处理 rememberInstructions 错误
if (message.error.message === 'rememberInstructions is not defined') {
return (
<>
<Markdown message={message} />
<Alert description="消息加载时发生错误" type="error" />
</>
)
}
// 处理网络错误
if (message.error.message === 'network error') {
return (
<>
<Markdown message={message} />
<Alert description={t('error.network')} type="error" />
</>
)
}
}
return (
<>
<Markdown message={message} />
@ -28,7 +53,13 @@ const MessageErrorInfo: FC<{ message: Message }> = ({ message }) => {
const HTTP_ERROR_CODES = [400, 401, 403, 404, 429, 500, 502, 503, 504]
if (message.error && HTTP_ERROR_CODES.includes(message.error?.status)) {
// Add more robust checks: ensure error is an object and status is a number before accessing/including
if (
message.error &&
typeof message.error === 'object' && // Check if error is an object
typeof message.error.status === 'number' && // Check if status is a number
HTTP_ERROR_CODES.includes(message.error.status) // Now safe to access status
) {
return <Alert description={t(`error.http.${message.error.status}`)} type="error" />
}

View File

@ -9,6 +9,7 @@ interface Props {
interface State {
hasError: boolean
errorMessage?: string
}
const ErrorFallback = ({ fallback }: { fallback?: React.ReactNode }) => {
@ -26,16 +27,52 @@ class MessageErrorBoundary extends React.Component<Props, State> {
this.state = { hasError: false }
}
static getDerivedStateFromError() {
return { hasError: true }
static getDerivedStateFromError(error: Error) {
// 检查是否是特定错误
let errorMessage: string | undefined = undefined
if (error.message === 'rememberInstructions is not defined') {
errorMessage = '消息加载时发生错误'
} else if (error.message === 'network error') {
errorMessage = '网络连接错误,请检查您的网络连接并重试'
} else if (
typeof error.message === 'string' &&
(error.message.includes('network') || error.message.includes('timeout') || error.message.includes('connection'))
) {
errorMessage = '网络连接问题'
}
return { hasError: true, errorMessage }
}
// 正确缩进 componentDidCatch
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log the detailed error information to the console
console.error('MessageErrorBoundary caught an error:', error, errorInfo)
// 如果是特定错误,记录更多信息
if (error.message === 'rememberInstructions is not defined') {
console.warn('Known issue with rememberInstructions detected in MessageErrorBoundary')
} else if (error.message === 'network error') {
console.warn('Network error detected in MessageErrorBoundary')
} else if (
typeof error.message === 'string' &&
(error.message.includes('network') || error.message.includes('timeout') || error.message.includes('connection'))
) {
console.warn('Network-related error detected in MessageErrorBoundary:', error.message)
}
}
// 正确缩进 render
render() {
if (this.state.hasError) {
// 如果有特定错误消息,显示自定义错误
if (this.state.errorMessage) {
return <Alert message="渲染错误" description={this.state.errorMessage} type="error" showIcon />
}
return <ErrorFallback fallback={this.props.fallback} />
}
return this.props.children
}
}
} // MessageErrorBoundary 类的结束括号,已删除多余的括号
export default MessageErrorBoundary

View File

@ -148,35 +148,42 @@ const MessageMenubar: FC<Props> = (props) => {
const imageUrls: string[] = []
let match
let content = editedText
while ((match = imageRegex.exec(editedText)) !== null) {
imageUrls.push(match[1])
content = content.replace(match[0], '')
}
// 更新消息内容,保留图片信息
await editMessage(message.id, {
await editMessage(message.id, {
content: content.trim(),
metadata: {
...message.metadata,
generateImage: imageUrls.length > 0 ? {
type: 'url',
images: imageUrls
} : undefined
}
})
resendMessage && handleResendUserMessage({
...message,
content: content.trim(),
metadata: {
...message.metadata,
generateImage: imageUrls.length > 0 ? {
type: 'url',
images: imageUrls
} : undefined
generateImage:
imageUrls.length > 0
? {
type: 'url',
images: imageUrls
}
: undefined
}
})
resendMessage &&
handleResendUserMessage({
...message,
content: content.trim(),
metadata: {
...message.metadata,
generateImage:
imageUrls.length > 0
? {
type: 'url',
images: imageUrls
}
: undefined
}
})
}
}, [message, editMessage, handleResendUserMessage, t])

View File

@ -0,0 +1,30 @@
import { Handle, Position } from '@xyflow/react';
import { Card, Typography } from 'antd';
import styled from 'styled-components';
interface CenterNodeProps {
data: {
label: string;
};
}
const CenterNode: React.FC<CenterNodeProps> = ({ data }) => {
return (
<NodeContainer>
<Card>
<Typography.Title level={4}>{data.label}</Typography.Title>
</Card>
<Handle type="source" position={Position.Bottom} id="b" />
<Handle type="source" position={Position.Right} id="r" />
<Handle type="source" position={Position.Left} id="l" />
<Handle type="source" position={Position.Top} id="t" />
</NodeContainer>
);
};
const NodeContainer = styled.div`
width: 150px;
text-align: center;
`;
export default CenterNode;

View File

@ -0,0 +1,261 @@
import { DeleteOutlined, EditOutlined, ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addMemoryList,
deleteMemoryList,
editMemoryList,
MemoryList,
setCurrentMemoryList,
toggleMemoryListActive
} from '@renderer/store/memory'
import { Button, Empty, Input, List, Modal, Switch, Tooltip, Typography } from 'antd'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const { Title } = Typography
const { confirm } = Modal
interface MemoryListManagerProps {
onSelectList?: (listId: string) => void
}
const MemoryListManager: React.FC<MemoryListManagerProps> = ({ 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<MemoryList | null>(null)
const [newListName, setNewListName] = useState('')
const [newListDescription, setNewListDescription] = useState('')
// 打开添加/编辑列表的模态框
const showModal = (list?: MemoryList) => {
if (list) {
setEditingList(list)
setNewListName(list.name)
setNewListDescription(list.description || '')
} else {
setEditingList(null)
setNewListName('')
setNewListDescription('')
}
setIsModalVisible(true)
}
// 处理模态框确认
const handleOk = () => {
if (!newListName.trim()) {
return // 名称不能为空
}
if (editingList) {
// 编辑现有列表
dispatch(
editMemoryList({
id: editingList.id,
name: newListName,
description: newListDescription
})
)
} else {
// 添加新列表
dispatch(
addMemoryList({
name: newListName,
description: newListDescription,
isActive: false
})
)
}
setIsModalVisible(false)
setNewListName('')
setNewListDescription('')
setEditingList(null)
}
// 处理模态框取消
const handleCancel = () => {
setIsModalVisible(false)
setNewListName('')
setNewListDescription('')
setEditingList(null)
}
// 删除记忆列表
const handleDelete = (list: MemoryList) => {
confirm({
title: t('settings.memory.confirmDeleteList'),
icon: <ExclamationCircleOutlined />,
content: t('settings.memory.confirmDeleteListContent', { name: list.name }),
okText: t('common.delete'),
okType: 'danger',
cancelText: t('common.cancel'),
onOk() {
dispatch(deleteMemoryList(list.id))
}
})
}
// 切换列表激活状态
const handleToggleActive = (list: MemoryList, checked: boolean) => {
dispatch(toggleMemoryListActive({ id: list.id, isActive: checked }))
}
// 选择列表
const handleSelectList = (listId: string) => {
dispatch(setCurrentMemoryList(listId))
if (onSelectList) {
onSelectList(listId)
}
}
return (
<Container>
<Header>
<Title level={4}>{t('settings.memory.memoryLists')}</Title>
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()}>
{t('settings.memory.addList')}
</Button>
</Header>
{memoryLists.length === 0 ? (
<Empty description={t('settings.memory.noLists')} />
) : (
<List
dataSource={memoryLists}
renderItem={(list) => (
<ListItem onClick={() => handleSelectList(list.id)} $isActive={list.id === currentListId}>
<ListItemContent>
<div>
<ListItemTitle>{list.name}</ListItemTitle>
{list.description && <ListItemDescription>{list.description}</ListItemDescription>}
</div>
<ListItemActions onClick={(e) => e.stopPropagation()}>
<Tooltip title={t('settings.memory.toggleActive')}>
<Switch
checked={list.isActive}
onChange={(checked) => handleToggleActive(list, checked)}
size="small"
/>
</Tooltip>
<Tooltip title={t('common.edit')}>
<Button
icon={<EditOutlined />}
type="text"
size="small"
onClick={(e) => {
e.stopPropagation()
showModal(list)
}}
/>
</Tooltip>
<Tooltip title={t('common.delete')}>
<Button
icon={<DeleteOutlined />}
type="text"
size="small"
danger
onClick={(e) => {
e.stopPropagation()
handleDelete(list)
}}
disabled={memoryLists.length <= 1} // 至少保留一个列表
/>
</Tooltip>
</ListItemActions>
</ListItemContent>
</ListItem>
)}
/>
)}
<Modal
title={editingList ? t('settings.memory.editList') : t('settings.memory.addList')}
open={isModalVisible}
onOk={handleOk}
onCancel={handleCancel}
okButtonProps={{ disabled: !newListName.trim() }}>
<FormItem>
<Label>{t('settings.memory.listName')}</Label>
<Input
value={newListName}
onChange={(e) => setNewListName(e.target.value)}
placeholder={t('settings.memory.listNamePlaceholder')}
maxLength={50}
/>
</FormItem>
<FormItem>
<Label>{t('settings.memory.listDescription')}</Label>
<Input.TextArea
value={newListDescription}
onChange={(e) => setNewListDescription(e.target.value)}
placeholder={t('settings.memory.listDescriptionPlaceholder')}
maxLength={200}
rows={3}
/>
</FormItem>
</Modal>
</Container>
)
}
const Container = styled.div`
margin-bottom: 20px;
`
const Header = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
`
const ListItem = styled.div<{ $isActive: boolean }>`
padding: 12px;
border-radius: 4px;
cursor: pointer;
background-color: ${(props) => (props.$isActive ? 'var(--color-bg-2)' : 'transparent')};
border: 1px solid ${(props) => (props.$isActive ? 'var(--color-primary)' : 'var(--color-border)')};
margin-bottom: 8px;
&:hover {
background-color: var(--color-bg-2);
}
`
const ListItemContent = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`
const ListItemTitle = styled.div`
font-weight: 500;
margin-bottom: 4px;
`
const ListItemDescription = styled.div`
font-size: 12px;
color: var(--color-text-3);
`
const ListItemActions = styled.div`
display: flex;
gap: 8px;
align-items: center;
`
const FormItem = styled.div`
margin-bottom: 16px;
`
const Label = styled.div`
margin-bottom: 8px;
font-weight: 500;
`
export default MemoryListManager

View File

@ -0,0 +1,220 @@
import { Memory } from '@renderer/store/memory'
import { applyNodeChanges, Background, Controls, MiniMap, ReactFlow, ReactFlowProvider } from '@xyflow/react'
import { Edge, Node, NodeTypes } from '@xyflow/react'
import { Empty } from 'antd'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import CenterNode from './CenterNode'
import MemoryNode from './MemoryNode'
interface MemoryMindMapProps {
memories: Memory[]
onEditMemory: (id: string) => void
onDeleteMemory: (id: string) => void
}
const MemoryMindMap: React.FC<MemoryMindMapProps> = ({ memories, onEditMemory, onDeleteMemory }) => {
const { t } = useTranslation()
const [nodes, setNodes] = useState<Node[]>([])
const [edges, setEdges] = useState<Edge[]>([])
// 处理节点拖动事件
const onNodesChange = useCallback((changes) => {
setNodes((nds) => {
// 中心节点不允许拖动
const filteredChanges = changes.filter((change) => {
if (change.type === 'position' && change.id === 'center') {
return false
}
return true
})
return applyNodeChanges(filteredChanges, nds)
})
}, [])
// 定义节点类型
const nodeTypes = useMemo<NodeTypes>(
() => ({
memoryNode: MemoryNode,
centerNode: CenterNode
}),
[]
)
// 转换记忆为节点和边
useMemo(() => {
if (memories.length === 0) {
setNodes([])
setEdges([])
return
}
// 创建中心节点
const centerNode: Node = {
id: 'center',
type: 'centerNode',
position: { x: 0, y: 0 },
data: { label: t('settings.memory.centerNodeLabel') },
draggable: false // 中心节点不允许拖动
}
// 计算合适的半径,确保节点不会太拥挤
const calculateRadius = () => {
const baseRadius = 300
if (memories.length <= 4) return baseRadius
if (memories.length <= 8) return baseRadius + 50
return baseRadius + 100
}
// 按分类组织记忆
const categorizedMemories: Record<string, Memory[]> = {}
// 将记忆分组
memories.forEach((memory) => {
const category = memory.category || t('settings.memory.uncategorized')
if (!categorizedMemories[category]) {
categorizedMemories[category] = []
}
categorizedMemories[category].push(memory)
})
// 创建记忆节点和边
const memoryNodes: Node[] = []
let categoryIndex = 0
const categories = Object.keys(categorizedMemories)
// 为每个分类创建节点
categories.forEach((category) => {
const categoryMemories = categorizedMemories[category]
const categoryAngle = (categoryIndex / categories.length) * 2 * Math.PI
// const categoryRadius = calculateRadius() * 0.5 // 分类节点距离中心较近
// 分类内的记忆节点
categoryMemories.forEach((memory, memIndex) => {
// 计算节点位置(围绕分类的圆形布局)
const memAngle = categoryAngle + ((memIndex / categoryMemories.length - 0.5) * Math.PI) / 2
const memRadius = calculateRadius()
const x = Math.cos(memAngle) * memRadius
const y = Math.sin(memAngle) * memRadius
memoryNodes.push({
id: memory.id,
type: 'memoryNode',
position: { x, y },
data: {
memory,
onEdit: onEditMemory,
onDelete: onDeleteMemory
},
draggable: true
})
})
categoryIndex++
})
// 创建从中心到每个记忆的边
const newEdges: Edge[] = memories.map((memory, index) => {
// 根据节点位置决定使用哪个连接点
const angle = (index / memories.length) * 2 * Math.PI
let sourceHandle = 'b' // 默认使用底部连接点
if (angle > Math.PI * 0.25 && angle < Math.PI * 0.75) {
sourceHandle = 't' // 上部
} else if (angle >= Math.PI * 0.75 && angle < Math.PI * 1.25) {
sourceHandle = 'r' // 右侧
} else if (angle >= Math.PI * 1.25 && angle < Math.PI * 1.75) {
sourceHandle = 'b' // 底部
} else {
sourceHandle = 'l' // 左侧
}
return {
id: `center-${memory.id}`,
source: 'center',
sourceHandle,
target: memory.id,
type: 'smoothstep',
animated: true
}
})
setNodes([centerNode, ...memoryNodes])
setEdges(newEdges)
}, [memories, onEditMemory, onDeleteMemory, t])
if (memories.length === 0) {
return <Empty description={t('settings.memory.noMemories')} />
}
return (
<Container>
<ReactFlowProvider>
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
fitView
minZoom={0.5}
maxZoom={2}
defaultViewport={{ x: 0, y: 0, zoom: 1 }}
defaultEdgeOptions={{
animated: true
}}
nodesDraggable={true}
nodesConnectable={false}
elementsSelectable={true}>
<Controls position="bottom-left" />
<MiniMap
nodeColor={(node) => {
return node.id === 'center' ? '#1890ff' : '#91d5ff'
}}
/>
<Background color="#aaa" gap={16} />
</ReactFlow>
</ReactFlowProvider>
</Container>
)
}
const Container = styled.div`
width: 100%;
height: 100%;
border: 1px solid var(--color-border);
border-radius: 4px;
position: relative;
/* 只增强选中的连接线样式 */
.react-flow__edge.selected {
.react-flow__edge-path {
stroke: #f5222d !important;
stroke-width: 4px !important;
}
}
/* 正常连接线样式 */
.react-flow__edge:not(.selected) {
.react-flow__edge-path {
stroke: #1890ff;
stroke-width: 1.5px;
stroke-dasharray: none;
}
}
/* 鼠标悬停在节点上时的样式 */
.react-flow__node:hover {
cursor: move;
}
/* 控制按钮样式 */
.react-flow__controls {
bottom: 10px;
left: 10px;
top: auto;
}
`
export default MemoryMindMap

View File

@ -0,0 +1,77 @@
import { DeleteOutlined, EditOutlined, TagOutlined } from '@ant-design/icons';
import { Memory } from '@renderer/store/memory';
import { Button, Card, Tag, Tooltip, Typography } from 'antd';
import { Handle, Position } from '@xyflow/react';
import styled from 'styled-components';
interface MemoryNodeProps {
data: {
memory: Memory;
onEdit: (id: string) => void;
onDelete: (id: string) => void;
};
}
const MemoryNode: React.FC<MemoryNodeProps> = ({ data }) => {
const { memory, onEdit, onDelete } = data;
return (
<NodeContainer>
<Handle type="target" position={Position.Top} />
<Card
size="small"
title={
<div>
{memory.category && (
<Tag color="blue" icon={<TagOutlined />} style={{ marginBottom: 4 }}>
{memory.category}
</Tag>
)}
<Typography.Text ellipsis style={{ width: 180, display: 'block' }}>
{memory.content}
</Typography.Text>
</div>
}
extra={
<div>
<Tooltip title="编辑">
<Button
icon={<EditOutlined />}
type="text"
size="small"
onClick={() => onEdit(memory.id)}
/>
</Tooltip>
<Tooltip title="删除">
<Button
icon={<DeleteOutlined />}
type="text"
danger
size="small"
onClick={() => onDelete(memory.id)}
/>
</Tooltip>
</div>
}
>
<MemoryMeta>
<span>{new Date(memory.createdAt).toLocaleString()}</span>
{memory.source && <span>{memory.source}</span>}
</MemoryMeta>
</Card>
</NodeContainer>
);
};
const NodeContainer = styled.div`
width: 220px;
`;
const MemoryMeta = styled.div`
display: flex;
flex-direction: column;
font-size: 12px;
color: var(--color-text-secondary);
`;
export default MemoryNode;

View File

@ -0,0 +1,115 @@
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
import { addShortMemoryItem } from '@renderer/services/MemoryService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { deleteShortMemory, setShortMemoryActive } from '@renderer/store/memory'
import { Button, Empty, Input, List, Modal, Switch, Tooltip, Typography } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
const { Title } = Typography
const { confirm } = Modal
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 = () => {
if (newMemoryContent.trim() && currentTopicId) {
addShortMemoryItem(newMemoryContent.trim(), currentTopicId)
setNewMemoryContent('') // 清空输入框
}
}
// 删除短记忆
const handleDeleteMemory = (id: string) => {
confirm({
title: t('settings.memory.confirmDelete'),
icon: <ExclamationCircleOutlined />,
content: t('settings.memory.confirmDeleteContent'),
onOk() {
dispatch(deleteShortMemory(id))
}
})
}
return (
<div className="short-memory-manager">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<Title level={4}>{t('settings.memory.shortMemory')}</Title>
<Tooltip title={t('settings.memory.toggleShortMemoryActive')}>
<Switch checked={shortMemoryActive} onChange={handleToggleActive} />
</Tooltip>
</div>
<div style={{ marginBottom: 16 }}>
<Input.TextArea
value={newMemoryContent}
onChange={(e) => setNewMemoryContent(e.target.value)}
placeholder={t('settings.memory.addShortMemoryPlaceholder')}
autoSize={{ minRows: 2, maxRows: 4 }}
disabled={!shortMemoryActive || !currentTopicId}
/>
<Button
type="primary"
onClick={handleAddMemory}
style={{ marginTop: 8 }}
disabled={!shortMemoryActive || !newMemoryContent.trim() || !currentTopicId}>
{t('settings.memory.addShortMemory')}
</Button>
</div>
<div className="short-memories-list">
{shortMemories.length > 0 ? (
<List
itemLayout="horizontal"
dataSource={shortMemories}
renderItem={(memory) => (
<List.Item
actions={[
<Tooltip title={t('settings.memory.delete')} key="delete">
<Button
icon={<DeleteOutlined />}
onClick={() => handleDeleteMemory(memory.id)}
type="text"
danger
/>
</Tooltip>
]}>
<List.Item.Meta
title={<div style={{ wordBreak: 'break-word' }}>{memory.content}</div>}
description={new Date(memory.createdAt).toLocaleString()}
/>
</List.Item>
)}
/>
) : (
<Empty
description={!currentTopicId ? t('settings.memory.noCurrentTopic') : t('settings.memory.noShortMemories')}
/>
)}
</div>
</div>
)
}
export default ShortMemoryManager

View File

@ -0,0 +1,535 @@
import {
AppstoreOutlined,
DeleteOutlined,
EditOutlined,
PlusOutlined,
SearchOutlined,
UnorderedListOutlined
} from '@ant-design/icons'
import { useTheme } from '@renderer/context/ThemeProvider'
import { TopicManager } from '@renderer/hooks/useTopic'
import { useMemoryService } from '@renderer/services/MemoryService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addMemory,
clearMemories,
deleteMemory,
editMemory,
setAnalyzeModel,
setAutoAnalyze,
setMemoryActive
} from '@renderer/store/memory'
import { Topic } from '@renderer/types'
import { Button, Empty, Input, List, message, Modal, Radio, Select, Switch, Tag, Tooltip } from 'antd'
import { FC, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import {
SettingContainer,
SettingDivider,
SettingGroup,
SettingHelpText,
SettingRow,
SettingRowTitle,
SettingTitle
} from '..'
import MemoryListManager from './MemoryListManager'
import MemoryMindMap from './MemoryMindMap'
import ShortMemoryManager from './ShortMemoryManager'
const MemorySettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { analyzeAndAddMemories } = useMemoryService()
// 从 Redux 获取记忆状态
const memories = useAppSelector((state) => state.memory?.memories || [])
const memoryLists = useAppSelector((state) => state.memory?.memoryLists || [])
const currentListId = useAppSelector((state) => state.memory?.currentListId || null)
const isActive = useAppSelector((state) => state.memory?.isActive || false)
const autoAnalyze = useAppSelector((state) => state.memory?.autoAnalyze || false)
const analyzeModel = useAppSelector((state) => state.memory?.analyzeModel || null)
// 从 Redux 获取所有模型,不仅仅是可用的模型
const providers = useAppSelector((state) => state.llm?.providers || [])
// 使用 useMemo 缓存模型数组,避免不必要的重新渲染
const models = useMemo(() => {
// 获取所有模型,不过滤可用性
return providers.flatMap((provider) => provider.models || [])
}, [providers])
// 使用 useMemo 缓存模型选项数组,避免不必要的重新渲染
const modelOptions = useMemo(() => {
if (models.length > 0) {
return models.map((model) => ({
label: model.name,
value: model.id
}))
} else {
return [
// 默认模型选项
{ label: 'GPT-3.5 Turbo', value: 'gpt-3.5-turbo' },
{ label: 'GPT-4', value: 'gpt-4' },
{ label: 'Claude 3 Opus', value: 'claude-3-opus-20240229' },
{ label: 'Claude 3 Sonnet', value: 'claude-3-sonnet-20240229' },
{ label: 'Claude 3 Haiku', value: 'claude-3-haiku-20240307' }
]
}
}, [models])
// 如果没有模型,添加一个默认模型
useEffect(() => {
if (models.length === 0 && !analyzeModel) {
// 设置一个默认模型 ID
dispatch(setAnalyzeModel('gpt-3.5-turbo'))
}
}, [models, analyzeModel, dispatch])
// 获取助手列表,用于话题信息补充
const assistants = useAppSelector((state) => state.assistants?.assistants || [])
// 加载所有话题
useEffect(() => {
const loadTopics = async () => {
try {
// 从数据库获取所有话题
const allTopics = await TopicManager.getAllTopics()
if (allTopics && allTopics.length > 0) {
// 获取话题的完整信息
const fullTopics = allTopics.map((dbTopic) => {
// 尝试从 Redux 中找到完整的话题信息
for (const assistant of assistants) {
if (assistant.topics) {
const topic = assistant.topics.find((t) => t.id === dbTopic.id)
if (topic) return topic
}
}
// 如果找不到,返回一个基本的话题对象
return {
id: dbTopic.id,
assistantId: '',
name: `话题 ${dbTopic.id.substring(0, 8)}`,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
messages: dbTopic.messages || []
}
})
// 按更新时间排序,最新的在前
const sortedTopics = fullTopics.sort((a, b) => {
return new Date(b.updatedAt || b.createdAt).getTime() - new Date(a.updatedAt || a.createdAt).getTime()
})
setTopics(sortedTopics)
}
} catch (error) {
console.error('Failed to load topics:', error)
}
}
loadTopics()
}, [assistants])
// 本地状态
const [isAddModalVisible, setIsAddModalVisible] = useState(false)
const [isEditModalVisible, setIsEditModalVisible] = useState(false)
const [isClearModalVisible, setIsClearModalVisible] = useState(false)
const [newMemory, setNewMemory] = useState('')
const [editingMemory, setEditingMemory] = useState<{ id: string; content: string } | null>(null)
const [viewMode, setViewMode] = useState<'list' | 'mindmap'>('list')
const [topics, setTopics] = useState<Topic[]>([])
const [selectedTopicId, setSelectedTopicId] = useState<string>('')
const [categoryFilter, setCategoryFilter] = useState<string | null>(null)
// 处理添加记忆
const handleAddMemory = () => {
if (newMemory.trim()) {
dispatch(
addMemory({
content: newMemory.trim(),
listId: currentListId || undefined
})
)
setNewMemory('')
setIsAddModalVisible(false)
message.success(t('settings.memory.addSuccess'))
}
}
// 处理编辑记忆
const handleEditMemory = () => {
if (editingMemory && editingMemory.content.trim()) {
dispatch(
editMemory({
id: editingMemory.id,
content: editingMemory.content.trim()
})
)
setEditingMemory(null)
setIsEditModalVisible(false)
message.success(t('settings.memory.editSuccess'))
}
}
// 处理删除记忆
const handleDeleteMemory = (id: string) => {
dispatch(deleteMemory(id))
message.success(t('settings.memory.deleteSuccess'))
}
// 处理清空记忆
const handleClearMemories = () => {
dispatch(clearMemories(currentListId || undefined))
setIsClearModalVisible(false)
message.success(t('settings.memory.clearSuccess'))
}
// 处理切换记忆功能
const handleToggleMemory = (checked: boolean) => {
dispatch(setMemoryActive(checked))
}
// 处理切换自动分析
const handleToggleAutoAnalyze = (checked: boolean) => {
dispatch(setAutoAnalyze(checked))
}
// 处理选择分析模型
const handleSelectModel = (modelId: string) => {
dispatch(setAnalyzeModel(modelId))
}
// 手动触发分析
const handleManualAnalyze = () => {
if (isActive && analyzeModel) {
message.info(t('settings.memory.startingAnalysis') || '开始分析...')
// 如果选择了话题,则分析选定的话题,否则分析当前话题
analyzeAndAddMemories(selectedTopicId || undefined)
} else {
message.warning(t('settings.memory.cannotAnalyze') || '无法分析,请检查设置')
}
}
return (
<SettingContainer theme={theme}>
<SettingGroup theme={theme}>
<SettingTitle>{t('settings.memory.title')}</SettingTitle>
<SettingHelpText>{t('settings.memory.description')}</SettingHelpText>
<SettingDivider />
{/* 记忆功能开关 */}
<SettingRow>
<SettingRowTitle>{t('settings.memory.enableMemory')}</SettingRowTitle>
<Switch checked={isActive} onChange={handleToggleMemory} />
</SettingRow>
{/* 自动分析开关 */}
<SettingRow>
<SettingRowTitle>{t('settings.memory.enableAutoAnalyze')}</SettingRowTitle>
<Switch checked={autoAnalyze} onChange={handleToggleAutoAnalyze} disabled={!isActive} />
</SettingRow>
{/* 分析模型选择 */}
{autoAnalyze && isActive && (
<SettingRow>
<SettingRowTitle>{t('settings.memory.analyzeModel')}</SettingRowTitle>
<Select
style={{ width: 250 }}
value={analyzeModel}
onChange={handleSelectModel}
placeholder={t('settings.memory.selectModel')}
options={modelOptions}
/>
</SettingRow>
)}
{/* 话题选择 */}
{isActive && (
<SettingRow>
<SettingRowTitle>{t('settings.memory.selectTopic') || '选择话题'}</SettingRowTitle>
<Select
style={{ width: 350 }}
value={selectedTopicId}
onChange={(value) => 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}
/>
</SettingRow>
)}
{/* 手动分析按钮 */}
{isActive && (
<SettingRow>
<SettingRowTitle>{t('settings.memory.manualAnalyze') || '手动分析'}</SettingRowTitle>
<Button onClick={handleManualAnalyze} disabled={!analyzeModel} icon={<SearchOutlined />}>
{t('settings.memory.analyzeNow') || '立即分析'}
</Button>
</SettingRow>
)}
<SettingDivider />
{/* 短记忆管理器 */}
<ShortMemoryManager />
<SettingDivider />
{/* 记忆列表管理器 */}
<MemoryListManager
onSelectList={() => {
// 当选择了一个记忆列表时,重置分类筛选器
setCategoryFilter(null)
}}
/>
<SettingDivider />
{/* 记忆列表标题和操作按钮 */}
<MemoryListHeader>
<SettingTitle>{t('settings.memory.memoriesList')}</SettingTitle>
<ButtonGroup>
<Radio.Group
value={viewMode}
onChange={(e) => setViewMode(e.target.value)}
buttonStyle="solid"
style={{ marginRight: 16 }}>
<Radio.Button value="list">
<UnorderedListOutlined /> {t('settings.memory.listView')}
</Radio.Button>
<Radio.Button value="mindmap">
<AppstoreOutlined /> {t('settings.memory.mindmapView')}
</Radio.Button>
</Radio.Group>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setIsAddModalVisible(true)}
disabled={!isActive}>
{t('settings.memory.addMemory')}
</Button>
<Button danger onClick={() => setIsClearModalVisible(true)} disabled={!isActive || memories.length === 0}>
{t('settings.memory.clearAll')}
</Button>
</ButtonGroup>
</MemoryListHeader>
{/* 分类筛选器 */}
{memories.length > 0 && (
<CategoryFilterContainer>
<span>{t('settings.memory.filterByCategory') || '按分类筛选:'}</span>
<div>
<Tag
color={categoryFilter === null ? 'blue' : undefined}
style={{ cursor: 'pointer', marginRight: 8 }}
onClick={() => setCategoryFilter(null)}>
{t('settings.memory.allCategories') || '全部'}
</Tag>
{Array.from(new Set(memories.filter((m) => m.category).map((m) => m.category))).map((category) => (
<Tag
key={category}
color={categoryFilter === category ? 'blue' : undefined}
style={{ cursor: 'pointer', marginRight: 8 }}
onClick={() => setCategoryFilter(category || null)}>
{category || t('settings.memory.uncategorized') || '未分类'}
</Tag>
))}
</div>
</CategoryFilterContainer>
)}
{/* 记忆列表 */}
<MemoryListContainer>
{viewMode === 'list' ? (
memories.length > 0 ? (
<List
itemLayout="horizontal"
style={{ minHeight: '700px' }}
dataSource={memories
.filter((memory) => (currentListId ? memory.listId === currentListId : true))
.filter((memory) => categoryFilter === null || memory.category === categoryFilter)}
renderItem={(memory) => (
<List.Item
actions={[
<Tooltip key="edit" title={t('common.edit')}>
<Button
icon={<EditOutlined />}
type="text"
onClick={() => {
setEditingMemory({ id: memory.id, content: memory.content })
setIsEditModalVisible(true)
}}
disabled={!isActive}
/>
</Tooltip>,
<Tooltip key="delete" title={t('common.delete')}>
<Button
icon={<DeleteOutlined />}
type="text"
danger
onClick={() => handleDeleteMemory(memory.id)}
disabled={!isActive}
/>
</Tooltip>
]}>
<List.Item.Meta
title={
<div>
{memory.category && (
<Tag color="blue" style={{ marginRight: 8 }}>
{memory.category}
</Tag>
)}
{memory.content}
</div>
}
description={
<MemoryItemMeta>
<span>{new Date(memory.createdAt).toLocaleString()}</span>
{memory.source && <span>{memory.source}</span>}
</MemoryItemMeta>
}
/>
</List.Item>
)}
/>
) : (
<Empty description={t('settings.memory.noMemories')} />
)
) : (
<MemoryMindMapContainer>
<MemoryMindMap
memories={memories.filter((memory) => (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}
/>
</MemoryMindMapContainer>
)}
</MemoryListContainer>
</SettingGroup>
{/* 添加记忆对话框 */}
<Modal
title={t('settings.memory.addMemory')}
open={isAddModalVisible}
onOk={handleAddMemory}
onCancel={() => setIsAddModalVisible(false)}
okButtonProps={{ disabled: !newMemory.trim() }}>
<Input.TextArea
rows={4}
value={newMemory}
onChange={(e) => setNewMemory(e.target.value)}
placeholder={t('settings.memory.memoryPlaceholder')}
/>
</Modal>
{/* 编辑记忆对话框 */}
<Modal
title={t('settings.memory.editMemory')}
open={isEditModalVisible}
onOk={handleEditMemory}
onCancel={() => setIsEditModalVisible(false)}
okButtonProps={{ disabled: !editingMemory?.content.trim() }}>
<Input.TextArea
rows={4}
value={editingMemory?.content || ''}
onChange={(e) => setEditingMemory((prev) => (prev ? { ...prev, content: e.target.value } : null))}
placeholder={t('settings.memory.memoryPlaceholder')}
/>
</Modal>
{/* 清空记忆确认对话框 */}
<Modal
title={t('settings.memory.clearConfirmTitle')}
open={isClearModalVisible}
onOk={handleClearMemories}
onCancel={() => setIsClearModalVisible(false)}
okButtonProps={{ danger: true }}
okText={t('common.confirm')}
cancelText={t('common.cancel')}>
<p>
{currentListId
? t('settings.memory.clearConfirmContentList', {
name: memoryLists.find((list) => list.id === currentListId)?.name || ''
})
: t('settings.memory.clearConfirmContent')}
</p>
</Modal>
</SettingContainer>
)
}
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: 800px;
overflow-y: auto;
border: 1px solid var(--color-border);
border-radius: 4px;
padding: 8px;
/* 确保容器高度可以自适应 */
&:has(.ant-list) {
height: auto;
}
`
const MemoryItemMeta = styled.div`
display: flex;
justify-content: space-between;
color: var(--color-text-3);
font-size: 12px;
`
const MemoryMindMapContainer = styled.div`
width: 100%;
height: 800px;
margin-bottom: 20px;
display: flex;
flex-direction: column;
`
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;
}
`
export default MemorySettings

View File

@ -2,6 +2,7 @@ import {
AppstoreOutlined,
CloudOutlined,
CodeOutlined,
ExperimentOutlined,
GlobalOutlined,
InfoCircleOutlined,
LayoutOutlined,
@ -26,6 +27,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'
@ -73,6 +75,12 @@ const SettingsPage: FC = () => {
{t('settings.mcp.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/memory">
<MenuItem className={isRoute('/settings/memory')}>
<ExperimentOutlined />
{t('settings.memory.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/general">
<MenuItem className={isRoute('/settings/general')}>
<SettingOutlined />
@ -130,6 +138,7 @@ const SettingsPage: FC = () => {
<Route path="model" element={<ModelSettings />} />
<Route path="web-search" element={<WebSearchSettings />} />
<Route path="mcp/*" element={<MCPSettings />} />
<Route path="memory" element={<MemorySettings />} />
<Route path="general" element={<GeneralSettings />} />
<Route path="display" element={<DisplaySettings />} />
{showMiniAppSettings && <Route path="miniapps" element={<MiniAppSettings />} />}

View File

@ -7,7 +7,8 @@ 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;
@ -16,7 +17,17 @@ export const SettingContainer = styled.div<{ theme?: ThemeMode }>`
background: ${(props) => (props.theme === 'dark' ? 'transparent' : 'var(--color-background-soft)')};
&::-webkit-scrollbar {
display: none;
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
`

View File

@ -10,12 +10,12 @@ 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 { parseAndCallTools } from '@renderer/utils/mcp-tools'
import { buildSystemPrompt } from '@renderer/utils/prompt'
import store from '@renderer/store'
import { getActiveServers } from '@renderer/store/mcp'
import { first, flatten, sum, takeRight } from 'lodash'
import OpenAI from 'openai'
@ -475,14 +475,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<string> {
const model = getDefaultModel()
public async generateText({
prompt,
content,
modelId
}: {
prompt: string
content: string
modelId?: string
}): Promise<string> {
// 使用指定的模型或默认模型
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 = 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: [

View File

@ -37,7 +37,7 @@ export default abstract class BaseProvider {
abstract summaries(messages: Message[], assistant: Assistant): Promise<string>
abstract summaryForSearch(messages: Message[], assistant: Assistant): Promise<string | null>
abstract suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]>
abstract generateText({ prompt, content }: { prompt: string; content: string }): Promise<string>
abstract generateText({ prompt, content, modelId }: { prompt: string; content: string; modelId?: string }): Promise<string>
abstract check(model: Model): Promise<{ valid: boolean; error: Error | null }>
abstract models(): Promise<OpenAI.Models.Model[]>
abstract generateImage(params: GenerateImageParams): Promise<string[]>

View File

@ -24,14 +24,14 @@ import { getStoreSetting } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService'
import { EVENT_NAMES } from '@renderer/services/EventService'
import store from '@renderer/store'
import { getActiveServers } from '@renderer/store/mcp'
import {
filterContextMessages,
filterEmptyMessages,
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 { parseAndCallTools } from '@renderer/utils/mcp-tools'
@ -230,7 +230,11 @@ export default class GeminiProvider extends BaseProvider {
let systemInstruction = assistant.prompt
if (mcpTools && mcpTools.length > 0) {
systemInstruction = await buildSystemPrompt(assistant.prompt || '', mcpTools, getActiveServers(store.getState()))
systemInstruction = await buildSystemPrompt(
assistant.prompt || '',
mcpTools,
getActiveServers(store.getState())
)
}
// const tools = mcpToolsToGeminiTools(mcpTools)
@ -466,11 +470,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<string> {
const model = getDefaultModel()
const systemMessage = { role: 'system', content: prompt }
public async generateText({
prompt,
content,
modelId
}: {
prompt: string
content: string
modelId?: string
}): Promise<string> {
// 使用指定的模型或默认模型
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 = 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(
{
@ -482,7 +515,7 @@ export default class GeminiProvider extends BaseProvider {
const chat = await geminiModel.startChat()
const messageContent = isGemmaModel(model)
? `<start_of_turn>user\n${prompt}<end_of_turn>\n<start_of_turn>user\n${content}<end_of_turn>`
? `<start_of_turn>user\n${enhancedPrompt}<end_of_turn>\n<start_of_turn>user\n${content}<end_of_turn>`
: content
const { response } = await chat.sendMessage(messageContent)

View File

@ -311,7 +311,15 @@ 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')
const enhancedPrompt = applyMemoriesToPrompt(assistant.prompt || '')
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',
@ -319,7 +327,11 @@ export default class OpenAIProvider extends BaseProvider {
}
}
if (mcpTools && mcpTools.length > 0) {
systemMessage.content = await buildSystemPrompt(systemMessage.content || '', mcpTools, getActiveServers(store.getState()))
systemMessage.content = await buildSystemPrompt(
systemMessage.content || '',
mcpTools,
getActiveServers(store.getState())
)
}
const userMessages: ChatCompletionMessageParam[] = []
@ -540,12 +552,21 @@ 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')
const enhancedPrompt = applyMemoriesToPrompt(assistant.prompt || '')
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)
@ -626,9 +647,23 @@ 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')
// 使用双重类型断言强制转换类型
const enhancedPrompt = applyMemoriesToPrompt(originalPrompt as string) 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 = {
@ -697,18 +732,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<string> {
const model = getDefaultModel()
public async generateText({
prompt,
content,
modelId
}: {
prompt: string
content: string
modelId?: string
}): Promise<string> {
// 使用指定的模型或默认模型
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 = 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 }
]
})
@ -790,7 +853,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',

View File

@ -88,8 +88,8 @@ export default class AiProvider {
return this.sdk.suggestions(messages, assistant)
}
public async generateText({ prompt, content }: { prompt: string; content: string }): Promise<string> {
return this.sdk.generateText({ prompt, content })
public async generateText({ prompt, content, modelId }: { prompt: string; content: string; modelId?: string }): Promise<string> {
return this.sdk.generateText({ prompt, content, modelId })
}
public async check(model: Model): Promise<{ valid: boolean; error: Error | null }> {

View File

@ -333,8 +333,28 @@ export async function fetchSearchSummary({ messages, assistant }: { messages: Me
}
}
export async function fetchGenerate({ prompt, content }: { prompt: string; content: string }): Promise<string> {
const model = getDefaultModel()
export async function fetchGenerate({
prompt,
content,
modelId
}: {
prompt: string
content: string
modelId?: string
}): Promise<string> {
// 使用指定的模型或默认模型
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)) {
@ -344,8 +364,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 ''
}
}

View File

@ -0,0 +1,382 @@
// 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
import { addMemory, addShortMemory, setAnalyzing } from '@renderer/store/memory'
import { useCallback, useEffect, useRef } from 'react' // Add useRef back
// 分析对话内容并提取重要信息
const analyzeConversation = async (
conversation: string,
modelId: string
): Promise<Array<{ content: string; category: string }>> => {
try {
// 构建分析提示词
const prompt = `
类别: 信息内容
-
-
-
-
-
:
${conversation}
`
console.log(`[Memory Analysis] Analyzing conversation using model: ${modelId}`)
// 获取模型和提供者
// 检查模型是否存在
const model = store
.getState()
.llm.providers // Access store directly
.flatMap((provider) => provider.models)
.find((model) => model.id === modelId)
if (!model) {
console.error(`[Memory Analysis] Model ${modelId} not found`)
return []
}
// 创建一个简单的助手对象或直接传递必要参数给API调用
// 注意AiProvider的generateText可能不需要完整的assistant对象结构
// 根据 AiProvider.generateText 的实际需要调整参数
console.log('[Memory Analysis] Calling AI.generateText...')
// 使用指定的模型进行分析
const result = await fetchGenerate({
prompt: prompt,
content: conversation,
modelId: modelId // 传递指定的模型 ID
})
console.log('[Memory Analysis] AI.generateText response:', result)
// 处理响应
if (!result) {
// Check if result is null or undefined
console.log('[Memory Analysis] No 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()
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
// e.g., 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.
// 记忆服务钩子 - 重构版
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)
// 使用 useCallback 定义分析函数,但减少依赖项
// 增加可选的 topicId 参数,允许分析指定的话题
const analyzeAndAddMemories = useCallback(
async (topicId?: string) => {
// 在函数执行时获取最新状态
const currentState = store.getState() // Use imported store
const memoryState = currentState.memory || {}
const messagesState = currentState.messages || {}
// 检查isAnalyzing状态是否卡住超过5分钟
if (memoryState.isAnalyzing && memoryState.lastAnalyzeTime) {
const now = Date.now()
const analyzeTime = memoryState.lastAnalyzeTime
if (now - analyzeTime > 5 * 60 * 1000) {
// 5分钟超时
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)
}
}
}
const latestConversation = messages.map((msg) => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n')
if (!latestConversation) {
console.log('[Memory Analysis] No conversation content to analyze.')
return
}
try {
dispatch(setAnalyzing(true))
console.log('[Memory Analysis] Starting analysis...')
console.log(`[Memory Analysis] Analyzing topic: ${targetTopicId}`)
console.log('[Memory Analysis] Conversation length:', latestConversation.length)
// 调用分析函数 (仍然是模拟的)
const memories = await analyzeConversation(latestConversation, memoryState.analyzeModel!)
console.log('[Memory Analysis] Analysis complete. Memories extracted:', memories)
// 添加提取的记忆
if (memories && memories.length > 0) {
// 智能去重使用AI模型检查语义相似的记忆
const existingMemories = store.getState().memory?.memories || []
// 首先进行简单的字符串匹配去重
const newMemories = memories.filter((memory) => {
return !existingMemories.some((m) => m.content === memory.content)
})
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
dispatch(
addMemory({
content: memory.content,
source: '自动分析',
category: memory.category,
listId: currentListId
})
)
console.log(
`[Memory Analysis] Added new memory: "${memory.content}" (${memory.category}) to list ${currentListId}`
)
}
console.log(`[Memory Analysis] Processed ${memories.length} potential memories, added ${newMemories.length}.`)
} else {
console.log('[Memory Analysis] No new memories extracted.')
}
} 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])
// 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])
// 返回分析函数以便在MemoryProvider中使用
return { analyzeAndAddMemories }
}
// 手动添加记忆
export const addMemoryItem = (content: string, listId?: string, category?: string) => {
// Use imported store directly
store.dispatch(
addMemory({
content,
source: '手动添加',
listId,
category
})
)
}
// 手动添加短记忆
export const addShortMemoryItem = (content: string, topicId: string) => {
// Use imported store directly
store.dispatch(
addShortMemory({
content,
topicId
})
)
}
// 将记忆应用到系统提示词
import { persistor } from '@renderer/store' // Import persistor
export const applyMemoriesToPrompt = (systemPrompt: string): string => {
// 检查持久化状态是否已加载完成
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 } = state.memory ||
{ isActive: false, memories: [], memoryLists: [], shortMemoryActive: false, shortMemories: [] }
// 获取当前话题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
})
let result = systemPrompt
let hasContent = false
// 处理短记忆
if (shortMemoryActive && shortMemories && shortMemories.length > 0 && currentTopicId) {
// 获取当前话题的短记忆
const topicShortMemories = shortMemories.filter(memory => memory.topicId === currentTopicId)
if (topicShortMemories.length > 0) {
const shortMemoryPrompt = topicShortMemories.map(memory => `- ${memory.content}`).join('\n')
console.log('[Memory] Short memory prompt:', shortMemoryPrompt)
// 添加短记忆到提示词
result = `${result}\n\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) {
// 只获取激活列表中的记忆
const activeMemories = memories.filter((memory) => activeListIds.includes(memory.listId))
if (activeMemories.length > 0) {
// 按列表分组构建记忆提示词
let memoryPrompt = ''
// 如果只有一个激活列表,直接列出记忆
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用户的长期记忆:\n${memoryPrompt}`
hasContent = true
}
}
}
if (hasContent) {
console.log('[Memory] Final prompt with memories applied')
} else {
console.log('[Memory] No memories to apply')
}
return result
}

View File

@ -10,6 +10,7 @@ import copilot from './copilot'
import knowledge from './knowledge'
import llm from './llm'
import mcp from './mcp'
import memory from './memory'
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
})

View File

@ -102,7 +102,8 @@ export const builtinMCPServers: MCPServer[] = [
id: nanoid(),
name: '@cherry/simpleremember',
type: 'inMemory',
description: '自动记忆工具,功能跟上面的记忆工具差不多。这个记忆会自动应用到对话中,无需显式调用。适合记住用户偏好、项目背景等长期有用信息.可以跨对话。',
description:
'自动记忆工具,功能跟上面的记忆工具差不多。这个记忆会自动应用到对话中,无需显式调用。适合记住用户偏好、项目背景等长期有用信息.可以跨对话。',
isActive: true
}
]

View File

@ -0,0 +1,363 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
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
}
// 短记忆项接口
export interface ShortMemory {
id: string
content: string
createdAt: string
topicId: string // 关联的对话话题ID
}
export interface MemoryState {
memoryLists: MemoryList[] // 记忆列表
memories: Memory[] // 所有记忆项
shortMemories: ShortMemory[] // 短记忆项
currentListId: string | null // 当前选中的记忆列表ID
isActive: boolean // 记忆功能是否激活
shortMemoryActive: boolean // 短记忆功能是否激活
autoAnalyze: boolean // 是否自动分析
analyzeModel: string | null // 用于分析的模型ID
lastAnalyzeTime: number | null // 上次分析时间
isAnalyzing: boolean // 是否正在分析
}
// 创建默认记忆列表
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,
analyzeModel: 'gpt-3.5-turbo', // 设置默认模型
lastAnalyzeTime: null,
isAnalyzing: false
}
const memorySlice = createSlice({
name: 'memory',
initialState,
reducers: {
// 添加新记忆
addMemory: (
state,
action: PayloadAction<{ content: string; source?: string; category?: string; listId?: 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
}
// 确保 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<string>) => {
// 确保 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<boolean>) => {
state.isActive = action.payload
},
// 设置是否自动分析
setAutoAnalyze: (state, action: PayloadAction<boolean>) => {
state.autoAnalyze = action.payload
},
// 设置分析模型
setAnalyzeModel: (state, action: PayloadAction<string | null>) => {
state.analyzeModel = action.payload
},
// 设置分析状态
setAnalyzing: (state, action: PayloadAction<boolean>) => {
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<string | undefined>) => {
// 确保 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<string>) => {
// 确保 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<string>) => {
// 确保 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 }>) => {
const newShortMemory: ShortMemory = {
id: nanoid(),
content: action.payload.content,
createdAt: new Date().toISOString(),
topicId: action.payload.topicId
}
// 确保 shortMemories 存在
if (!state.shortMemories) {
state.shortMemories = []
}
state.shortMemories.push(newShortMemory)
},
// 删除短记忆
deleteShortMemory: (state, action: PayloadAction<string>) => {
// 确保 shortMemories 存在
if (!state.shortMemories) {
state.shortMemories = []
return
}
state.shortMemories = state.shortMemories.filter((memory) => memory.id !== action.payload)
},
// 清空指定话题的短记忆
clearShortMemories: (state, action: PayloadAction<string | undefined>) => {
// 确保 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<boolean>) => {
state.shortMemoryActive = action.payload
}
}
})
export const {
addMemory,
deleteMemory,
editMemory,
setMemoryActive,
setAutoAnalyze,
setAnalyzeModel,
setAnalyzing,
importMemories,
clearMemories,
addMemoryList,
deleteMemoryList,
editMemoryList,
setCurrentMemoryList,
toggleMemoryListActive,
// 短记忆相关的action
addShortMemory,
deleteShortMemory,
clearShortMemories,
setShortMemoryActive
} = memorySlice.actions
export default memorySlice.reducer

View File

@ -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))
}
})
}

View File

@ -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```'
}
}
}

View File

@ -147,28 +147,55 @@ ${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<string> => {
// 获取记忆
let memoriesPrompt = '';
export const buildSystemPrompt = async (
userSystemPrompt: string,
tools: MCPTool[],
mcpServers: MCPServer[] = []
): Promise<string> => {
// 获取MCP记忆
let mcpMemoriesPrompt = ''
try {
memoriesPrompt = await getRememberedMemories(mcpServers);
mcpMemoriesPrompt = await getRememberedMemories(mcpServers)
} catch (error) {
console.error('Error getting memories:', error);
}
// 添加记忆工具的使用说明
const rememberInstructions = '\n\n您可以使用remember工具记住用户的长期偏好和重要信息。当用户说"请记住..."或"记住..."时使用remember工具存储这些信息。记忆会自动应用到所有对话中无需显式调用。';
const enhancedPrompt = userSystemPrompt + rememberInstructions + memoriesPrompt;
if (tools && tools.length > 0) {
return SYSTEM_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', enhancedPrompt)
.replace('{{ TOOL_USE_EXAMPLES }}', ToolUseExamples)
.replace('{{ AVAILABLE_TOOLS }}', AvailableTools(tools))
console.error('Error getting MCP memories:', error)
}
return enhancedPrompt
}
// 获取内置记忆
let appMemoriesPrompt = ''
try {
// 应用内置记忆功能
console.log('[Prompt] Applying app memories to prompt')
// 直接将用户系统提示词传递给 applyMemoriesToPrompt让它添加记忆
appMemoriesPrompt = 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

View File

@ -4,64 +4,65 @@ import { MCPServer } from '@renderer/types'
export async function getRememberedMemories(mcpServers: MCPServer[]): Promise<string> {
try {
// 查找simpleremember服务器
const rememberServer = mcpServers.find(server => server.name === '@cherry/simpleremember' && server.isActive);
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] Server not found or not active')
return ''
}
console.log('[SimpleRemember] Found server:', rememberServer.name, 'isActive:', rememberServer.isActive);
console.log('[SimpleRemember] Found server:', rememberServer.name, 'isActive:', rememberServer.isActive)
// 调用get_memories工具
try {
console.log('[SimpleRemember] Calling get_memories tool...');
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);
console.log('[SimpleRemember] get_memories response:', response)
if (response.isError) {
console.error('[SimpleRemember] Error getting memories:', response);
return '';
console.error('[SimpleRemember] Error getting memories:', response)
return ''
}
// 解析记忆
// 根据MCP规范工具返回的是content数组而不是data
let memories = [];
let memories = []
if (response.content && response.content.length > 0 && response.content[0].text) {
try {
memories = JSON.parse(response.content[0].text);
memories = JSON.parse(response.content[0].text)
} catch (parseError) {
console.error('[SimpleRemember] Failed to parse memories JSON:', parseError);
return '';
console.error('[SimpleRemember] Failed to parse memories JSON:', parseError)
return ''
}
} else if (response.data) {
// 兼容旧版本的返回格式
memories = response.data;
memories = response.data
}
console.log('[SimpleRemember] Parsed memories:', memories);
console.log('[SimpleRemember] Parsed memories:', memories)
if (!Array.isArray(memories) || memories.length === 0) {
console.log('[SimpleRemember] No memories found or invalid format');
return '';
console.log('[SimpleRemember] No memories found or invalid format')
return ''
}
// 构建记忆提示词
const memoryPrompt = memories.map(memory => `- ${memory.content}`).join('\n');
console.log('[SimpleRemember] Generated memory prompt:', memoryPrompt);
// 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}`;
return `\n\n用户的记忆:\n${memoryPrompt}`
} catch (toolError) {
console.error('[SimpleRemember] Error calling get_memories tool:', toolError);
return '';
console.error('[SimpleRemember] Error calling get_memories tool:', toolError)
return ''
}
} catch (error) {
console.error('[SimpleRemember] Error in getRememberedMemories:', error);
return '';
console.error('[SimpleRemember] Error in getRememberedMemories:', error)
return ''
}
}