mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
记忆功能
This commit is contained in:
parent
54a8f31422
commit
8aea052bd6
@ -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}`)
|
||||
}
|
||||
|
||||
@ -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)}`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
72
src/renderer/src/components/MemoryProvider.tsx
Normal file
72
src/renderer/src/components/MemoryProvider.tsx
Normal 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
|
||||
@ -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'
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "启用",
|
||||
|
||||
@ -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" />
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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])
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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
|
||||
220
src/renderer/src/pages/settings/MemorySettings/MemoryMindMap.tsx
Normal file
220
src/renderer/src/pages/settings/MemorySettings/MemoryMindMap.tsx
Normal 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
|
||||
@ -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;
|
||||
@ -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
|
||||
535
src/renderer/src/pages/settings/MemorySettings/index.tsx
Normal file
535
src/renderer/src/pages/settings/MemorySettings/index.tsx
Normal 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
|
||||
@ -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 />} />}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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[]>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 }> {
|
||||
|
||||
@ -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 ''
|
||||
}
|
||||
}
|
||||
|
||||
382
src/renderer/src/services/MemoryService.ts
Normal file
382
src/renderer/src/services/MemoryService.ts
Normal 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
|
||||
}
|
||||
@ -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
|
||||
})
|
||||
|
||||
|
||||
@ -102,7 +102,8 @@ export const builtinMCPServers: MCPServer[] = [
|
||||
id: nanoid(),
|
||||
name: '@cherry/simpleremember',
|
||||
type: 'inMemory',
|
||||
description: '自动记忆工具,功能跟上面的记忆工具差不多。这个记忆会自动应用到对话中,无需显式调用。适合记住用户偏好、项目背景等长期有用信息.可以跨对话。',
|
||||
description:
|
||||
'自动记忆工具,功能跟上面的记忆工具差不多。这个记忆会自动应用到对话中,无需显式调用。适合记住用户偏好、项目背景等长期有用信息.可以跨对话。',
|
||||
isActive: true
|
||||
}
|
||||
]
|
||||
|
||||
363
src/renderer/src/store/memory.ts
Normal file
363
src/renderer/src/store/memory.ts
Normal 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
|
||||
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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```'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 ''
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user