Compare commits
11 Commits
copilot/fi
...
hlink/om3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2822a5e65d | ||
|
|
26b798f345 | ||
|
|
7aec8b4a35 | ||
|
|
994ab7362f | ||
|
|
bbdcd85014 | ||
|
|
249ab3d59f | ||
|
|
5df40ffc14 | ||
|
|
2bbe2f7ae5 | ||
|
|
f0876eaef0 | ||
|
|
aa8c7fd66f | ||
|
|
b8dffce149 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.2-batemo",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -75,6 +75,7 @@
|
||||
"adm-zip": "^0.5.16",
|
||||
"async-mutex": "^0.5.0",
|
||||
"color": "^5.0.0",
|
||||
"d3": "^7.9.0",
|
||||
"diff": "^7.0.0",
|
||||
"docx": "^9.0.2",
|
||||
"electron-log": "^5.1.5",
|
||||
@@ -121,6 +122,7 @@
|
||||
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
|
||||
"@tryfabric/martian": "^1.2.4",
|
||||
"@types/adm-zip": "^0",
|
||||
"@types/d3": "^7",
|
||||
"@types/diff": "^7",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/lodash": "^4.17.5",
|
||||
|
||||
@@ -153,5 +153,14 @@ export enum IpcChannel {
|
||||
// Search Window
|
||||
SearchWindow_Open = 'search-window:open',
|
||||
SearchWindow_Close = 'search-window:close',
|
||||
SearchWindow_OpenUrl = 'search-window:open-url'
|
||||
SearchWindow_OpenUrl = 'search-window:open-url',
|
||||
|
||||
// Memory File Storage
|
||||
Memory_LoadData = 'memory:load-data',
|
||||
Memory_SaveData = 'memory:save-data',
|
||||
Memory_DeleteShortMemoryById = 'memory:delete-short-memory-by-id',
|
||||
|
||||
// Long-term Memory File Storage
|
||||
LongTermMemory_LoadData = 'long-term-memory:load-data',
|
||||
LongTermMemory_SaveData = 'long-term-memory:save-data'
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import './services/MemoryFileService'
|
||||
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import './services/MemoryFileService'
|
||||
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { isMac, isWin } from '@main/constant'
|
||||
@@ -18,6 +20,7 @@ import FileStorage from './services/FileStorage'
|
||||
import { GeminiService } from './services/GeminiService'
|
||||
import KnowledgeService from './services/KnowledgeService'
|
||||
import mcpService from './services/MCPService'
|
||||
import { memoryFileService } from './services/MemoryFileService'
|
||||
import * as NutstoreService from './services/NutstoreService'
|
||||
import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
import { ProxyConfig, proxyManager } from './services/ProxyManager'
|
||||
@@ -305,4 +308,21 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle(IpcChannel.SearchWindow_OpenUrl, async (_, uid: string, url: string) => {
|
||||
return await searchService.openUrlInSearchWindow(uid, url)
|
||||
})
|
||||
|
||||
// memory
|
||||
ipcMain.handle(IpcChannel.Memory_LoadData, async () => {
|
||||
return await memoryFileService.loadData()
|
||||
})
|
||||
ipcMain.handle(IpcChannel.Memory_SaveData, async (_, data, forceOverwrite = false) => {
|
||||
return await memoryFileService.saveData(data, forceOverwrite)
|
||||
})
|
||||
ipcMain.handle(IpcChannel.Memory_DeleteShortMemoryById, async (_, id) => {
|
||||
return await memoryFileService.deleteShortMemoryById(id)
|
||||
})
|
||||
ipcMain.handle(IpcChannel.LongTermMemory_LoadData, async () => {
|
||||
return await memoryFileService.loadLongTermData()
|
||||
})
|
||||
ipcMain.handle(IpcChannel.LongTermMemory_SaveData, async (_, data, forceOverwrite = false) => {
|
||||
return await memoryFileService.saveLongTermData(data, forceOverwrite)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)}`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
321
src/main/mcpServers/simpleremember.ts
Normal file
321
src/main/mcpServers/simpleremember.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
// src/main/mcpServers/simpleremember.ts
|
||||
import { getConfigDir } from '@main/utils/file'
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ErrorCode,
|
||||
ListPromptsRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
McpError
|
||||
} from '@modelcontextprotocol/sdk/types.js'
|
||||
import { Mutex } from 'async-mutex'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
// 定义记忆文件路径
|
||||
const defaultMemoryPath = path.join(getConfigDir(), 'simpleremember.json')
|
||||
|
||||
// 记忆项接口
|
||||
interface Memory {
|
||||
content: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// 记忆存储结构
|
||||
interface MemoryStorage {
|
||||
memories: Memory[]
|
||||
}
|
||||
|
||||
class SimpleRememberManager {
|
||||
private memoryPath: string
|
||||
private memories: Memory[] = []
|
||||
private fileMutex: Mutex = new Mutex()
|
||||
|
||||
constructor(memoryPath: string) {
|
||||
this.memoryPath = memoryPath
|
||||
}
|
||||
|
||||
// 静态工厂方法用于初始化
|
||||
public static async create(memoryPath: string): Promise<SimpleRememberManager> {
|
||||
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 })
|
||||
try {
|
||||
await fs.access(this.memoryPath)
|
||||
} catch (error) {
|
||||
// 文件不存在,创建一个空文件
|
||||
await fs.writeFile(this.memoryPath, JSON.stringify({ memories: [] }, null, 2))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to ensure memory path exists:', error)
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Failed to ensure memory path: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 从磁盘加载记忆
|
||||
private async _loadMemoriesFromDisk(): Promise<void> {
|
||||
try {
|
||||
const data = await fs.readFile(this.memoryPath, 'utf-8')
|
||||
// 处理空文件情况
|
||||
if (data.trim() === '') {
|
||||
this.memories = []
|
||||
await this._persistMemories()
|
||||
return
|
||||
}
|
||||
const storage: MemoryStorage = JSON.parse(data)
|
||||
this.memories = storage.memories || []
|
||||
} catch (error) {
|
||||
if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') {
|
||||
this.memories = []
|
||||
await this._persistMemories()
|
||||
} else if (error instanceof SyntaxError) {
|
||||
console.error('Failed to parse simpleremember.json, initializing with empty memories:', error)
|
||||
this.memories = []
|
||||
await this._persistMemories()
|
||||
} else {
|
||||
console.error('Unexpected error loading memories:', error)
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Failed to load memories: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将记忆持久化到磁盘
|
||||
private async _persistMemories(): Promise<void> {
|
||||
const release = await this.fileMutex.acquire()
|
||||
try {
|
||||
const storage: MemoryStorage = {
|
||||
memories: this.memories
|
||||
}
|
||||
await fs.writeFile(this.memoryPath, JSON.stringify(storage, null, 2))
|
||||
} catch (error) {
|
||||
console.error('Failed to save memories:', error)
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Failed to save memories: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新记忆
|
||||
async remember(memory: string): Promise<Memory> {
|
||||
const newMemory: Memory = {
|
||||
content: memory,
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
this.memories.push(newMemory)
|
||||
await this._persistMemories()
|
||||
return newMemory
|
||||
}
|
||||
|
||||
// 获取所有记忆
|
||||
async getAllMemories(): Promise<Memory[]> {
|
||||
return [...this.memories]
|
||||
}
|
||||
|
||||
// 获取记忆 - 这个方法会被get_memories工具调用
|
||||
async get_memories(): Promise<Memory[]> {
|
||||
return this.getAllMemories()
|
||||
}
|
||||
}
|
||||
|
||||
// 定义工具 - 按照MCP规范定义工具
|
||||
const REMEMBER_TOOL = {
|
||||
name: 'remember',
|
||||
description:
|
||||
'用于记忆长期有用信息的工具。这个工具会自动应用记忆,无需显式调用。只用于存储长期有用的信息,不适合临时信息。',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
memory: {
|
||||
type: 'string',
|
||||
description: '要记住的简洁(1句话)记忆内容'
|
||||
}
|
||||
},
|
||||
required: ['memory']
|
||||
}
|
||||
}
|
||||
|
||||
const GET_MEMORIES_TOOL = {
|
||||
name: 'get_memories',
|
||||
description: '获取所有已存储的记忆',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加日志以便调试
|
||||
console.log('[SimpleRemember] Defined tools:', { REMEMBER_TOOL, GET_MEMORIES_TOOL })
|
||||
|
||||
class SimpleRememberServer {
|
||||
public server: Server
|
||||
private simpleRememberManager: SimpleRememberManager | null = null
|
||||
private initializationPromise: Promise<void>
|
||||
|
||||
constructor(envPath: string = '') {
|
||||
const memoryPath = envPath ? (path.isAbsolute(envPath) ? envPath : path.resolve(envPath)) : defaultMemoryPath
|
||||
|
||||
console.log('[SimpleRemember] Creating server with memory path:', memoryPath)
|
||||
|
||||
// 初始化服务器
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'simple-remember-server',
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {
|
||||
// 按照MCP规范声明工具能力
|
||||
listChanged: true
|
||||
},
|
||||
// 添加空的prompts能力,表示支持提示词功能但没有实际的提示词
|
||||
prompts: {}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
console.log('[SimpleRemember] Server initialized with tools capability')
|
||||
|
||||
// 手动添加工具到服务器的工具列表中
|
||||
console.log('[SimpleRemember] Adding tools to server')
|
||||
|
||||
// 先设置请求处理程序,再初始化管理器
|
||||
this.setupRequestHandlers()
|
||||
this.initializationPromise = this._initializeManager(memoryPath)
|
||||
|
||||
console.log('[SimpleRemember] Server initialization complete')
|
||||
// 打印工具信息以确认它们已注册
|
||||
console.log('[SimpleRemember] Tools registered:', [REMEMBER_TOOL.name, GET_MEMORIES_TOOL.name])
|
||||
}
|
||||
|
||||
private async _initializeManager(memoryPath: string): Promise<void> {
|
||||
try {
|
||||
this.simpleRememberManager = await SimpleRememberManager.create(memoryPath)
|
||||
console.log('SimpleRememberManager initialized successfully.')
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize SimpleRememberManager:', error)
|
||||
this.simpleRememberManager = null
|
||||
}
|
||||
}
|
||||
|
||||
private async _getManager(): Promise<SimpleRememberManager> {
|
||||
if (!this.simpleRememberManager) {
|
||||
await this.initializationPromise
|
||||
if (!this.simpleRememberManager) {
|
||||
throw new McpError(ErrorCode.InternalError, 'SimpleRememberManager is not initialized')
|
||||
}
|
||||
}
|
||||
return this.simpleRememberManager
|
||||
}
|
||||
|
||||
setupRequestHandlers() {
|
||||
// 添加对prompts/list请求的处理
|
||||
this.server.setRequestHandler(ListPromptsRequestSchema, async (request) => {
|
||||
console.log('[SimpleRemember] Listing prompts request received', request)
|
||||
|
||||
// 返回空的提示词列表
|
||||
return {
|
||||
prompts: []
|
||||
}
|
||||
})
|
||||
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async (request) => {
|
||||
// 直接返回工具列表,不需要等待管理器初始化
|
||||
console.log('[SimpleRemember] Listing tools request received', request)
|
||||
|
||||
// 打印工具定义以确保它们存在
|
||||
console.log('[SimpleRemember] REMEMBER_TOOL:', JSON.stringify(REMEMBER_TOOL))
|
||||
console.log('[SimpleRemember] GET_MEMORIES_TOOL:', JSON.stringify(GET_MEMORIES_TOOL))
|
||||
|
||||
const toolsList = [REMEMBER_TOOL, GET_MEMORIES_TOOL]
|
||||
console.log('[SimpleRemember] Returning tools:', JSON.stringify(toolsList))
|
||||
|
||||
// 按照MCP规范返回工具列表
|
||||
return {
|
||||
tools: toolsList
|
||||
// 如果有分页,可以添加nextCursor
|
||||
// nextCursor: "next-page-cursor"
|
||||
}
|
||||
})
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params
|
||||
|
||||
console.log(`[SimpleRemember] Received tool call: ${name}`, args)
|
||||
|
||||
try {
|
||||
const manager = await this._getManager()
|
||||
|
||||
if (name === 'remember') {
|
||||
if (!args || typeof args.memory !== 'string') {
|
||||
console.error(`[SimpleRemember] Invalid arguments for ${name}:`, args)
|
||||
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'memory' string is required.`)
|
||||
}
|
||||
console.log(`[SimpleRemember] Remembering: "${args.memory}"`)
|
||||
const result = await manager.remember(args.memory)
|
||||
console.log(`[SimpleRemember] Memory saved successfully:`, result)
|
||||
// 按照MCP规范返回工具调用结果
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `记忆已保存: "${args.memory}"`
|
||||
}
|
||||
],
|
||||
isError: false
|
||||
}
|
||||
}
|
||||
|
||||
if (name === 'get_memories') {
|
||||
console.log(`[SimpleRemember] Getting all memories`)
|
||||
const memories = await manager.get_memories()
|
||||
console.log(`[SimpleRemember] Retrieved ${memories.length} memories`)
|
||||
// 按照MCP规范返回工具调用结果
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(memories, null, 2)
|
||||
}
|
||||
],
|
||||
isError: false
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`[SimpleRemember] Unknown tool: ${name}`)
|
||||
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`)
|
||||
} catch (error) {
|
||||
console.error(`[SimpleRemember] Error handling tool call ${name}:`, error)
|
||||
// 按照MCP规范返回工具调用错误
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default SimpleRememberServer
|
||||
@@ -8,6 +8,7 @@ import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
import { createClient, CreateDirectoryOptions, FileStat } from 'webdav'
|
||||
|
||||
import { getConfigDir } from '../utils/file'
|
||||
import WebDav from './WebDav'
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
@@ -111,10 +112,29 @@ class BackupManager {
|
||||
// 使用流式复制
|
||||
await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => {
|
||||
copiedSize += size
|
||||
const progress = Math.min(80, 20 + Math.floor((copiedSize / totalSize) * 60))
|
||||
const progress = Math.min(70, 20 + Math.floor((copiedSize / totalSize) * 50))
|
||||
onProgress({ stage: 'copying_files', progress, total: 100 })
|
||||
})
|
||||
|
||||
// 复制记忆数据文件
|
||||
const configDir = getConfigDir()
|
||||
const memoryDataPath = path.join(configDir, 'memory-data.json')
|
||||
const tempConfigDir = path.join(this.tempDir, 'Config')
|
||||
const tempMemoryDataPath = path.join(tempConfigDir, 'memory-data.json')
|
||||
|
||||
// 确保目录存在
|
||||
await fs.ensureDir(tempConfigDir)
|
||||
|
||||
// 如果记忆数据文件存在,则复制
|
||||
if (await fs.pathExists(memoryDataPath)) {
|
||||
await fs.copy(memoryDataPath, tempMemoryDataPath)
|
||||
Logger.log('[BackupManager] Memory data file copied')
|
||||
onProgress({ stage: 'copying_memory_data', progress: 75, total: 100 })
|
||||
} else {
|
||||
Logger.log('[BackupManager] Memory data file not found, skipping')
|
||||
onProgress({ stage: 'copying_memory_data', progress: 75, total: 100 })
|
||||
}
|
||||
|
||||
await this.setWritableRecursive(tempDataDir)
|
||||
onProgress({ stage: 'compressing', progress: 80, total: 100 })
|
||||
|
||||
@@ -176,11 +196,32 @@ class BackupManager {
|
||||
// 使用流式复制
|
||||
await this.copyDirWithProgress(sourcePath, destPath, (size) => {
|
||||
copiedSize += size
|
||||
const progress = Math.min(90, 40 + Math.floor((copiedSize / totalSize) * 50))
|
||||
const progress = Math.min(80, 40 + Math.floor((copiedSize / totalSize) * 40))
|
||||
onProgress({ stage: 'copying_files', progress, total: 100 })
|
||||
})
|
||||
|
||||
Logger.log('[backup] step 4: clean up temp directory')
|
||||
// 恢复记忆数据文件
|
||||
Logger.log('[backup] step 4: restore memory data file')
|
||||
const tempConfigDir = path.join(this.tempDir, 'Config')
|
||||
const tempMemoryDataPath = path.join(tempConfigDir, 'memory-data.json')
|
||||
|
||||
if (await fs.pathExists(tempMemoryDataPath)) {
|
||||
const configDir = getConfigDir()
|
||||
const memoryDataPath = path.join(configDir, 'memory-data.json')
|
||||
|
||||
// 确保目录存在
|
||||
await fs.ensureDir(configDir)
|
||||
|
||||
// 复制记忆数据文件
|
||||
await fs.copy(tempMemoryDataPath, memoryDataPath)
|
||||
Logger.log('[backup] Memory data file restored')
|
||||
onProgress({ stage: 'restoring_memory_data', progress: 90, total: 100 })
|
||||
} else {
|
||||
Logger.log('[backup] Memory data file not found in backup, skipping')
|
||||
onProgress({ stage: 'restoring_memory_data', progress: 90, total: 100 })
|
||||
}
|
||||
|
||||
Logger.log('[backup] step 5: clean up temp directory')
|
||||
// 清理临时目录
|
||||
await this.setWritableRecursive(this.tempDir)
|
||||
await fs.remove(this.tempDir)
|
||||
|
||||
@@ -111,6 +111,20 @@ export class StreamableHTTPClientTransport implements Transport {
|
||||
headers.set('last-event-id', this._lastEventId)
|
||||
}
|
||||
|
||||
// 删除可能存在的HTTP/2伪头部
|
||||
if (headers.has(':path')) {
|
||||
headers.delete(':path')
|
||||
}
|
||||
if (headers.has(':method')) {
|
||||
headers.delete(':method')
|
||||
}
|
||||
if (headers.has(':authority')) {
|
||||
headers.delete(':authority')
|
||||
}
|
||||
if (headers.has(':scheme')) {
|
||||
headers.delete(':scheme')
|
||||
}
|
||||
|
||||
const response = await fetch(this._url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
@@ -216,6 +230,21 @@ export class StreamableHTTPClientTransport implements Transport {
|
||||
headers.set('content-type', 'application/json')
|
||||
headers.set('accept', 'application/json, text/event-stream')
|
||||
|
||||
// 添加错误处理,确保不使用HTTP/2伪头部
|
||||
// 删除可能存在的HTTP/2伪头部
|
||||
if (headers.has(':path')) {
|
||||
headers.delete(':path')
|
||||
}
|
||||
if (headers.has(':method')) {
|
||||
headers.delete(':method')
|
||||
}
|
||||
if (headers.has(':authority')) {
|
||||
headers.delete(':authority')
|
||||
}
|
||||
if (headers.has(':scheme')) {
|
||||
headers.delete(':scheme')
|
||||
}
|
||||
|
||||
const init = {
|
||||
...this._requestInit,
|
||||
method: 'POST',
|
||||
|
||||
310
src/main/services/MemoryFileService.ts
Normal file
310
src/main/services/MemoryFileService.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
import log from 'electron-log'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { getConfigDir } from '../utils/file'
|
||||
|
||||
// 定义记忆文件路径
|
||||
const memoryDataPath = path.join(getConfigDir(), 'memory-data.json')
|
||||
// 定义长期记忆文件路径
|
||||
const longTermMemoryDataPath = path.join(getConfigDir(), 'long-term-memory-data.json')
|
||||
|
||||
export class MemoryFileService {
|
||||
constructor() {
|
||||
this.registerIpcHandlers()
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
try {
|
||||
// 确保配置目录存在
|
||||
const configDir = path.dirname(memoryDataPath)
|
||||
try {
|
||||
await fs.mkdir(configDir, { recursive: true })
|
||||
} catch (mkdirError) {
|
||||
log.warn('Failed to create config directory, it may already exist:', mkdirError)
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
try {
|
||||
await fs.access(memoryDataPath)
|
||||
} catch (accessError) {
|
||||
// 文件不存在,创建默认文件
|
||||
log.info('Memory data file does not exist, creating default file')
|
||||
const defaultData = {
|
||||
memoryLists: [
|
||||
{
|
||||
id: 'default',
|
||||
name: '默认列表',
|
||||
isActive: true
|
||||
}
|
||||
],
|
||||
shortMemories: [],
|
||||
analyzeModel: 'gpt-3.5-turbo',
|
||||
shortMemoryAnalyzeModel: 'gpt-3.5-turbo',
|
||||
historicalContextAnalyzeModel: 'gpt-3.5-turbo',
|
||||
vectorizeModel: 'gpt-3.5-turbo'
|
||||
}
|
||||
await fs.writeFile(memoryDataPath, JSON.stringify(defaultData, null, 2))
|
||||
return defaultData
|
||||
}
|
||||
|
||||
// 读取文件
|
||||
const data = await fs.readFile(memoryDataPath, 'utf-8')
|
||||
const parsedData = JSON.parse(data)
|
||||
log.info('Memory data loaded successfully')
|
||||
return parsedData
|
||||
} catch (error) {
|
||||
log.error('Failed to load memory data:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async saveData(data: any, forceOverwrite: boolean = false) {
|
||||
try {
|
||||
// 确保配置目录存在
|
||||
const configDir = path.dirname(memoryDataPath)
|
||||
try {
|
||||
await fs.mkdir(configDir, { recursive: true })
|
||||
} catch (mkdirError) {
|
||||
log.warn('Failed to create config directory, it may already exist:', mkdirError)
|
||||
}
|
||||
|
||||
// 如果强制覆盖,直接使用传入的数据
|
||||
if (forceOverwrite) {
|
||||
log.info('Force overwrite enabled for short memory data, using provided data directly')
|
||||
|
||||
// 确保数据包含必要的字段
|
||||
const defaultData = {
|
||||
memoryLists: [],
|
||||
shortMemories: [],
|
||||
analyzeModel: '',
|
||||
shortMemoryAnalyzeModel: '',
|
||||
historicalContextAnalyzeModel: '',
|
||||
vectorizeModel: ''
|
||||
}
|
||||
|
||||
// 合并默认数据和传入的数据,确保数据结构完整
|
||||
const completeData = { ...defaultData, ...data }
|
||||
|
||||
// 保存数据
|
||||
await fs.writeFile(memoryDataPath, JSON.stringify(completeData, null, 2))
|
||||
log.info('Memory data saved successfully (force overwrite)')
|
||||
return true
|
||||
}
|
||||
|
||||
// 尝试读取现有数据并合并
|
||||
let existingData = {}
|
||||
try {
|
||||
await fs.access(memoryDataPath)
|
||||
const fileContent = await fs.readFile(memoryDataPath, 'utf-8')
|
||||
existingData = JSON.parse(fileContent)
|
||||
log.info('Existing memory data loaded for merging')
|
||||
} catch (readError) {
|
||||
log.warn('No existing memory data found or failed to read:', readError)
|
||||
// 如果文件不存在或读取失败,使用空对象
|
||||
}
|
||||
|
||||
// 合并数据,注意数组的处理
|
||||
const mergedData = { ...existingData }
|
||||
|
||||
// 处理每个属性
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
// 如果是数组属性,需要特殊处理
|
||||
if (Array.isArray(value) && Array.isArray(mergedData[key])) {
|
||||
// 对于 shortMemories 和 memories,直接使用传入的数组,完全替换现有的记忆
|
||||
if (key === 'shortMemories' || key === 'memories') {
|
||||
mergedData[key] = value
|
||||
log.info(`Replacing ${key} array with provided data`)
|
||||
} else {
|
||||
// 其他数组属性,使用新值
|
||||
mergedData[key] = value
|
||||
}
|
||||
} else {
|
||||
// 非数组属性,直接使用新值
|
||||
mergedData[key] = value
|
||||
}
|
||||
})
|
||||
|
||||
// 保存合并后的数据
|
||||
await fs.writeFile(memoryDataPath, JSON.stringify(mergedData, null, 2))
|
||||
log.info('Memory data saved successfully')
|
||||
return true
|
||||
} catch (error) {
|
||||
log.error('Failed to save memory data:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async loadLongTermData() {
|
||||
try {
|
||||
// 确保配置目录存在
|
||||
const configDir = path.dirname(longTermMemoryDataPath)
|
||||
try {
|
||||
await fs.mkdir(configDir, { recursive: true })
|
||||
} catch (mkdirError) {
|
||||
log.warn('Failed to create config directory, it may already exist:', mkdirError)
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
try {
|
||||
await fs.access(longTermMemoryDataPath)
|
||||
} catch (accessError) {
|
||||
// 文件不存在,创建默认文件
|
||||
log.info('Long-term memory data file does not exist, creating default file')
|
||||
const now = new Date().toISOString()
|
||||
const defaultData = {
|
||||
memoryLists: [
|
||||
{
|
||||
id: 'default',
|
||||
name: '默认列表',
|
||||
isActive: true,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
],
|
||||
memories: [],
|
||||
currentListId: 'default',
|
||||
analyzeModel: 'gpt-3.5-turbo'
|
||||
}
|
||||
await fs.writeFile(longTermMemoryDataPath, JSON.stringify(defaultData, null, 2))
|
||||
return defaultData
|
||||
}
|
||||
|
||||
// 读取文件
|
||||
const data = await fs.readFile(longTermMemoryDataPath, 'utf-8')
|
||||
const parsedData = JSON.parse(data)
|
||||
log.info('Long-term memory data loaded successfully')
|
||||
return parsedData
|
||||
} catch (error) {
|
||||
log.error('Failed to load long-term memory data:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async saveLongTermData(data: any, forceOverwrite: boolean = false) {
|
||||
try {
|
||||
// 确保配置目录存在
|
||||
const configDir = path.dirname(longTermMemoryDataPath)
|
||||
try {
|
||||
await fs.mkdir(configDir, { recursive: true })
|
||||
} catch (mkdirError) {
|
||||
log.warn('Failed to create config directory, it may already exist:', mkdirError)
|
||||
}
|
||||
|
||||
// 如果强制覆盖,直接使用传入的数据
|
||||
if (forceOverwrite) {
|
||||
log.info('Force overwrite enabled, using provided data directly')
|
||||
|
||||
// 确保数据包含必要的字段
|
||||
const defaultData = {
|
||||
memoryLists: [],
|
||||
memories: [],
|
||||
currentListId: '',
|
||||
analyzeModel: ''
|
||||
}
|
||||
|
||||
// 合并默认数据和传入的数据,确保数据结构完整
|
||||
const completeData = { ...defaultData, ...data }
|
||||
|
||||
// 保存数据
|
||||
await fs.writeFile(longTermMemoryDataPath, JSON.stringify(completeData, null, 2))
|
||||
log.info('Long-term memory data saved successfully (force overwrite)')
|
||||
return true
|
||||
}
|
||||
|
||||
// 尝试读取现有数据并合并
|
||||
let existingData = {}
|
||||
try {
|
||||
await fs.access(longTermMemoryDataPath)
|
||||
const fileContent = await fs.readFile(longTermMemoryDataPath, 'utf-8')
|
||||
existingData = JSON.parse(fileContent)
|
||||
log.info('Existing long-term memory data loaded for merging')
|
||||
} catch (readError) {
|
||||
log.warn('No existing long-term memory data found or failed to read:', readError)
|
||||
// 如果文件不存在或读取失败,使用空对象
|
||||
}
|
||||
|
||||
// 合并数据,注意数组的处理
|
||||
const mergedData = { ...existingData }
|
||||
|
||||
// 处理每个属性
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
// 如果是数组属性,需要特殊处理
|
||||
if (Array.isArray(value) && Array.isArray(mergedData[key])) {
|
||||
// 对于 memories 和 shortMemories,直接使用传入的数组,完全替换现有的记忆
|
||||
if (key === 'memories' || key === 'shortMemories') {
|
||||
mergedData[key] = value
|
||||
log.info(`Replacing ${key} array with provided data`)
|
||||
} else {
|
||||
// 其他数组属性,使用新值
|
||||
mergedData[key] = value
|
||||
}
|
||||
} else {
|
||||
// 非数组属性,直接使用新值
|
||||
mergedData[key] = value
|
||||
}
|
||||
})
|
||||
|
||||
// 保存合并后的数据
|
||||
await fs.writeFile(longTermMemoryDataPath, JSON.stringify(mergedData, null, 2))
|
||||
log.info('Long-term memory data saved successfully')
|
||||
return true
|
||||
} catch (error) {
|
||||
log.error('Failed to save long-term memory data:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定ID的短期记忆
|
||||
* @param id 要删除的短期记忆ID
|
||||
* @returns 是否成功删除
|
||||
*/
|
||||
async deleteShortMemoryById(id: string) {
|
||||
try {
|
||||
// 检查文件是否存在
|
||||
try {
|
||||
await fs.access(memoryDataPath)
|
||||
} catch (accessError) {
|
||||
log.error('Memory data file does not exist, cannot delete memory')
|
||||
return false
|
||||
}
|
||||
|
||||
// 读取文件
|
||||
const fileContent = await fs.readFile(memoryDataPath, 'utf-8')
|
||||
const data = JSON.parse(fileContent)
|
||||
|
||||
// 检查shortMemories数组是否存在
|
||||
if (!data.shortMemories || !Array.isArray(data.shortMemories)) {
|
||||
log.error('No shortMemories array found in memory data file')
|
||||
return false
|
||||
}
|
||||
|
||||
// 过滤掉要删除的记忆
|
||||
const originalLength = data.shortMemories.length
|
||||
data.shortMemories = data.shortMemories.filter((memory: any) => memory.id !== id)
|
||||
|
||||
// 如果长度没变,说明没有找到要删除的记忆
|
||||
if (data.shortMemories.length === originalLength) {
|
||||
log.warn(`Short memory with ID ${id} not found, nothing to delete`)
|
||||
return false
|
||||
}
|
||||
|
||||
// 写回文件
|
||||
await fs.writeFile(memoryDataPath, JSON.stringify(data, null, 2))
|
||||
log.info(`Successfully deleted short memory with ID ${id}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
log.error('Failed to delete short memory:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private registerIpcHandlers() {
|
||||
// 注册处理函数已移至ipc.ts文件中
|
||||
// 这里不需要重复注册
|
||||
}
|
||||
}
|
||||
|
||||
// 创建并导出MemoryFileService实例
|
||||
export const memoryFileService = new MemoryFileService()
|
||||
@@ -127,7 +127,15 @@ export class ProxyManager {
|
||||
const [protocol, address] = proxyUrl.split('://')
|
||||
const [host, port] = address.split(':')
|
||||
if (!protocol.includes('socks')) {
|
||||
setGlobalDispatcher(new ProxyAgent(proxyUrl))
|
||||
// 使用标准方式创建ProxyAgent,但添加错误处理
|
||||
try {
|
||||
// 尝试使用代理
|
||||
const agent = new ProxyAgent(proxyUrl)
|
||||
setGlobalDispatcher(agent)
|
||||
console.log('[Proxy] Successfully set HTTP proxy:', proxyUrl)
|
||||
} catch (error) {
|
||||
console.error('[Proxy] Failed to set proxy:', error)
|
||||
}
|
||||
} else {
|
||||
const dispatcher = socksDispatcher({
|
||||
port: parseInt(port),
|
||||
|
||||
7
src/preload/index.d.ts
vendored
7
src/preload/index.d.ts
vendored
@@ -190,6 +190,13 @@ declare global {
|
||||
closeSearchWindow: (uid: string) => Promise<string>
|
||||
openUrlInSearchWindow: (uid: string, url: string) => Promise<string>
|
||||
}
|
||||
memory: {
|
||||
loadData: () => Promise<any>
|
||||
saveData: (data: any, forceOverwrite?: boolean) => Promise<boolean>
|
||||
deleteShortMemoryById: (id: string) => Promise<boolean>
|
||||
loadLongTermData: () => Promise<any>
|
||||
saveLongTermData: (data: any, forceOverwrite?: boolean) => Promise<boolean>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +177,14 @@ const api = {
|
||||
openSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Open, uid),
|
||||
closeSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Close, uid),
|
||||
openUrlInSearchWindow: (uid: string, url: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_OpenUrl, uid, url)
|
||||
},
|
||||
memory: {
|
||||
loadData: () => ipcRenderer.invoke(IpcChannel.Memory_LoadData),
|
||||
saveData: (data: any) => ipcRenderer.invoke(IpcChannel.Memory_SaveData, data),
|
||||
deleteShortMemoryById: (id: string) => ipcRenderer.invoke(IpcChannel.Memory_DeleteShortMemoryById, id),
|
||||
loadLongTermData: () => ipcRenderer.invoke(IpcChannel.LongTermMemory_LoadData),
|
||||
saveLongTermData: (data: any, forceOverwrite: boolean = false) =>
|
||||
ipcRenderer.invoke(IpcChannel.LongTermMemory_SaveData, data, forceOverwrite)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.markdown {
|
||||
color: var(--color-text);
|
||||
line-height: 1.6;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
word-break: break-word;
|
||||
|
||||
|
||||
230
src/renderer/src/components/MemoryProvider.tsx
Normal file
230
src/renderer/src/components/MemoryProvider.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import { useMemoryService } from '@renderer/services/MemoryService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import store from '@renderer/store'
|
||||
import {
|
||||
clearShortMemories,
|
||||
loadLongTermMemoryData,
|
||||
loadMemoryData,
|
||||
setAdaptiveAnalysisEnabled,
|
||||
setAnalysisDepth,
|
||||
setAnalysisFrequency,
|
||||
setAutoAnalyze,
|
||||
setAutoRecommendMemories,
|
||||
setContextualRecommendationEnabled,
|
||||
setCurrentMemoryList,
|
||||
setDecayEnabled,
|
||||
setDecayRate,
|
||||
setFreshnessEnabled,
|
||||
setInterestTrackingEnabled,
|
||||
setMemoryActive,
|
||||
setMonitoringEnabled,
|
||||
setPriorityManagementEnabled,
|
||||
setRecommendationThreshold,
|
||||
setShortMemoryActive
|
||||
} from '@renderer/store/memory'
|
||||
import { FC, ReactNode, useEffect, useRef } from 'react'
|
||||
|
||||
interface MemoryProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* 记忆功能提供者组件
|
||||
* 这个组件负责初始化记忆功能并在适当的时候触发记忆分析
|
||||
*/
|
||||
const MemoryProvider: FC<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(() => {
|
||||
console.log('[MemoryProvider] Loading memory data from file')
|
||||
// 使用Redux Thunk加载短期记忆数据
|
||||
dispatch(loadMemoryData())
|
||||
.then((result) => {
|
||||
if (result.payload) {
|
||||
console.log('[MemoryProvider] Short-term memory data loaded successfully via Redux Thunk')
|
||||
|
||||
// 更新所有设置
|
||||
const data = result.payload
|
||||
|
||||
// 基本设置
|
||||
if (data.isActive !== undefined) dispatch(setMemoryActive(data.isActive))
|
||||
if (data.shortMemoryActive !== undefined) dispatch(setShortMemoryActive(data.shortMemoryActive))
|
||||
if (data.autoAnalyze !== undefined) dispatch(setAutoAnalyze(data.autoAnalyze))
|
||||
|
||||
// 自适应分析相关
|
||||
if (data.adaptiveAnalysisEnabled !== undefined)
|
||||
dispatch(setAdaptiveAnalysisEnabled(data.adaptiveAnalysisEnabled))
|
||||
if (data.analysisFrequency !== undefined) dispatch(setAnalysisFrequency(data.analysisFrequency))
|
||||
if (data.analysisDepth !== undefined) dispatch(setAnalysisDepth(data.analysisDepth))
|
||||
|
||||
// 用户关注点相关
|
||||
if (data.interestTrackingEnabled !== undefined)
|
||||
dispatch(setInterestTrackingEnabled(data.interestTrackingEnabled))
|
||||
|
||||
// 性能监控相关
|
||||
if (data.monitoringEnabled !== undefined) dispatch(setMonitoringEnabled(data.monitoringEnabled))
|
||||
|
||||
// 智能优先级与时效性管理相关
|
||||
if (data.priorityManagementEnabled !== undefined)
|
||||
dispatch(setPriorityManagementEnabled(data.priorityManagementEnabled))
|
||||
if (data.decayEnabled !== undefined) dispatch(setDecayEnabled(data.decayEnabled))
|
||||
if (data.freshnessEnabled !== undefined) dispatch(setFreshnessEnabled(data.freshnessEnabled))
|
||||
if (data.decayRate !== undefined) dispatch(setDecayRate(data.decayRate))
|
||||
|
||||
// 上下文感知记忆推荐相关
|
||||
if (data.contextualRecommendationEnabled !== undefined)
|
||||
dispatch(setContextualRecommendationEnabled(data.contextualRecommendationEnabled))
|
||||
if (data.autoRecommendMemories !== undefined) dispatch(setAutoRecommendMemories(data.autoRecommendMemories))
|
||||
if (data.recommendationThreshold !== undefined)
|
||||
dispatch(setRecommendationThreshold(data.recommendationThreshold))
|
||||
|
||||
console.log('[MemoryProvider] Memory settings loaded successfully')
|
||||
} else {
|
||||
console.log('[MemoryProvider] No short-term memory data loaded or loading failed')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[MemoryProvider] Error loading short-term memory data:', error)
|
||||
})
|
||||
|
||||
// 使用Redux Thunk加载长期记忆数据
|
||||
dispatch(loadLongTermMemoryData())
|
||||
.then((result) => {
|
||||
if (result.payload) {
|
||||
console.log('[MemoryProvider] Long-term memory data loaded successfully via Redux Thunk')
|
||||
|
||||
// 确保在长期记忆数据加载后,检查并设置当前记忆列表
|
||||
setTimeout(() => {
|
||||
const state = store.getState().memory
|
||||
if (!state.currentListId && state.memoryLists && state.memoryLists.length > 0) {
|
||||
// 先尝试找到一个isActive为true的列表
|
||||
const activeList = state.memoryLists.find((list) => list.isActive)
|
||||
if (activeList) {
|
||||
console.log('[MemoryProvider] Auto-selecting active memory list:', activeList.name)
|
||||
dispatch(setCurrentMemoryList(activeList.id))
|
||||
} else {
|
||||
// 如果没有激活的列表,使用第一个列表
|
||||
console.log('[MemoryProvider] Auto-selecting first memory list:', state.memoryLists[0].name)
|
||||
dispatch(setCurrentMemoryList(state.memoryLists[0].id))
|
||||
}
|
||||
}
|
||||
}, 500) // 添加一个小延迟,确保状态已更新
|
||||
} else {
|
||||
console.log('[MemoryProvider] No long-term memory data loaded or loading failed')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[MemoryProvider] Error loading long-term memory data:', error)
|
||||
})
|
||||
}, [dispatch])
|
||||
|
||||
// 当对话更新时,触发记忆分析
|
||||
useEffect(() => {
|
||||
if (isActive && autoAnalyze && analyzeModel && messages.length > 0) {
|
||||
// 获取当前的分析频率
|
||||
const memoryState = store.getState().memory || {}
|
||||
const analysisFrequency = memoryState.analysisFrequency || 5
|
||||
const adaptiveAnalysisEnabled = memoryState.adaptiveAnalysisEnabled || false
|
||||
|
||||
// 检查是否有新消息需要分析
|
||||
const newMessagesCount = messages.length - lastAnalyzedCountRef.current
|
||||
|
||||
// 使用自适应分析频率
|
||||
if (
|
||||
newMessagesCount >= analysisFrequency ||
|
||||
(messages.length % analysisFrequency === 0 && lastAnalyzedCountRef.current === 0)
|
||||
) {
|
||||
console.log(
|
||||
`[Memory Analysis] Triggering analysis with ${newMessagesCount} new messages (frequency: ${analysisFrequency})`
|
||||
)
|
||||
|
||||
// 将当前话题ID传递给分析函数
|
||||
analyzeAndAddMemories(currentTopic)
|
||||
lastAnalyzedCountRef.current = messages.length
|
||||
|
||||
// 性能监控:记录当前分析触发时的消息数量
|
||||
if (adaptiveAnalysisEnabled) {
|
||||
console.log(`[Memory Analysis] Adaptive analysis enabled, current frequency: ${analysisFrequency}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isActive, autoAnalyze, analyzeModel, messages.length, analyzeAndAddMemories, currentTopic])
|
||||
|
||||
// 当对话话题切换时,清除上一个话题的短记忆
|
||||
useEffect(() => {
|
||||
// 如果短记忆功能激活且当前话题发生变化
|
||||
if (shortMemoryActive && currentTopic !== previousTopicRef.current && previousTopicRef.current) {
|
||||
console.log(`[Memory] Topic changed from ${previousTopicRef.current} to ${currentTopic}, clearing short memories`)
|
||||
// 清除上一个话题的短记忆
|
||||
dispatch(clearShortMemories(previousTopicRef.current))
|
||||
}
|
||||
|
||||
// 更新上一次的话题ID
|
||||
previousTopicRef.current = currentTopic || null
|
||||
}, [currentTopic, shortMemoryActive, dispatch])
|
||||
|
||||
// 监控记忆列表变化,确保总是有一个选中的记忆列表
|
||||
useEffect(() => {
|
||||
// 立即检查一次
|
||||
const checkAndSetMemoryList = () => {
|
||||
const state = store.getState().memory
|
||||
if (state.memoryLists && state.memoryLists.length > 0) {
|
||||
// 如果没有选中的记忆列表,或者选中的列表不存在
|
||||
if (!state.currentListId || !state.memoryLists.some((list) => list.id === state.currentListId)) {
|
||||
// 先尝试找到一个isActive为true的列表
|
||||
const activeList = state.memoryLists.find((list) => list.isActive)
|
||||
if (activeList) {
|
||||
console.log('[MemoryProvider] Setting active memory list:', activeList.name)
|
||||
dispatch(setCurrentMemoryList(activeList.id))
|
||||
} else if (state.memoryLists.length > 0) {
|
||||
// 如果没有激活的列表,使用第一个列表
|
||||
console.log('[MemoryProvider] Setting first memory list:', state.memoryLists[0].name)
|
||||
dispatch(setCurrentMemoryList(state.memoryLists[0].id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 立即检查一次
|
||||
checkAndSetMemoryList()
|
||||
|
||||
// 设置定时器,每秒检查一次,持续5秒
|
||||
const intervalId = setInterval(checkAndSetMemoryList, 1000)
|
||||
const timeoutId = setTimeout(() => {
|
||||
clearInterval(intervalId)
|
||||
}, 5000)
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId)
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}, [dispatch])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
export default MemoryProvider
|
||||
273
src/renderer/src/components/Popups/ShortMemoryPopup.tsx
Normal file
273
src/renderer/src/components/Popups/ShortMemoryPopup.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import { DeleteOutlined, InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { Box } from '@renderer/components/Layout'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { addShortMemoryItem, analyzeAndAddShortMemories } from '@renderer/services/MemoryService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import store from '@renderer/store'
|
||||
import { deleteShortMemory } from '@renderer/store/memory'
|
||||
import { Button, Card, Col, Empty, Input, List, message, Modal, Row, Statistic, Tooltip } from 'antd'
|
||||
import _ from 'lodash'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
// 不再需要确认对话框
|
||||
|
||||
const ButtonGroup = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
const MemoryContent = styled.div`
|
||||
word-break: break-word;
|
||||
`
|
||||
|
||||
interface ShowParams {
|
||||
topicId: string
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ topicId, resolve }) => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const [open, setOpen] = useState(true)
|
||||
|
||||
// 获取短记忆状态
|
||||
const shortMemoryActive = useAppSelector((state) => state.memory?.shortMemoryActive || false)
|
||||
const shortMemories = useAppSelector((state) => {
|
||||
const allShortMemories = state.memory?.shortMemories || []
|
||||
// 只显示当前话题的短记忆
|
||||
return topicId ? allShortMemories.filter((memory) => memory.topicId === topicId) : []
|
||||
})
|
||||
|
||||
// 添加短记忆的状态
|
||||
const [newMemoryContent, setNewMemoryContent] = useState('')
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
||||
|
||||
// 添加新的短记忆 - 使用防抖减少频繁更新
|
||||
const handleAddMemory = useCallback(
|
||||
_.debounce(() => {
|
||||
if (newMemoryContent.trim() && topicId) {
|
||||
addShortMemoryItem(newMemoryContent.trim(), topicId)
|
||||
setNewMemoryContent('') // 清空输入框
|
||||
}
|
||||
}, 300),
|
||||
[newMemoryContent, topicId]
|
||||
)
|
||||
|
||||
// 手动分析对话内容 - 使用节流避免频繁分析操作
|
||||
const handleAnalyzeConversation = useCallback(
|
||||
_.throttle(async () => {
|
||||
if (!topicId || !shortMemoryActive) return
|
||||
|
||||
setIsAnalyzing(true)
|
||||
try {
|
||||
const result = await analyzeAndAddShortMemories(topicId)
|
||||
if (result) {
|
||||
// 如果有新的短期记忆被添加
|
||||
Modal.success({
|
||||
title: t('settings.memory.shortMemoryAnalysisSuccess') || '分析成功',
|
||||
content: t('settings.memory.shortMemoryAnalysisSuccessContent') || '已成功提取并添加重要信息到短期记忆'
|
||||
})
|
||||
} else {
|
||||
// 如果没有新的短期记忆被添加
|
||||
Modal.info({
|
||||
title: t('settings.memory.shortMemoryAnalysisNoNew') || '无新信息',
|
||||
content: t('settings.memory.shortMemoryAnalysisNoNewContent') || '未发现新的重要信息或所有信息已存在'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to analyze conversation:', error)
|
||||
Modal.error({
|
||||
title: t('settings.memory.shortMemoryAnalysisError') || '分析失败',
|
||||
content: t('settings.memory.shortMemoryAnalysisErrorContent') || '分析对话内容时出错'
|
||||
})
|
||||
} finally {
|
||||
setIsAnalyzing(false)
|
||||
}
|
||||
}, 1000),
|
||||
[topicId, shortMemoryActive, t]
|
||||
)
|
||||
|
||||
// 删除短记忆 - 直接删除无需确认,使用节流避免频繁删除操作
|
||||
const handleDeleteMemory = useCallback(
|
||||
_.throttle(async (id: string) => {
|
||||
// 先从当前状态中获取要删除的记忆之外的所有记忆
|
||||
const state = store.getState().memory
|
||||
const filteredShortMemories = state.shortMemories.filter((memory) => memory.id !== id)
|
||||
|
||||
// 执行删除操作
|
||||
dispatch(deleteShortMemory(id))
|
||||
|
||||
// 直接使用 window.api.memory.saveData 方法保存过滤后的列表
|
||||
try {
|
||||
// 加载当前文件数据
|
||||
const currentData = await window.api.memory.loadData()
|
||||
|
||||
// 替换 shortMemories 数组
|
||||
const newData = {
|
||||
...currentData,
|
||||
shortMemories: filteredShortMemories
|
||||
}
|
||||
|
||||
// 使用 true 参数强制覆盖文件
|
||||
const result = await window.api.memory.saveData(newData, true)
|
||||
|
||||
if (result) {
|
||||
console.log(`[ShortMemoryPopup] Successfully deleted short memory with ID ${id}`)
|
||||
message.success(t('settings.memory.deleteSuccess') || '删除成功')
|
||||
} else {
|
||||
console.error(`[ShortMemoryPopup] Failed to delete short memory with ID ${id}`)
|
||||
message.error(t('settings.memory.deleteError') || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ShortMemoryPopup] Failed to delete short memory:', error)
|
||||
message.error(t('settings.memory.deleteError') || '删除失败')
|
||||
}
|
||||
}, 500),
|
||||
[dispatch, t]
|
||||
)
|
||||
|
||||
const onClose = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const afterClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
ShortMemoryPopup.hide = onClose
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('settings.memory.shortMemory')}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
afterClose={afterClose}
|
||||
footer={null}
|
||||
width={500}
|
||||
centered>
|
||||
<Box mb={16}>
|
||||
<Input.TextArea
|
||||
value={newMemoryContent}
|
||||
onChange={(e) => setNewMemoryContent(e.target.value)}
|
||||
placeholder={t('settings.memory.addShortMemoryPlaceholder')}
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
disabled={!shortMemoryActive || !topicId}
|
||||
/>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => handleAddMemory()}
|
||||
disabled={!shortMemoryActive || !newMemoryContent.trim() || !topicId}>
|
||||
{t('settings.memory.addShortMemory')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleAnalyzeConversation()}
|
||||
loading={isAnalyzing}
|
||||
disabled={!shortMemoryActive || !topicId}>
|
||||
{t('settings.memory.analyzeConversation') || '分析对话'}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
|
||||
{/* 性能监控统计信息 */}
|
||||
<Box mb={16}>
|
||||
<Card
|
||||
size="small"
|
||||
title={t('settings.memory.performanceStats') || '系统性能统计'}
|
||||
extra={<InfoCircleOutlined />}>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title={t('settings.memory.totalAnalyses') || '总分析次数'}
|
||||
value={store.getState().memory?.analysisStats?.totalAnalyses || 0}
|
||||
precision={0}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title={t('settings.memory.successRate') || '成功率'}
|
||||
value={
|
||||
store.getState().memory?.analysisStats?.totalAnalyses
|
||||
? ((store.getState().memory?.analysisStats?.successfulAnalyses || 0) /
|
||||
(store.getState().memory?.analysisStats?.totalAnalyses || 1)) *
|
||||
100
|
||||
: 0
|
||||
}
|
||||
precision={1}
|
||||
suffix="%"
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title={t('settings.memory.avgAnalysisTime') || '平均分析时间'}
|
||||
value={store.getState().memory?.analysisStats?.averageAnalysisTime || 0}
|
||||
precision={0}
|
||||
suffix="ms"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
<MemoriesList>
|
||||
{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={<MemoryContent>{memory.content}</MemoryContent>}
|
||||
description={new Date(memory.createdAt).toLocaleString()}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Empty description={!topicId ? t('settings.memory.noCurrentTopic') : t('settings.memory.noShortMemories')} />
|
||||
)}
|
||||
</MemoriesList>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const MemoriesList = styled.div`
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
`
|
||||
|
||||
const TopViewKey = 'ShortMemoryPopup'
|
||||
|
||||
export default class ShortMemoryPopup {
|
||||
static hide: () => void = () => {}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -498,6 +498,12 @@
|
||||
"copied": "Copied!",
|
||||
"copy.failed": "Copy failed",
|
||||
"copy.success": "Copied!",
|
||||
"copy_id": "Copy Message ID",
|
||||
"id_copied": "Message ID copied",
|
||||
"id_found": "Original message found",
|
||||
"reference": "Reference message",
|
||||
"reference.error": "Failed to find original message",
|
||||
"referenced_message": "Referenced Message",
|
||||
"error.chunk_overlap_too_large": "Chunk overlap cannot be greater than chunk size",
|
||||
"error.dimension_too_large": "Content size is too large",
|
||||
"error.enter.api.host": "Please enter your API host first",
|
||||
@@ -1024,6 +1030,127 @@
|
||||
"launch.onboot": "Start Automatically on Boot",
|
||||
"launch.title": "Launch",
|
||||
"launch.totray": "Minimize to Tray on Launch",
|
||||
"memory": {
|
||||
"historicalContext": {
|
||||
"title": "Historical Dialog Context",
|
||||
"description": "Allow AI to automatically reference historical dialogs when needed, to provide more coherent answers.",
|
||||
"enable": "Enable Historical Dialog Context",
|
||||
"enableTip": "When enabled, AI will automatically analyze and reference historical dialogs when needed, to provide more coherent answers",
|
||||
"analyzeModelTip": "Select the model used for historical dialog context analysis, it's recommended to choose a model with faster response"
|
||||
},
|
||||
"title": "Memory Function",
|
||||
"description": "Manage AI assistant's long-term memory, automatically analyze conversations and extract important information",
|
||||
"enableMemory": "Enable Memory Function",
|
||||
"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",
|
||||
"resetAnalyzingState": "Reset Analysis State",
|
||||
"filterSensitiveInfo": "Filter Sensitive Information",
|
||||
"filterSensitiveInfoTip": "When enabled, memory function will not extract API keys, passwords, or other sensitive information",
|
||||
"resetLongTermMemory": "Reset Analysis Markers",
|
||||
"resetLongTermMemorySuccess": "Long-term memory analysis markers reset",
|
||||
"resetLongTermMemoryNoChange": "No analysis markers to reset",
|
||||
"resetLongTermMemoryError": "Failed to reset long-term memory analysis markers",
|
||||
"saveAllSettings": "Save All Settings",
|
||||
"saveAllSettingsDescription": "Save all memory function settings to file to ensure they persist after application restart.",
|
||||
"saveAllSettingsSuccess": "All settings saved successfully",
|
||||
"saveAllSettingsError": "Failed to save settings",
|
||||
"analyzeConversation": "Analyze Conversation",
|
||||
"shortMemoryAnalysisSuccess": "Analysis Successful",
|
||||
"shortMemoryAnalysisSuccessContent": "Successfully extracted and added important information to short-term memory",
|
||||
"shortMemoryAnalysisNoNew": "No New Information",
|
||||
"shortMemoryAnalysisNoNewContent": "No new important information found or all information already exists",
|
||||
"shortMemoryAnalysisError": "Analysis Failed",
|
||||
"shortMemoryAnalysisErrorContent": "Error occurred while analyzing conversation content",
|
||||
"performanceStats": "Performance Statistics",
|
||||
"totalAnalyses": "Total Analyses",
|
||||
"successRate": "Success Rate",
|
||||
"avgAnalysisTime": "Average Analysis Time",
|
||||
"deduplication": {
|
||||
"title": "Memory Deduplication",
|
||||
"description": "Analyze similar memories in your memory library and provide intelligent merging suggestions.",
|
||||
"selectList": "Select Memory List",
|
||||
"allLists": "All Lists",
|
||||
"selectTopic": "Select Topic",
|
||||
"similarityThreshold": "Similarity Threshold",
|
||||
"startAnalysis": "Start Analysis",
|
||||
"help": "Help",
|
||||
"helpTitle": "Memory Deduplication Help",
|
||||
"helpContent1": "This feature analyzes similar memories in your memory library and provides merging suggestions.",
|
||||
"helpContent2": "The similarity threshold determines how similar two memories need to be to be considered for merging. Higher values require more similarity.",
|
||||
"helpContent3": "When you apply the results, similar memories will be merged into a new memory and the original memories will be deleted.",
|
||||
"analyzing": "Analyzing...",
|
||||
"noSimilarMemories": "No similar memories found",
|
||||
"similarGroups": "Similar Memory Groups",
|
||||
"group": "Group",
|
||||
"items": "items",
|
||||
"originalMemories": "Original Memories",
|
||||
"mergedResult": "Merged Result",
|
||||
"other": "Other",
|
||||
"applyResults": "Apply Results",
|
||||
"confirmApply": "Confirm Apply Results",
|
||||
"confirmApplyContent": "Applying deduplication results will merge similar memories and delete the original ones. This action cannot be undone. Are you sure you want to continue?",
|
||||
"applySuccess": "Applied Successfully",
|
||||
"applySuccessContent": "Memory deduplication has been successfully applied"
|
||||
},
|
||||
"shortMemoryDeduplication": {
|
||||
"title": "Short Memory Deduplication",
|
||||
"description": "Analyze similar memories in your short-term memory and provide intelligent merging suggestions.",
|
||||
"selectTopic": "Select Topic",
|
||||
"similarityThreshold": "Similarity Threshold",
|
||||
"startAnalysis": "Start Analysis",
|
||||
"help": "Help",
|
||||
"helpTitle": "Short Memory Deduplication Help",
|
||||
"helpContent1": "This feature analyzes similar memories in your short-term memory and provides merging suggestions.",
|
||||
"helpContent2": "The similarity threshold determines how similar two memories need to be to be considered for merging. Higher values require more similarity.",
|
||||
"helpContent3": "When you apply the results, similar memories will be merged into a new memory and the original memories will be deleted.",
|
||||
"analyzing": "Analyzing...",
|
||||
"noSimilarMemories": "No similar memories found",
|
||||
"similarGroups": "Similar Memory Groups",
|
||||
"group": "Group",
|
||||
"items": "items",
|
||||
"originalMemories": "Original Memories",
|
||||
"mergedResult": "Merged Result",
|
||||
"other": "Other",
|
||||
"applyResults": "Apply Results",
|
||||
"confirmApply": "Confirm Apply Results",
|
||||
"confirmApplyContent": "Applying deduplication results will merge similar memories and delete the original ones. This action cannot be undone. Are you sure you want to continue?",
|
||||
"applySuccess": "Applied Successfully",
|
||||
"applySuccessContent": "Short memory deduplication has been successfully applied"
|
||||
},
|
||||
"selectTopic": "Select Topic",
|
||||
"selectTopicPlaceholder": "Select a topic to analyze",
|
||||
"filterByCategory": "Filter by Category",
|
||||
"allCategories": "All",
|
||||
"uncategorized": "Uncategorized",
|
||||
"shortMemory": "Short-term Memory",
|
||||
"loading": "Loading...",
|
||||
"longMemory": "Long-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",
|
||||
|
||||
@@ -1351,6 +1351,64 @@
|
||||
"privacy": {
|
||||
"title": "プライバシー設定",
|
||||
"enable_privacy_mode": "匿名エラーレポートとデータ統計の送信"
|
||||
},
|
||||
"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": "短期メモリー",
|
||||
"longMemory": "長期メモリー",
|
||||
"toggleShortMemoryActive": "短期メモリー機能を切り替え",
|
||||
"addShortMemory": "短期メモリーを追加",
|
||||
"addShortMemoryPlaceholder": "短期メモリーの内容を入力、現在の会話のみ有効",
|
||||
"noShortMemories": "短期メモリーなし",
|
||||
"noCurrentTopic": "まず会話トピックを選択してください",
|
||||
"confirmDelete": "削除の確認",
|
||||
"confirmDeleteContent": "この短期メモリーを削除しますか?",
|
||||
"delete": "削除",
|
||||
"performanceStats": "パフォーマンス統計",
|
||||
"totalAnalyses": "分析回数合計",
|
||||
"successRate": "成功率",
|
||||
"avgAnalysisTime": "平均分析時間"
|
||||
}
|
||||
},
|
||||
"translate": {
|
||||
@@ -1393,4 +1451,4 @@
|
||||
"visualization": "可視化"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1351,6 +1351,60 @@
|
||||
"privacy": {
|
||||
"title": "Настройки приватности",
|
||||
"enable_privacy_mode": "Анонимная отправка отчетов об ошибках и статистики"
|
||||
},
|
||||
"memory": {
|
||||
"title": "[to be translated]:记忆功能",
|
||||
"description": "[to be translated]:管理AI助手的长期记忆,自动分析对话并提取重要信息",
|
||||
"enableMemory": "[to be translated]:启用记忆功能",
|
||||
"enableAutoAnalyze": "[to be translated]:启用自动分析",
|
||||
"analyzeModel": "[to be translated]:分析模型",
|
||||
"selectModel": "[to be translated]:选择模型",
|
||||
"memoriesList": "[to be translated]:记忆列表",
|
||||
"memoryLists": "[to be translated]:记忆角色",
|
||||
"addMemory": "[to be translated]:添加记忆",
|
||||
"editMemory": "[to be translated]:编辑记忆",
|
||||
"clearAll": "[to be translated]:清空全部",
|
||||
"noMemories": "[to be translated]:暂无记忆",
|
||||
"memoryPlaceholder": "[to be translated]:输入要记住的内容",
|
||||
"addSuccess": "[to be translated]:记忆添加成功",
|
||||
"editSuccess": "[to be translated]:记忆编辑成功",
|
||||
"deleteSuccess": "[to be translated]:记忆删除成功",
|
||||
"clearSuccess": "[to be translated]:记忆清空成功",
|
||||
"clearConfirmTitle": "[to be translated]:确认清空",
|
||||
"clearConfirmContent": "[to be translated]:确定要清空所有记忆吗?此操作无法撤销。",
|
||||
"listView": "[to be translated]:列表视图",
|
||||
"mindmapView": "[to be translated]:思维导图",
|
||||
"centerNodeLabel": "[to be translated]:用户记忆",
|
||||
"manualAnalyze": "[to be translated]:手动分析",
|
||||
"analyzeNow": "[to be translated]:立即分析",
|
||||
"startingAnalysis": "[to be translated]:开始分析...",
|
||||
"cannotAnalyze": "[to be translated]:无法分析,请检查设置",
|
||||
"selectTopic": "[to be translated]:选择话题",
|
||||
"selectTopicPlaceholder": "[to be translated]:选择要分析的话题",
|
||||
"filterByCategory": "[to be translated]:按分类筛选",
|
||||
"allCategories": "[to be translated]:全部",
|
||||
"uncategorized": "[to be translated]:未分类",
|
||||
"addList": "[to be translated]:添加记忆列表",
|
||||
"editList": "[to be translated]:编辑记忆列表",
|
||||
"listName": "[to be translated]:列表名称",
|
||||
"listNamePlaceholder": "[to be translated]:输入列表名称",
|
||||
"listDescription": "[to be translated]:列表描述",
|
||||
"listDescriptionPlaceholder": "[to be translated]:输入列表描述(可选)",
|
||||
"noLists": "[to be translated]:暂无记忆列表",
|
||||
"confirmDeleteList": "[to be translated]:确认删除列表",
|
||||
"confirmDeleteListContent": "[to be translated]:确定要删除 {{name}} 列表吗?此操作将同时删除列表中的所有记忆,且不可恢复。",
|
||||
"toggleActive": "[to be translated]:切换激活状态",
|
||||
"clearConfirmContentList": "[to be translated]:确定要清空 {{name}} 中的所有记忆吗?此操作不可恢复。",
|
||||
"shortMemory": "[to be translated]:短期记忆",
|
||||
"longMemory": "[to be translated]:长期记忆",
|
||||
"toggleShortMemoryActive": "[to be translated]:切换短期记忆功能",
|
||||
"addShortMemory": "[to be translated]:添加短期记忆",
|
||||
"addShortMemoryPlaceholder": "[to be translated]:输入短期记忆内容,只在当前对话中有效",
|
||||
"noShortMemories": "[to be translated]:暂无短期记忆",
|
||||
"noCurrentTopic": "[to be translated]:请先选择一个对话话题",
|
||||
"confirmDelete": "[to be translated]:确认删除",
|
||||
"confirmDeleteContent": "[to be translated]:确定要删除这条短期记忆吗?",
|
||||
"delete": "[to be translated]:删除"
|
||||
}
|
||||
},
|
||||
"translate": {
|
||||
@@ -1393,4 +1447,4 @@
|
||||
"visualization": "Визуализация"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -498,6 +498,12 @@
|
||||
"copied": "已复制",
|
||||
"copy.failed": "复制失败",
|
||||
"copy.success": "复制成功",
|
||||
"copy_id": "复制消息ID",
|
||||
"id_copied": "消息ID已复制",
|
||||
"id_found": "已找到原始消息",
|
||||
"reference": "引用消息",
|
||||
"reference.error": "无法找到原始消息",
|
||||
"referenced_message": "引用的消息",
|
||||
"error.chunk_overlap_too_large": "分段重叠不能大于分段大小",
|
||||
"error.dimension_too_large": "内容尺寸过大",
|
||||
"error.enter.api.host": "请输入您的 API 地址",
|
||||
@@ -1024,6 +1030,188 @@
|
||||
"launch.onboot": "开机自动启动",
|
||||
"launch.title": "启动",
|
||||
"launch.totray": "启动时最小化到托盘",
|
||||
"memory": {
|
||||
"historicalContext": {
|
||||
"title": "历史对话上下文",
|
||||
"description": "允许AI在需要时自动引用历史对话,以提供更连贯的回答。",
|
||||
"enable": "启用历史对话上下文",
|
||||
"enableTip": "启用后,AI会在需要时自动分析并引用历史对话,以提供更连贯的回答",
|
||||
"analyzeModelTip": "选择用于历史对话上下文分析的模型,建议选择响应较快的模型"
|
||||
},
|
||||
"title": "记忆功能",
|
||||
"description": "管理AI助手的长期记忆,自动分析对话并提取重要信息",
|
||||
"enableMemory": "启用记忆功能",
|
||||
"enableAutoAnalyze": "启用自动分析",
|
||||
"analyzeModel": "长期记忆分析模型",
|
||||
"shortMemoryAnalyzeModel": "短期记忆分析模型",
|
||||
"selectModel": "选择模型",
|
||||
"memoriesList": "记忆列表",
|
||||
"memoryLists": "记忆角色",
|
||||
"addMemory": "添加记忆",
|
||||
"editMemory": "编辑记忆",
|
||||
"clearAll": "清空全部",
|
||||
"noMemories": "暂无记忆",
|
||||
"memoryPlaceholder": "输入要记住的内容",
|
||||
"addSuccess": "记忆添加成功",
|
||||
"editSuccess": "记忆编辑成功",
|
||||
"deleteSuccess": "记忆删除成功",
|
||||
"clearSuccess": "记忆清空成功",
|
||||
"clearConfirmTitle": "确认清空",
|
||||
"clearConfirmContent": "确定要清空所有记忆吗?此操作无法撤销。",
|
||||
"listView": "列表视图",
|
||||
"mindmapView": "思维导图",
|
||||
"centerNodeLabel": "用户记忆",
|
||||
"manualAnalyze": "手动分析",
|
||||
"analyzeNow": "立即分析",
|
||||
"startingAnalysis": "开始分析...",
|
||||
"cannotAnalyze": "无法分析,请检查设置",
|
||||
"resetAnalyzingState": "重置分析状态",
|
||||
"filterSensitiveInfo": "过滤敏感信息",
|
||||
"filterSensitiveInfoTip": "启用后,记忆功能将不会提取API密钥、密码等敏感信息",
|
||||
"resetLongTermMemory": "重置分析标记",
|
||||
"resetLongTermMemorySuccess": "长期记忆分析标记已重置",
|
||||
"resetLongTermMemoryNoChange": "没有需要重置的分析标记",
|
||||
"resetLongTermMemoryError": "重置长期记忆分析标记失败",
|
||||
"saveAllSettings": "保存所有设置",
|
||||
"saveAllSettingsDescription": "将所有记忆功能的设置保存到文件中,确保应用重启后设置仍然生效。",
|
||||
"saveAllSettingsSuccess": "所有设置已成功保存",
|
||||
"saveAllSettingsError": "保存设置失败",
|
||||
"analyzeConversation": "分析对话",
|
||||
"shortMemoryAnalysisSuccess": "分析成功",
|
||||
"shortMemoryAnalysisSuccessContent": "已成功提取并添加重要信息到短期记忆",
|
||||
"shortMemoryAnalysisNoNew": "无新信息",
|
||||
"shortMemoryAnalysisNoNewContent": "未发现新的重要信息或所有信息已存在",
|
||||
"shortMemoryAnalysisError": "分析失败",
|
||||
"shortMemoryAnalysisErrorContent": "分析对话内容时出错",
|
||||
"performanceStats": "性能统计",
|
||||
"totalAnalyses": "总分析次数",
|
||||
"successRate": "成功率",
|
||||
"avgAnalysisTime": "平均分析时间",
|
||||
"priorityManagement": {
|
||||
"title": "智能优先级与时效性管理",
|
||||
"description": "智能管理记忆的优先级、衰减和鲜度,确保最重要和最相关的记忆优先显示。",
|
||||
"enable": "启用智能优先级管理",
|
||||
"enableTip": "启用后,系统将根据重要性、访问频率和时间因素自动排序记忆",
|
||||
"decay": "记忆衰减",
|
||||
"decayRate": "衰减速率",
|
||||
"decayRateTip": "值越大,记忆衰减越快。0.05表示每天衰减5%",
|
||||
"freshness": "记忆鲜度",
|
||||
"freshnessTip": "考虑记忆的创建时间和最后访问时间,优先显示较新的记忆",
|
||||
"updateNow": "立即更新",
|
||||
"updateNowTip": "立即更新所有记忆的优先级排序",
|
||||
"update": "更新"
|
||||
},
|
||||
"contextualRecommendation": {
|
||||
"title": "上下文感知记忆推荐",
|
||||
"description": "根据当前对话上下文,智能推荐相关的记忆内容。",
|
||||
"enable": "启用上下文感知记忆推荐",
|
||||
"enableTip": "启用后,系统将根据当前对话上下文自动推荐相关记忆",
|
||||
"autoRecommend": "自动推荐记忆",
|
||||
"autoRecommendTip": "启用后,系统将定期自动分析当前对话并推荐相关记忆",
|
||||
"threshold": "推荐阈值",
|
||||
"thresholdTip": "设置记忆推荐的相似度阈值,值越高要求越严格",
|
||||
"clearRecommendations": "清除当前推荐",
|
||||
"clearRecommendationsTip": "清除当前的记忆推荐列表",
|
||||
"clear": "清除",
|
||||
"decayTip": "随着时间推移,未访问的记忆重要性会逐渐降低",
|
||||
"decayRate": "衰减速率",
|
||||
"decayRateTip": "值越大,记忆衰减越快。0.05表示每天衰减5%",
|
||||
"freshness": "记忆鲜度",
|
||||
"freshnessTip": "考虑记忆的创建时间和最后访问时间,优先显示较新的记忆",
|
||||
"updateNow": "立即更新优先级",
|
||||
"updateNowTip": "手动更新所有记忆的优先级和鲜度评分",
|
||||
"update": "更新"
|
||||
},
|
||||
"deduplication": {
|
||||
"title": "记忆去重与合并",
|
||||
"description": "分析记忆库中的相似记忆,提供智能合并建议。",
|
||||
"selectList": "选择记忆列表",
|
||||
"allLists": "所有列表",
|
||||
"selectTopic": "选择话题",
|
||||
"similarityThreshold": "相似度阈值",
|
||||
"startAnalysis": "开始分析",
|
||||
"help": "帮助",
|
||||
"helpTitle": "记忆去重与合并帮助",
|
||||
"helpContent1": "该功能会分析记忆库中的相似记忆,并提供合并建议。",
|
||||
"helpContent2": "相似度阈值决定了两条记忆被认为相似的程度,值越高,要求越严格。",
|
||||
"helpContent3": "应用结果后,相似的记忆将被合并为一条新记忆,原记忆将被删除。",
|
||||
"analyzing": "分析中...",
|
||||
"noSimilarMemories": "未发现相似记忆",
|
||||
"similarGroups": "相似记忆组",
|
||||
"group": "组",
|
||||
"items": "项",
|
||||
"originalMemories": "原始记忆",
|
||||
"mergedResult": "合并结果",
|
||||
"other": "其他",
|
||||
"applyResults": "应用结果",
|
||||
"confirmApply": "确认应用去重结果",
|
||||
"confirmApplyContent": "应用去重结果将合并相似记忆并删除原记忆,此操作不可撤销。确定要继续吗?",
|
||||
"applySuccess": "应用成功",
|
||||
"applySuccessContent": "记忆去重与合并已成功应用"
|
||||
},
|
||||
"shortMemoryDeduplication": {
|
||||
"title": "短期记忆去重与合并",
|
||||
"description": "分析短期记忆中的相似记忆,提供智能合并建议。",
|
||||
"selectTopic": "选择话题",
|
||||
"similarityThreshold": "相似度阈值",
|
||||
"startAnalysis": "开始分析",
|
||||
"help": "帮助",
|
||||
"helpTitle": "短期记忆去重与合并帮助",
|
||||
"helpContent1": "该功能会分析短期记忆中的相似记忆,并提供合并建议。",
|
||||
"helpContent2": "相似度阈值决定了两条记忆被认为相似的程度,值越高,要求越严格。",
|
||||
"helpContent3": "应用结果后,相似的记忆将被合并为一条新记忆,原记忆将被删除。",
|
||||
"analyzing": "分析中...",
|
||||
"noSimilarMemories": "未发现相似记忆",
|
||||
"similarGroups": "相似记忆组",
|
||||
"group": "组",
|
||||
"items": "项",
|
||||
"originalMemories": "原始记忆",
|
||||
"mergedResult": "合并结果",
|
||||
"other": "其他",
|
||||
"applyResults": "应用结果",
|
||||
"confirmApply": "确认应用去重结果",
|
||||
"confirmApplyContent": "应用去重结果将合并相似记忆并删除原记忆,此操作不可撤销。确定要继续吗?",
|
||||
"applySuccess": "应用成功",
|
||||
"applySuccessContent": "短期记忆去重与合并已成功应用"
|
||||
},
|
||||
"selectTopic": "选择话题",
|
||||
"selectTopicPlaceholder": "选择要分析的话题",
|
||||
"filterByCategory": "按分类筛选",
|
||||
"allCategories": "全部",
|
||||
"uncategorized": "未分类",
|
||||
"addList": "添加记忆列表",
|
||||
"editList": "编辑记忆列表",
|
||||
"listName": "列表名称",
|
||||
"listNamePlaceholder": "输入列表名称",
|
||||
"listDescription": "列表描述",
|
||||
"listDescriptionPlaceholder": "输入列表描述(可选)",
|
||||
"noLists": "暂无记忆列表",
|
||||
"confirmDeleteList": "确认删除列表",
|
||||
"confirmDeleteListContent": "确定要删除 {{name}} 列表吗?此操作将同时删除列表中的所有记忆,且不可恢复。",
|
||||
"toggleActive": "切换激活状态",
|
||||
"clearConfirmContentList": "确定要清空 {{name}} 中的所有记忆吗?此操作不可恢复。",
|
||||
"shortMemory": "短期记忆",
|
||||
"loading": "加载中...",
|
||||
"longMemory": "长期记忆",
|
||||
"shortMemorySettings": "短期记忆设置",
|
||||
"shortMemoryDescription": "管理与当前对话相关的短期记忆",
|
||||
"longMemorySettings": "长期记忆设置",
|
||||
"longMemoryDescription": "管理跨对话的长期记忆",
|
||||
"toggleShortMemoryActive": "切换短期记忆功能",
|
||||
"addShortMemory": "添加短期记忆",
|
||||
"addShortMemoryPlaceholder": "输入短期记忆内容,只在当前对话中有效",
|
||||
"noShortMemories": "暂无短期记忆",
|
||||
"noCurrentTopic": "请先选择一个对话话题",
|
||||
"confirmDelete": "确认删除",
|
||||
"confirmDeleteContent": "确定要删除这条短期记忆吗?",
|
||||
"confirmDeleteAll": "确认删除全部",
|
||||
"confirmDeleteAllContent": "确定要删除该话题下的所有短期记忆吗?",
|
||||
"delete": "删除",
|
||||
"cancel": "取消",
|
||||
"allTopics": "所有话题",
|
||||
"noTopics": "没有话题",
|
||||
"shortMemoriesByTopic": "按话题分组的短期记忆"
|
||||
},
|
||||
"mcp": {
|
||||
"actions": "操作",
|
||||
"active": "启用",
|
||||
|
||||
@@ -1351,6 +1351,64 @@
|
||||
"privacy": {
|
||||
"title": "隱私設定",
|
||||
"enable_privacy_mode": "匿名發送錯誤報告和資料統計"
|
||||
},
|
||||
"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": "短期記憶",
|
||||
"longMemory": "長期記憶",
|
||||
"toggleShortMemoryActive": "切換短期記憶功能",
|
||||
"addShortMemory": "添加短期記憶",
|
||||
"addShortMemoryPlaceholder": "輸入短期記憶內容,只在當前對話中有效",
|
||||
"noShortMemories": "暫無短期記憶",
|
||||
"noCurrentTopic": "請先選擇一個對話話題",
|
||||
"confirmDelete": "確認刪除",
|
||||
"confirmDeleteContent": "確定要刪除這條短期記憶嗎?",
|
||||
"delete": "刪除",
|
||||
"performanceStats": "性能統計",
|
||||
"totalAnalyses": "總分析次數",
|
||||
"successRate": "成功率",
|
||||
"avgAnalysisTime": "平均分析時間"
|
||||
}
|
||||
},
|
||||
"translate": {
|
||||
@@ -1393,4 +1451,4 @@
|
||||
"visualization": "視覺化"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
|
||||
import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { checkRateLimit, getUserMessage } from '@renderer/services/MessagesService'
|
||||
import { checkRateLimit, findMessageById, getUserMessage } from '@renderer/services/MessagesService'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { estimateMessageUsage, estimateTextTokens as estimateTxtTokens } from '@renderer/services/TokenService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
@@ -180,9 +180,135 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE)
|
||||
|
||||
try {
|
||||
// 检查用户输入是否包含消息ID
|
||||
const uuidRegex = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i
|
||||
|
||||
// 从文本中提取所有消息ID
|
||||
const matches = text.match(new RegExp(uuidRegex, 'g'))
|
||||
|
||||
// 如果只有ID且没有其他内容,则直接查找原始消息
|
||||
if (matches && matches.length > 0 && text.trim() === matches.join(' ')) {
|
||||
try {
|
||||
// 创建引用消息
|
||||
const userMessage = getUserMessage({ assistant, topic, type: 'text', content: '' })
|
||||
userMessage.referencedMessages = []
|
||||
|
||||
// 处理所有匹配到的ID
|
||||
let foundAnyMessage = false
|
||||
for (const messageId of matches) {
|
||||
console.log(`[引用消息] 尝试查找消息ID: ${messageId}`)
|
||||
const originalMessage = await findMessageById(messageId)
|
||||
if (originalMessage) {
|
||||
userMessage.referencedMessages.push({
|
||||
id: originalMessage.id,
|
||||
content: originalMessage.content,
|
||||
role: originalMessage.role,
|
||||
createdAt: originalMessage.createdAt
|
||||
})
|
||||
foundAnyMessage = true
|
||||
console.log(`[引用消息] 找到消息ID: ${messageId}`)
|
||||
} else {
|
||||
console.log(`[引用消息] 未找到消息ID: ${messageId}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (foundAnyMessage) {
|
||||
// 发送引用消息
|
||||
userMessage.usage = await estimateMessageUsage(userMessage)
|
||||
currentMessageId.current = userMessage.id
|
||||
|
||||
dispatch(
|
||||
_sendMessage(userMessage, assistant, topic, {
|
||||
mentions: mentionModels
|
||||
})
|
||||
)
|
||||
|
||||
// 清空输入框
|
||||
setText('')
|
||||
setFiles([])
|
||||
setTimeout(() => setText(''), 500)
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
setExpend(false)
|
||||
|
||||
window.message.success({
|
||||
content:
|
||||
t('message.ids_found', { count: userMessage.referencedMessages.length }) ||
|
||||
`已找到${userMessage.referencedMessages.length}条原始消息`,
|
||||
key: 'message-id-found'
|
||||
})
|
||||
return
|
||||
} else {
|
||||
window.message.error({
|
||||
content: t('message.id_not_found') || '未找到原始消息',
|
||||
key: 'message-id-not-found'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[引用消息] 查找消息ID时出错:`, error)
|
||||
window.message.error({ content: t('message.id_error') || '查找原始消息时出错', key: 'message-id-error' })
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是单独的ID或者没有找到原始消息,则正常发送消息
|
||||
// 先检查消息内容是否包含消息ID,如果是则将其替换为空字符串
|
||||
let messageContent = text
|
||||
|
||||
// 如果消息内容包含消息ID,则将其替换为空字符串
|
||||
if (matches && matches.length > 0) {
|
||||
// 检查是否是纯消息ID
|
||||
const isOnlyUUID = text.trim() === matches[0]
|
||||
if (isOnlyUUID) {
|
||||
messageContent = ''
|
||||
} else {
|
||||
// 如果消息内容包含消息ID,则将消息ID替换为空字符串
|
||||
for (const match of matches) {
|
||||
messageContent = messageContent.replace(match, '')
|
||||
}
|
||||
// 去除多余的空格
|
||||
messageContent = messageContent.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch the sendMessage action with all options
|
||||
const uploadedFiles = await FileManager.uploadFiles(files)
|
||||
const userMessage = getUserMessage({ assistant, topic, type: 'text', content: text })
|
||||
const userMessage = getUserMessage({ assistant, topic, type: 'text', content: messageContent })
|
||||
|
||||
// 如果消息内容包含消息ID,则添加引用
|
||||
if (matches && matches.length > 0) {
|
||||
try {
|
||||
// 初始化引用消息数组
|
||||
userMessage.referencedMessages = []
|
||||
|
||||
// 处理所有匹配到的ID
|
||||
for (const messageId of matches) {
|
||||
console.log(`[引用消息] 尝试查找消息ID作为引用: ${messageId}`)
|
||||
const originalMessage = await findMessageById(messageId)
|
||||
if (originalMessage) {
|
||||
userMessage.referencedMessages.push({
|
||||
id: originalMessage.id,
|
||||
content: originalMessage.content,
|
||||
role: originalMessage.role,
|
||||
createdAt: originalMessage.createdAt
|
||||
})
|
||||
console.log(`[引用消息] 找到消息ID作为引用: ${messageId}`)
|
||||
} else {
|
||||
console.log(`[引用消息] 未找到消息ID作为引用: ${messageId}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到了引用消息,显示成功提示
|
||||
if (userMessage.referencedMessages.length > 0) {
|
||||
window.message.success({
|
||||
content:
|
||||
t('message.ids_found', { count: userMessage.referencedMessages.length }) ||
|
||||
`已找到${userMessage.referencedMessages.length}条原始消息`,
|
||||
key: 'message-id-found'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[引用消息] 查找消息ID作为引用时出错:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadedFiles) {
|
||||
userMessage.files = uploadedFiles
|
||||
@@ -388,6 +514,20 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const isEnterPressed = event.keyCode == 13
|
||||
|
||||
// 检查是否是消息ID格式
|
||||
if (isEnterPressed && !event.shiftKey) {
|
||||
const uuidRegex = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i
|
||||
const currentText = text.trim()
|
||||
const isUUID = uuidRegex.test(currentText) && currentText.length === 36
|
||||
|
||||
if (isUUID) {
|
||||
// 如果是消息ID格式,则不显示ID在对话中
|
||||
event.preventDefault()
|
||||
sendMessage()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 按下Tab键,自动选中${xxx}
|
||||
if (event.key === 'Tab' && inputFocus) {
|
||||
event.preventDefault()
|
||||
@@ -536,7 +676,19 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
|
||||
const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newText = e.target.value
|
||||
setText(newText)
|
||||
|
||||
// 检查是否包含UUID格式的消息ID
|
||||
const uuidRegex = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i
|
||||
const matches = newText.match(new RegExp(uuidRegex, 'g'))
|
||||
|
||||
// 如果输入的内容只是一个UUID,不更新文本框内容,直接处理引用
|
||||
if (matches && matches.length === 1 && newText.trim() === matches[0]) {
|
||||
// 不立即更新文本框,等待用户按下回车键时再处理
|
||||
setText(newText)
|
||||
} else {
|
||||
// 正常更新文本框内容
|
||||
setText(newText)
|
||||
}
|
||||
|
||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||
const cursorPosition = textArea?.selectionStart ?? 0
|
||||
@@ -559,8 +711,41 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
async (event: ClipboardEvent) => {
|
||||
const clipboardText = event.clipboardData?.getData('text')
|
||||
if (clipboardText) {
|
||||
// Prioritize the text when pasting.
|
||||
// handled by the default event
|
||||
// 检查粘贴的内容是否是消息ID
|
||||
const uuidRegex = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i
|
||||
const isUUID = uuidRegex.test(clipboardText.trim()) && clipboardText.trim().length === 36
|
||||
|
||||
if (isUUID) {
|
||||
// 如果是消息ID,则阻止默认粘贴行为,自定义处理
|
||||
event.preventDefault()
|
||||
|
||||
// 获取当前文本框的内容和光标位置
|
||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||
if (textArea) {
|
||||
const currentText = textArea.value
|
||||
const cursorPosition = textArea.selectionStart
|
||||
const cursorEnd = textArea.selectionEnd
|
||||
|
||||
// 如果有选中文本,则替换选中文本;否则在光标位置插入
|
||||
const newText =
|
||||
currentText.substring(0, cursorPosition) + clipboardText.trim() + currentText.substring(cursorEnd)
|
||||
|
||||
setText(newText)
|
||||
|
||||
// 将光标移到插入的ID后面
|
||||
const newCursorPosition = cursorPosition + clipboardText.trim().length
|
||||
setTimeout(() => {
|
||||
if (textArea) {
|
||||
textArea.focus()
|
||||
textArea.setSelectionRange(newCursorPosition, newCursorPosition)
|
||||
}
|
||||
}, 0)
|
||||
} else {
|
||||
// 如果无法获取textArea,则直接设置文本
|
||||
setText(clipboardText.trim())
|
||||
}
|
||||
}
|
||||
// 其他文本内容由默认事件处理
|
||||
} else {
|
||||
for (const file of event.clipboardData?.files || []) {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -25,7 +25,7 @@ import ImagePreview from './ImagePreview'
|
||||
import Link from './Link'
|
||||
|
||||
const ALLOWED_ELEMENTS =
|
||||
/<(style|p|div|span|b|i|strong|em|ul|ol|li|table|tr|td|th|thead|tbody|h[1-6]|blockquote|pre|code|br|hr|svg|path|circle|rect|line|polyline|polygon|text|g|defs|title|desc|tspan|sub|sup)/i
|
||||
/<(style|p|div|span|b|i|strong|em|ul|ol|li|table|tr|td|th|thead|tbody|h[1-6]|blockquote|pre|code|br|hr|svg|path|circle|rect|line|polyline|polygon|text|g|defs|title|desc|tspan|sub|sup|think)/i
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
@@ -56,7 +56,27 @@ const Markdown: FC<Props> = ({ message }) => {
|
||||
a: (props: any) => <Link {...props} citationData={parseJSON(findCitationInChildren(props.children))} />,
|
||||
code: CodeBlock,
|
||||
img: ImagePreview,
|
||||
pre: (props: any) => <pre style={{ overflow: 'visible' }} {...props} />
|
||||
pre: (props: any) => <pre style={{ overflow: 'visible' }} {...props} />,
|
||||
// 自定义处理think标签
|
||||
think: (props: any) => {
|
||||
// 将think标签内容渲染为带样式的div
|
||||
return (
|
||||
<div
|
||||
className="thinking-content"
|
||||
style={{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||||
padding: '10px 15px',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '15px',
|
||||
borderLeft: '3px solid var(--color-primary)',
|
||||
fontStyle: 'italic',
|
||||
color: 'var(--color-text-2)'
|
||||
}}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '5px' }}>思考过程:</div>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
} as Partial<Components>
|
||||
return baseComponents
|
||||
}, [])
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { Assistant, Message, Topic } from '@renderer/types'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { Divider, Dropdown } from 'antd'
|
||||
import { ItemType } from 'antd/es/menu/interface'
|
||||
import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -65,7 +66,11 @@ const MessageItem: FC<Props> = ({
|
||||
|
||||
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
const _selectedText = window.getSelection()?.toString()
|
||||
const _selectedText = window.getSelection()?.toString() || ''
|
||||
|
||||
// 无论是否选中文本,都设置上下文菜单位置
|
||||
setContextMenuPosition({ x: e.clientX, y: e.clientY })
|
||||
|
||||
if (_selectedText) {
|
||||
const quotedText =
|
||||
_selectedText
|
||||
@@ -73,8 +78,10 @@ const MessageItem: FC<Props> = ({
|
||||
.map((line) => `> ${line}`)
|
||||
.join('\n') + '\n-------------'
|
||||
setSelectedQuoteText(quotedText)
|
||||
setContextMenuPosition({ x: e.clientX, y: e.clientY })
|
||||
setSelectedText(_selectedText)
|
||||
} else {
|
||||
setSelectedQuoteText('')
|
||||
setSelectedText('')
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -134,7 +141,7 @@ const MessageItem: FC<Props> = ({
|
||||
{contextMenuPosition && (
|
||||
<Dropdown
|
||||
overlayStyle={{ left: contextMenuPosition.x, top: contextMenuPosition.y, zIndex: 1000 }}
|
||||
menu={{ items: getContextMenuItems(t, selectedQuoteText, selectedText) }}
|
||||
menu={{ items: getContextMenuItems(t, selectedQuoteText, selectedText, message) }}
|
||||
open={true}
|
||||
trigger={['contextMenu']}>
|
||||
<div />
|
||||
@@ -181,23 +188,46 @@ const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolea
|
||||
: undefined
|
||||
}
|
||||
|
||||
const getContextMenuItems = (t: (key: string) => string, selectedQuoteText: string, selectedText: string) => [
|
||||
{
|
||||
key: 'copy',
|
||||
label: t('common.copy'),
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(selectedText)
|
||||
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'quote',
|
||||
label: t('chat.message.quote'),
|
||||
onClick: () => {
|
||||
EventEmitter.emit(EVENT_NAMES.QUOTE_TEXT, selectedQuoteText)
|
||||
}
|
||||
const getContextMenuItems = (
|
||||
t: (key: string) => string,
|
||||
selectedQuoteText: string,
|
||||
selectedText: string,
|
||||
message: Message
|
||||
): ItemType[] => {
|
||||
const items: ItemType[] = []
|
||||
|
||||
// 只有在选中文本时,才添加复制和引用选项
|
||||
if (selectedText) {
|
||||
items.push({
|
||||
key: 'copy',
|
||||
label: t('common.copy'),
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(selectedText)
|
||||
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
||||
}
|
||||
})
|
||||
|
||||
items.push({
|
||||
key: 'quote',
|
||||
label: t('chat.message.quote'),
|
||||
onClick: () => {
|
||||
EventEmitter.emit(EVENT_NAMES.QUOTE_TEXT, selectedQuoteText)
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
// 添加复制消息ID选项,但不显示ID
|
||||
items.push({
|
||||
key: 'copy_id',
|
||||
label: t('message.copy_id') || '复制消息ID',
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(message.id)
|
||||
window.message.success({ content: t('message.id_copied') || '消息ID已复制', key: 'copy-message-id' })
|
||||
}
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
const MessageContainer = styled.div`
|
||||
display: flex;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { getBriefInfo } from '@renderer/utils'
|
||||
import { withMessageThought } from '@renderer/utils/formats'
|
||||
import { Divider, Flex } from 'antd'
|
||||
import { Collapse, Divider, Flex } from 'antd'
|
||||
import { clone } from 'lodash'
|
||||
import React, { Fragment, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -203,8 +203,100 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
|
||||
<Flex gap="8px" wrap style={{ marginBottom: 10 }}>
|
||||
{message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)}
|
||||
</Flex>
|
||||
<MessageThought message={message} />
|
||||
<MessageTools message={message} />
|
||||
{message.referencedMessages && message.referencedMessages.length > 0 && (
|
||||
<div>
|
||||
{message.referencedMessages.map((refMsg, index) => (
|
||||
<Collapse
|
||||
key={refMsg.id}
|
||||
className="reference-collapse"
|
||||
defaultActiveKey={['1']}
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<div className="reference-header-label">
|
||||
<span className="reference-title">
|
||||
{t('message.referenced_message')}{' '}
|
||||
{message.referencedMessages && message.referencedMessages.length > 1
|
||||
? `(${index + 1}/${message.referencedMessages.length})`
|
||||
: ''}
|
||||
</span>
|
||||
<span className="reference-role">{refMsg.role === 'user' ? t('common.you') : 'AI'}</span>
|
||||
</div>
|
||||
),
|
||||
extra: (
|
||||
<span
|
||||
className="reference-id"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
navigator.clipboard.writeText(refMsg.id)
|
||||
window.message.success({
|
||||
content: t('message.id_copied') || '消息ID已复制',
|
||||
key: 'copy-reference-id'
|
||||
})
|
||||
}}>
|
||||
ID: {refMsg.id}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div className="reference-content">
|
||||
<div className="reference-text">{refMsg.content}</div>
|
||||
<div className="reference-bottom-spacing"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 兼容旧版本的referencedMessage */}
|
||||
{!message.referencedMessages && (message as any).referencedMessage && (
|
||||
<Collapse
|
||||
className="reference-collapse"
|
||||
defaultActiveKey={['1']}
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<div className="reference-header-label">
|
||||
<span className="reference-title">{t('message.referenced_message')}</span>
|
||||
<span className="reference-role">
|
||||
{(message as any).referencedMessage.role === 'user' ? t('common.you') : 'AI'}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
extra: (
|
||||
<span
|
||||
className="reference-id"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
navigator.clipboard.writeText((message as any).referencedMessage.id)
|
||||
window.message.success({
|
||||
content: t('message.id_copied') || '消息ID已复制',
|
||||
key: 'copy-reference-id'
|
||||
})
|
||||
}}>
|
||||
ID: {(message as any).referencedMessage.id}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div className="reference-content">
|
||||
<div className="reference-text">{(message as any).referencedMessage.content}</div>
|
||||
<div className="reference-bottom-spacing"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<div className="message-content-tools">
|
||||
<MessageThought message={message} />
|
||||
<MessageTools message={message} />
|
||||
</div>
|
||||
<Markdown message={{ ...message, content: processedContent.replace(toolUseRegex, '') }} />
|
||||
{message.metadata?.generateImage && <MessageImage message={message} />}
|
||||
{message.translatedContent && (
|
||||
@@ -312,4 +404,132 @@ const SearchEntryPoint = styled.div`
|
||||
margin: 10px 2px;
|
||||
`
|
||||
|
||||
// 引用消息样式 - 使用全局样式
|
||||
const referenceStyles = `
|
||||
.reference-collapse {
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: 8px !important;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg-1) !important;
|
||||
|
||||
.ant-collapse-header {
|
||||
padding: 2px 8px !important;
|
||||
background-color: var(--color-bg-2);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 18px;
|
||||
min-height: 18px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.ant-collapse-expand-icon {
|
||||
height: 18px;
|
||||
line-height: 14px;
|
||||
padding-top: 0 !important;
|
||||
margin-top: -2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.ant-collapse-header-text {
|
||||
flex: 0 1 auto;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.ant-collapse-extra {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 10px;
|
||||
padding-right: 0;
|
||||
position: relative;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.reference-header-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 14px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.reference-title {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.reference-role {
|
||||
color: var(--color-text-2);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.reference-id {
|
||||
color: var(--color-text-3);
|
||||
font-size: 9px;
|
||||
cursor: pointer;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s ease;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-3);
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-collapse-extra {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 12px !important;
|
||||
padding-top: 8px !important;
|
||||
padding-bottom: 2px !important;
|
||||
}
|
||||
|
||||
.reference-content {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.reference-text {
|
||||
color: var(--color-text-1);
|
||||
font-size: 14px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.reference-bottom-spacing {
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// 将样式添加到文档中
|
||||
try {
|
||||
if (typeof document !== 'undefined') {
|
||||
const styleElement = document.createElement('style')
|
||||
styleElement.textContent =
|
||||
referenceStyles +
|
||||
`
|
||||
.message-content-tools {
|
||||
margin-top: 20px;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(styleElement)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to add reference styles:', error)
|
||||
}
|
||||
|
||||
export default React.memo(MessageContent)
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -1,16 +1,93 @@
|
||||
import { LinkOutlined } from '@ant-design/icons'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { findMessageById } from '@renderer/services/MessagesService'
|
||||
import { Message } from '@renderer/types'
|
||||
import { Button, Modal, Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
// 添加引用消息的弹窗组件
|
||||
const ReferenceModal: React.FC<{ message: Message | null; visible: boolean; onClose: () => void }> = ({
|
||||
message,
|
||||
visible,
|
||||
onClose
|
||||
}) => {
|
||||
if (!message) return null
|
||||
|
||||
return (
|
||||
<Modal title={`引用消息`} open={visible} onCancel={onClose} footer={null} width={600}>
|
||||
<ReferenceContent>
|
||||
<div className="message-role">{message.role === 'user' ? t('common.you') : 'AI'}</div>
|
||||
<div className="message-content">{message.content}</div>
|
||||
<div className="message-time">{new Date(message.createdAt).toLocaleString()}</div>
|
||||
</ReferenceContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const ReferenceContent = styled.div`
|
||||
padding: 10px;
|
||||
|
||||
.message-role {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
text-align: right;
|
||||
}
|
||||
`
|
||||
|
||||
const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({ message, isLastMessage }) => {
|
||||
const { generating } = useRuntime()
|
||||
const [isModalVisible, setIsModalVisible] = useState(false)
|
||||
const [referencedMessage, setReferencedMessage] = useState<Message | null>(null)
|
||||
|
||||
// 渲染引用消息弹窗
|
||||
const renderReferenceModal = () => {
|
||||
return <ReferenceModal message={referencedMessage} visible={isModalVisible} onClose={handleModalClose} />
|
||||
}
|
||||
|
||||
const locateMessage = () => {
|
||||
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, false)
|
||||
}
|
||||
|
||||
const showReferenceModal = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation() // 防止触发父元素的点击事件
|
||||
|
||||
try {
|
||||
// 复制ID到剪贴板,便于用户手动使用
|
||||
navigator.clipboard.writeText(message.id)
|
||||
|
||||
// 查找原始消息
|
||||
const originalMessage = await findMessageById(message.id)
|
||||
if (originalMessage) {
|
||||
setReferencedMessage(originalMessage)
|
||||
setIsModalVisible(true)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to find referenced message:', error)
|
||||
window.message.error({
|
||||
content: t('message.reference.error') || '无法找到原始消息',
|
||||
key: 'reference-message-error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalClose = () => {
|
||||
setIsModalVisible(false)
|
||||
}
|
||||
|
||||
if (!message.usage) {
|
||||
return <div />
|
||||
}
|
||||
@@ -18,7 +95,16 @@ const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({
|
||||
if (message.role === 'user') {
|
||||
return (
|
||||
<MessageMetadata className="message-tokens" onClick={locateMessage}>
|
||||
Tokens: {message?.usage?.total_tokens}
|
||||
<span className="tokens">Tokens: {message?.usage?.total_tokens}</span>
|
||||
<Tooltip title={t('message.reference') || '引用消息'}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<LinkOutlined />}
|
||||
onClick={showReferenceModal}
|
||||
className="reference-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
</MessageMetadata>
|
||||
)
|
||||
}
|
||||
@@ -47,11 +133,25 @@ const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({
|
||||
<span className="tokens">
|
||||
Tokens: {message?.usage?.total_tokens} ↑{message?.usage?.prompt_tokens} ↓{message?.usage?.completion_tokens}
|
||||
</span>
|
||||
<Tooltip title={t('message.reference') || '引用消息'}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<LinkOutlined />}
|
||||
onClick={showReferenceModal}
|
||||
className="reference-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
</MessageMetadata>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
return (
|
||||
<>
|
||||
{renderReferenceModal()}
|
||||
{null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const MessageMetadata = styled.div`
|
||||
@@ -61,6 +161,10 @@ const MessageMetadata = styled.div`
|
||||
margin: 2px 0;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 5px;
|
||||
|
||||
.metrics {
|
||||
display: none;
|
||||
@@ -79,6 +183,26 @@ const MessageMetadata = styled.div`
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.reference-button {
|
||||
padding: 0;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-2);
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.anticon {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default MessgeTokens
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { FormOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { BookOutlined, FormOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import ShortMemoryPopup from '@renderer/components/Popups/ShortMemoryPopup'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
@@ -10,10 +11,11 @@ import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { analyzeAndAddShortMemories } from '@renderer/services/MemoryService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setNarrowMode } from '@renderer/store/settings'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
@@ -27,7 +29,7 @@ interface Props {
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
}
|
||||
|
||||
const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
const HeaderNavbar: FC<Props> = ({ activeAssistant, activeTopic }) => {
|
||||
const { assistant } = useAssistant(activeAssistant.id)
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { topicPosition, sidebarIcons, narrowMode } = useSettings()
|
||||
@@ -55,6 +57,28 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
dispatch(setNarrowMode(!narrowMode))
|
||||
}
|
||||
|
||||
const handleShowShortMemory = () => {
|
||||
if (activeTopic && activeTopic.id) {
|
||||
ShortMemoryPopup.show({ topicId: activeTopic.id })
|
||||
}
|
||||
}
|
||||
|
||||
const handleAnalyzeShortMemory = async () => {
|
||||
if (activeTopic && activeTopic.id) {
|
||||
try {
|
||||
const result = await analyzeAndAddShortMemories(activeTopic.id)
|
||||
if (result) {
|
||||
window.message.success(t('settings.memory.shortMemoryAnalysisSuccess') || '分析成功')
|
||||
} else {
|
||||
window.message.info(t('settings.memory.shortMemoryAnalysisNoNew') || '无新信息')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to analyze conversation for short memory:', error)
|
||||
window.message.error(t('settings.memory.shortMemoryAnalysisError') || '分析失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Navbar className="home-navbar">
|
||||
{showAssistants && (
|
||||
@@ -86,6 +110,14 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
</HStack>
|
||||
<HStack alignItems="center" gap={8}>
|
||||
<UpdateAppButton />
|
||||
<Tooltip title={t('settings.memory.shortMemory')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={handleShowShortMemory}>
|
||||
<BookOutlined />
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
<AnalyzeButton onClick={handleAnalyzeShortMemory}>
|
||||
{t('settings.memory.analyzeConversation') || '分析对话'}
|
||||
</AnalyzeButton>
|
||||
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={() => SearchPopup.show()}>
|
||||
<SearchOutlined />
|
||||
@@ -156,4 +188,17 @@ const NarrowIcon = styled(NavbarIcon)`
|
||||
}
|
||||
`
|
||||
|
||||
const AnalyzeButton = styled(Button)`
|
||||
font-size: 12px;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
-webkit-app-region: none;
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default HeaderNavbar
|
||||
|
||||
@@ -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,499 @@
|
||||
import { ClearOutlined, DeleteOutlined } from '@ant-design/icons'
|
||||
import { TopicManager } from '@renderer/hooks/useTopic'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import store from '@renderer/store'
|
||||
import { deleteShortMemory } from '@renderer/store/memory'
|
||||
import { Button, Collapse, Empty, List, Modal, Pagination, Tooltip, Typography } from 'antd'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
// 定义话题和记忆的接口
|
||||
interface TopicWithMemories {
|
||||
topic: {
|
||||
id: string
|
||||
name: string
|
||||
assistantId: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
messages: any[]
|
||||
}
|
||||
memories: ShortMemory[]
|
||||
currentPage?: number // 当前页码
|
||||
}
|
||||
|
||||
// 短期记忆接口
|
||||
interface ShortMemory {
|
||||
id: string
|
||||
content: string
|
||||
topicId: string
|
||||
createdAt: string
|
||||
updatedAt?: string // 可选属性
|
||||
}
|
||||
|
||||
// 记忆项组件的属性
|
||||
interface MemoryItemProps {
|
||||
memory: ShortMemory
|
||||
onDelete: (id: string) => void
|
||||
t: any
|
||||
index: number // 添加索引属性,用于显示序号
|
||||
}
|
||||
|
||||
// 样式组件
|
||||
const LoadingContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
color: var(--color-text-2);
|
||||
`
|
||||
|
||||
const StyledCollapse = styled(Collapse)`
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
.ant-collapse-item {
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-collapse-header {
|
||||
background-color: var(--color-bg-2);
|
||||
padding: 8px 16px !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 确保折叠图标不会遮挡内容 */
|
||||
.ant-collapse-expand-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 4px 0 !important; /* 减少上下内边距,保持左右为0 */
|
||||
}
|
||||
`
|
||||
|
||||
const CollapseHeader = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding-right: 24px; /* 为删除按钮留出空间 */
|
||||
|
||||
/* 左侧内容区域,包含话题名称和记忆数量 */
|
||||
> span {
|
||||
margin-right: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 删除按钮样式 */
|
||||
.ant-btn {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
const MemoryCount = styled.span`
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
z-index: 1; /* 确保计数显示在最上层 */
|
||||
`
|
||||
|
||||
const MemoryContent = styled.div`
|
||||
word-break: break-word;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 4px;
|
||||
padding: 4px 0;
|
||||
`
|
||||
|
||||
const PaginationContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid var(--color-border);
|
||||
`
|
||||
|
||||
const AnimatedListItem = styled(List.Item)`
|
||||
transition: all 0.3s ease;
|
||||
padding: 8px 24px; /* 增加左右内边距,减少上下内边距 */
|
||||
margin: 4px 0; /* 减少上下外边距 */
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.deleting {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
/* 增加内容区域的内边距 */
|
||||
.ant-list-item-meta {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
/* 调整内容区域的标题和描述文字间距 */
|
||||
.ant-list-item-meta-title {
|
||||
margin-bottom: 4px; /* 减少标题和描述之间的间距 */
|
||||
}
|
||||
|
||||
.ant-list-item-meta-description {
|
||||
padding-left: 4px;
|
||||
}
|
||||
`
|
||||
|
||||
// 记忆项组件
|
||||
const MemoryItem = memo(({ memory, onDelete, t, index }: MemoryItemProps) => {
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsDeleting(true)
|
||||
// 添加小延迟,让动画有时间播放
|
||||
setTimeout(() => {
|
||||
onDelete(memory.id)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatedListItem
|
||||
className={isDeleting ? 'deleting' : ''}
|
||||
actions={[
|
||||
<Tooltip title={t('settings.memory.delete')} key="delete">
|
||||
<Button icon={<DeleteOutlined />} onClick={handleDelete} type="text" danger />
|
||||
</Tooltip>
|
||||
]}>
|
||||
<List.Item.Meta
|
||||
title={
|
||||
<MemoryContent>
|
||||
<strong>{index + 1}. </strong>
|
||||
{memory.content}
|
||||
</MemoryContent>
|
||||
}
|
||||
description={new Date(memory.createdAt).toLocaleString()}
|
||||
/>
|
||||
</AnimatedListItem>
|
||||
)
|
||||
})
|
||||
|
||||
// 主组件
|
||||
const CollapsibleShortMemoryManager = () => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// 获取短期记忆
|
||||
const shortMemories = useAppSelector((state) => state.memory?.shortMemories || [])
|
||||
|
||||
// 本地状态
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [topicsWithMemories, setTopicsWithMemories] = useState<TopicWithMemories[]>([])
|
||||
const [activeKeys, setActiveKeys] = useState<string[]>([])
|
||||
|
||||
// 加载所有话题和对应的短期记忆
|
||||
useEffect(() => {
|
||||
const loadTopicsWithMemories = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// 从数据库获取所有话题
|
||||
const allTopics = await TopicManager.getAllTopics()
|
||||
|
||||
// 获取所有助手及其话题,确保我们使用与左侧列表相同的话题名称
|
||||
const assistants = store.getState().assistants?.assistants || []
|
||||
const allAssistantTopics = assistants.flatMap((assistant) => assistant.topics || [])
|
||||
|
||||
if (allTopics && allTopics.length > 0) {
|
||||
// 创建话题和记忆的映射
|
||||
const topicsMemories: TopicWithMemories[] = []
|
||||
|
||||
for (const dbTopic of allTopics) {
|
||||
// 获取该话题的短期记忆
|
||||
const topicMemories = shortMemories.filter((memory) => memory.topicId === dbTopic.id)
|
||||
|
||||
// 只添加有短期记忆的话题
|
||||
if (topicMemories.length > 0) {
|
||||
// 首先尝试从助手的话题列表中找到完整的话题信息
|
||||
let topicInfo = allAssistantTopics.find((topic) => topic.id === dbTopic.id)
|
||||
|
||||
// 如果在助手话题中找不到,则尝试从数据库获取
|
||||
if (!topicInfo) {
|
||||
try {
|
||||
const fullTopic = await TopicManager.getTopic(dbTopic.id)
|
||||
if (fullTopic) {
|
||||
// 数据库中的话题可能没有name属性,所以需要手动构造
|
||||
// 使用默认的话题名称格式
|
||||
const topicName = `话题 ${dbTopic.id.substring(0, 8)}`
|
||||
topicInfo = {
|
||||
id: dbTopic.id,
|
||||
assistantId: '',
|
||||
name: topicName,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
messages: []
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to get topic name for ${dbTopic.id}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果还是找不到,使用默认名称
|
||||
if (!topicInfo) {
|
||||
topicInfo = {
|
||||
id: dbTopic.id,
|
||||
assistantId: '',
|
||||
name: `话题 ${dbTopic.id.substring(0, 8)}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
messages: []
|
||||
}
|
||||
}
|
||||
|
||||
topicsMemories.push({
|
||||
topic: topicInfo,
|
||||
memories: topicMemories,
|
||||
currentPage: 1 // 初始化为第一页
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 按更新时间排序,最新的在前
|
||||
const sortedTopicsMemories = topicsMemories.sort((a, b) => {
|
||||
// 使用最新记忆的时间进行排序
|
||||
const aLatestMemory = a.memories.sort(
|
||||
(m1, m2) => new Date(m2.createdAt).getTime() - new Date(m1.createdAt).getTime()
|
||||
)[0]
|
||||
|
||||
const bLatestMemory = b.memories.sort(
|
||||
(m1, m2) => new Date(m2.createdAt).getTime() - new Date(m1.createdAt).getTime()
|
||||
)[0]
|
||||
|
||||
return new Date(bLatestMemory.createdAt).getTime() - new Date(aLatestMemory.createdAt).getTime()
|
||||
})
|
||||
|
||||
setTopicsWithMemories(sortedTopicsMemories)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load topics with memories:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (shortMemories.length > 0) {
|
||||
loadTopicsWithMemories()
|
||||
} else {
|
||||
setTopicsWithMemories([])
|
||||
setLoading(false)
|
||||
}
|
||||
}, [shortMemories.length])
|
||||
|
||||
// 处理折叠面板变化
|
||||
const handleCollapseChange = (keys: string | string[]) => {
|
||||
setActiveKeys(Array.isArray(keys) ? keys : [keys])
|
||||
}
|
||||
|
||||
// 处理分页变化
|
||||
const handlePageChange = useCallback((page: number, topicId: string) => {
|
||||
setTopicsWithMemories((prev) =>
|
||||
prev.map((item) => (item.topic.id === topicId ? { ...item, currentPage: page } : item))
|
||||
)
|
||||
}, [])
|
||||
|
||||
// 删除话题下的所有短期记忆
|
||||
const handleDeleteTopicMemories = useCallback(
|
||||
async (topicId: string) => {
|
||||
// 显示确认对话框
|
||||
Modal.confirm({
|
||||
title: t('settings.memory.confirmDeleteAll'),
|
||||
content: t('settings.memory.confirmDeleteAllContent'),
|
||||
okText: t('settings.memory.delete'),
|
||||
cancelText: t('settings.memory.cancel'),
|
||||
onOk: async () => {
|
||||
// 获取该话题的所有记忆
|
||||
const state = store.getState().memory
|
||||
const topicMemories = state.shortMemories.filter((memory) => memory.topicId === topicId)
|
||||
const memoryIds = topicMemories.map((memory) => memory.id)
|
||||
|
||||
// 过滤掉要删除的记忆
|
||||
const filteredShortMemories = state.shortMemories.filter((memory) => memory.topicId !== topicId)
|
||||
|
||||
// 更新本地状态
|
||||
setTopicsWithMemories((prev) => prev.filter((item) => item.topic.id !== topicId))
|
||||
|
||||
// 更新 Redux store
|
||||
for (const id of memoryIds) {
|
||||
dispatch(deleteShortMemory(id))
|
||||
}
|
||||
|
||||
// 保存到本地存储
|
||||
try {
|
||||
const currentData = await window.api.memory.loadData()
|
||||
const newData = {
|
||||
...currentData,
|
||||
shortMemories: filteredShortMemories
|
||||
}
|
||||
const result = await window.api.memory.saveData(newData, true)
|
||||
|
||||
if (result) {
|
||||
console.log(`[CollapsibleShortMemoryManager] Successfully deleted all memories for topic ${topicId}`)
|
||||
} else {
|
||||
console.error(`[CollapsibleShortMemoryManager] Failed to delete all memories for topic ${topicId}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[CollapsibleShortMemoryManager] Failed to delete all memories:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
[dispatch, t]
|
||||
)
|
||||
|
||||
// 删除短记忆 - 直接删除无需确认
|
||||
const handleDeleteMemory = useCallback(
|
||||
async (id: string) => {
|
||||
// 先从当前状态中获取要删除的记忆之外的所有记忆
|
||||
const state = store.getState().memory
|
||||
const filteredShortMemories = state.shortMemories.filter((memory) => memory.id !== id)
|
||||
|
||||
// 在本地更新topicsWithMemories,避免触发useEffect
|
||||
setTopicsWithMemories((prev) => {
|
||||
return prev
|
||||
.map((item) => {
|
||||
// 如果该话题包含要删除的记忆,则更新该话题的记忆列表
|
||||
if (item.memories.some((memory) => memory.id === id)) {
|
||||
return {
|
||||
...item,
|
||||
memories: item.memories.filter((memory) => memory.id !== id)
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
.filter((item) => item.memories.length > 0) // 移除没有记忆的话题
|
||||
})
|
||||
|
||||
// 执行删除操作
|
||||
dispatch(deleteShortMemory(id))
|
||||
|
||||
// 直接使用 window.api.memory.saveData 方法保存过滤后的列表
|
||||
try {
|
||||
// 加载当前文件数据
|
||||
const currentData = await window.api.memory.loadData()
|
||||
|
||||
// 替换 shortMemories 数组
|
||||
const newData = {
|
||||
...currentData,
|
||||
shortMemories: filteredShortMemories
|
||||
}
|
||||
|
||||
// 使用 true 参数强制覆盖文件
|
||||
const result = await window.api.memory.saveData(newData, true)
|
||||
|
||||
if (result) {
|
||||
console.log(`[CollapsibleShortMemoryManager] Successfully deleted short memory with ID ${id}`)
|
||||
// 使用App组件而不是静态方法,避免触发重新渲染
|
||||
// message.success(t('settings.memory.deleteSuccess') || '删除成功')
|
||||
} else {
|
||||
console.error(`[CollapsibleShortMemoryManager] Failed to delete short memory with ID ${id}`)
|
||||
// message.error(t('settings.memory.deleteError') || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[CollapsibleShortMemoryManager] Failed to delete short memory:', error)
|
||||
// message.error(t('settings.memory.deleteError') || '删除失败')
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={4}>
|
||||
{t('settings.memory.shortMemoriesByTopic') || '按话题分组的短期记忆'}
|
||||
</Typography.Title>
|
||||
|
||||
{loading ? (
|
||||
<LoadingContainer>{t('settings.memory.loading') || '加载中...'}</LoadingContainer>
|
||||
) : topicsWithMemories.length > 0 ? (
|
||||
<StyledCollapse
|
||||
activeKey={activeKeys}
|
||||
onChange={handleCollapseChange}
|
||||
items={topicsWithMemories.map(({ topic, memories, currentPage }) => ({
|
||||
key: topic.id,
|
||||
label: (
|
||||
<CollapseHeader>
|
||||
<span>
|
||||
{topic.name}
|
||||
<MemoryCount>{memories.length}</MemoryCount>
|
||||
</span>
|
||||
<Tooltip title={t('settings.memory.confirmDeleteAll')}>
|
||||
<Button
|
||||
icon={<ClearOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation() // 阻止事件冒泡,避免触发折叠面板的展开/收起
|
||||
handleDeleteTopicMemories(topic.id)
|
||||
}}
|
||||
type="text"
|
||||
danger
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
</CollapseHeader>
|
||||
),
|
||||
children: (
|
||||
<div>
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={memories.slice(
|
||||
(currentPage ? currentPage - 1 : 0) * 15,
|
||||
(currentPage ? currentPage - 1 : 0) * 15 + 15
|
||||
)}
|
||||
style={{ padding: '4px 0' }}
|
||||
renderItem={(memory, index) => (
|
||||
<MemoryItem
|
||||
key={memory.id}
|
||||
memory={memory}
|
||||
onDelete={handleDeleteMemory}
|
||||
t={t}
|
||||
index={(currentPage ? currentPage - 1 : 0) * 15 + index}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{memories.length > 15 && (
|
||||
<PaginationContainer>
|
||||
<Pagination
|
||||
current={currentPage || 1}
|
||||
onChange={(page) => handlePageChange(page, topic.id)}
|
||||
total={memories.length}
|
||||
pageSize={15}
|
||||
size="small"
|
||||
showSizeChanger={false}
|
||||
/>
|
||||
</PaginationContainer>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<Empty description={t('settings.memory.noShortMemories') || '没有短期记忆'} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CollapsibleShortMemoryManager
|
||||
@@ -0,0 +1,174 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
clearCurrentRecommendations,
|
||||
saveMemoryData,
|
||||
setAutoRecommendMemories,
|
||||
setContextualRecommendationEnabled,
|
||||
setRecommendationThreshold
|
||||
} from '@renderer/store/memory'
|
||||
import { Button, InputNumber, Slider, Switch, Tooltip } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const SliderContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
margin-right: 16px;
|
||||
`
|
||||
|
||||
const ContextualRecommendationSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// 获取相关状态
|
||||
const contextualRecommendationEnabled = useAppSelector((state) => state.memory.contextualRecommendationEnabled)
|
||||
const autoRecommendMemories = useAppSelector((state) => state.memory.autoRecommendMemories)
|
||||
const recommendationThreshold = useAppSelector((state) => state.memory.recommendationThreshold)
|
||||
|
||||
// 处理开关状态变化
|
||||
const handleContextualRecommendationToggle = async (checked: boolean) => {
|
||||
dispatch(setContextualRecommendationEnabled(checked))
|
||||
|
||||
// 保存设置
|
||||
try {
|
||||
await dispatch(saveMemoryData({ contextualRecommendationEnabled: checked })).unwrap()
|
||||
console.log('[ContextualRecommendationSettings] Contextual recommendation enabled setting saved:', checked)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[ContextualRecommendationSettings] Failed to save contextual recommendation enabled setting:',
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAutoRecommendToggle = async (checked: boolean) => {
|
||||
dispatch(setAutoRecommendMemories(checked))
|
||||
|
||||
// 保存设置
|
||||
try {
|
||||
await dispatch(saveMemoryData({ autoRecommendMemories: checked })).unwrap()
|
||||
console.log('[ContextualRecommendationSettings] Auto recommend memories setting saved:', checked)
|
||||
} catch (error) {
|
||||
console.error('[ContextualRecommendationSettings] Failed to save auto recommend memories setting:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理推荐阈值变化
|
||||
const handleThresholdChange = async (value: number | null) => {
|
||||
if (value !== null) {
|
||||
dispatch(setRecommendationThreshold(value))
|
||||
|
||||
// 保存设置
|
||||
try {
|
||||
await dispatch(saveMemoryData({ recommendationThreshold: value })).unwrap()
|
||||
console.log('[ContextualRecommendationSettings] Recommendation threshold setting saved:', value)
|
||||
} catch (error) {
|
||||
console.error('[ContextualRecommendationSettings] Failed to save recommendation threshold setting:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清除当前推荐
|
||||
const handleClearRecommendations = () => {
|
||||
dispatch(clearCurrentRecommendations())
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup>
|
||||
<SettingTitle>{t('settings.memory.contextualRecommendation.title') || '上下文感知记忆推荐'}</SettingTitle>
|
||||
<SettingHelpText>
|
||||
{t('settings.memory.contextualRecommendation.description') ||
|
||||
'根据当前对话上下文智能推荐相关记忆,提高AI回复的相关性和连贯性。'}
|
||||
</SettingHelpText>
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.memory.contextualRecommendation.enable') || '启用上下文感知记忆推荐'}
|
||||
<Tooltip
|
||||
title={
|
||||
t('settings.memory.contextualRecommendation.enableTip') ||
|
||||
'启用后,系统将根据当前对话上下文自动推荐相关记忆'
|
||||
}>
|
||||
<InfoCircleOutlined style={{ marginLeft: 8 }} />
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<Switch checked={contextualRecommendationEnabled} onChange={handleContextualRecommendationToggle} />
|
||||
</SettingRow>
|
||||
|
||||
<SettingDivider />
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.memory.contextualRecommendation.autoRecommend') || '自动推荐记忆'}
|
||||
<Tooltip
|
||||
title={
|
||||
t('settings.memory.contextualRecommendation.autoRecommendTip') ||
|
||||
'启用后,系统将定期自动分析当前对话并推荐相关记忆'
|
||||
}>
|
||||
<InfoCircleOutlined style={{ marginLeft: 8 }} />
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<Switch
|
||||
checked={autoRecommendMemories}
|
||||
onChange={handleAutoRecommendToggle}
|
||||
disabled={!contextualRecommendationEnabled}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.memory.contextualRecommendation.threshold') || '推荐阈值'}
|
||||
<Tooltip
|
||||
title={
|
||||
t('settings.memory.contextualRecommendation.thresholdTip') || '设置记忆推荐的相似度阈值,值越高要求越严格'
|
||||
}>
|
||||
<InfoCircleOutlined style={{ marginLeft: 8 }} />
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<SliderContainer>
|
||||
<Slider
|
||||
min={0.1}
|
||||
max={0.9}
|
||||
step={0.05}
|
||||
value={recommendationThreshold}
|
||||
onChange={handleThresholdChange}
|
||||
disabled={!contextualRecommendationEnabled}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</SliderContainer>
|
||||
<InputNumber
|
||||
min={0.1}
|
||||
max={0.9}
|
||||
step={0.05}
|
||||
value={recommendationThreshold}
|
||||
onChange={handleThresholdChange}
|
||||
disabled={!contextualRecommendationEnabled}
|
||||
style={{ width: 70 }}
|
||||
/>
|
||||
</div>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.memory.contextualRecommendation.clearRecommendations') || '清除当前推荐'}
|
||||
<Tooltip
|
||||
title={t('settings.memory.contextualRecommendation.clearRecommendationsTip') || '清除当前的记忆推荐列表'}>
|
||||
<InfoCircleOutlined style={{ marginLeft: 8 }} />
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<Button onClick={handleClearRecommendations} disabled={!contextualRecommendationEnabled}>
|
||||
{t('settings.memory.contextualRecommendation.clear') || '清除'}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContextualRecommendationSettings
|
||||
@@ -0,0 +1,115 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { saveMemoryData, setHistoricalContextAnalyzeModel } from '@renderer/store/memory'
|
||||
import { setEnableHistoricalContext } from '@renderer/store/settings'
|
||||
import { Button, Switch, Tooltip } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const HistoricalContextSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const { providers } = useProviders()
|
||||
|
||||
// 获取相关状态
|
||||
const enableHistoricalContext = useAppSelector((state) => state.settings.enableHistoricalContext)
|
||||
const historicalContextAnalyzeModel = useAppSelector((state) => state.memory.historicalContextAnalyzeModel)
|
||||
|
||||
// 处理开关状态变化
|
||||
const handleHistoricalContextToggle = (checked: boolean) => {
|
||||
dispatch(setEnableHistoricalContext(checked))
|
||||
}
|
||||
|
||||
// 处理模型选择变化
|
||||
const handleModelChange = async (modelId: string) => {
|
||||
dispatch(setHistoricalContextAnalyzeModel(modelId))
|
||||
console.log('[HistoricalContextSettings] Historical context analyze model set:', modelId)
|
||||
|
||||
// 使用Redux Thunk保存到JSON文件
|
||||
try {
|
||||
await dispatch(saveMemoryData({ historicalContextAnalyzeModel: modelId })).unwrap()
|
||||
console.log('[HistoricalContextSettings] Historical context analyze model saved to file successfully:', modelId)
|
||||
} catch (error) {
|
||||
console.error('[HistoricalContextSettings] Failed to save historical context analyze model to file:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前选中模型的名称
|
||||
const getSelectedModelName = () => {
|
||||
if (!historicalContextAnalyzeModel) return ''
|
||||
|
||||
// 遍历所有服务商的模型找到匹配的模型
|
||||
for (const provider of Object.values(providers)) {
|
||||
const model = provider.models.find((m) => m.id === historicalContextAnalyzeModel)
|
||||
if (model) {
|
||||
return `${model.name} | ${provider.name}`
|
||||
}
|
||||
}
|
||||
|
||||
return historicalContextAnalyzeModel
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup>
|
||||
<SettingTitle>{t('settings.memory.historicalContext.title') || '历史对话上下文'}</SettingTitle>
|
||||
<SettingHelpText>
|
||||
{t('settings.memory.historicalContext.description') || '允许AI在需要时自动引用历史对话,以提供更连贯的回答。'}
|
||||
</SettingHelpText>
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.memory.historicalContext.enable') || '启用历史对话上下文'}
|
||||
<Tooltip
|
||||
title={
|
||||
t('settings.memory.historicalContext.enableTip') ||
|
||||
'启用后,AI会在需要时自动分析并引用历史对话,以提供更连贯的回答'
|
||||
}>
|
||||
<InfoCircleOutlined style={{ marginLeft: 8 }} />
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<Switch checked={enableHistoricalContext} onChange={handleHistoricalContextToggle} />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.memory.analyzeModel') || '分析模型'}
|
||||
<Tooltip
|
||||
title={
|
||||
t('settings.memory.historicalContext.analyzeModelTip') ||
|
||||
'选择用于历史对话上下文分析的模型,建议选择响应较快的模型'
|
||||
}>
|
||||
<InfoCircleOutlined style={{ marginLeft: 8 }} />
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
// 找到当前选中的模型对象
|
||||
let currentModel: { id: string; provider: string; name: string; group: string } | undefined
|
||||
if (historicalContextAnalyzeModel) {
|
||||
for (const provider of Object.values(providers)) {
|
||||
const model = provider.models.find((m) => m.id === historicalContextAnalyzeModel)
|
||||
if (model) {
|
||||
currentModel = model
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectedModel = await SelectModelPopup.show({ model: currentModel })
|
||||
if (selectedModel) {
|
||||
handleModelChange(selectedModel.id)
|
||||
}
|
||||
}}
|
||||
style={{ width: 300 }}>
|
||||
{historicalContextAnalyzeModel ? getSelectedModelName() : t('settings.memory.selectModel') || '选择模型'}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default HistoricalContextSettings
|
||||
@@ -0,0 +1,448 @@
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
DownOutlined,
|
||||
MergeCellsOutlined,
|
||||
QuestionCircleOutlined,
|
||||
RightOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { TopicManager } from '@renderer/hooks/useTopic'
|
||||
import {
|
||||
applyDeduplicationResult,
|
||||
deduplicateAndMergeMemories,
|
||||
DeduplicationResult
|
||||
} from '@renderer/services/MemoryDeduplicationService'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import store from '@renderer/store'
|
||||
import { Topic } from '@renderer/types'
|
||||
import { Button, Card, Collapse, Empty, List, Modal, Slider, Space, Spin, Tag, Typography } from 'antd'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
// 使用items属性,不再需要Panel组件
|
||||
const { Title, Text, Paragraph } = Typography
|
||||
|
||||
interface MemoryDeduplicationPanelProps {
|
||||
title?: string
|
||||
description?: string
|
||||
translationPrefix?: string
|
||||
applyResults?: (result: DeduplicationResult) => void
|
||||
isShortMemory?: boolean
|
||||
}
|
||||
|
||||
const MemoryDeduplicationPanel: React.FC<MemoryDeduplicationPanelProps> = ({
|
||||
title,
|
||||
description,
|
||||
translationPrefix = 'settings.memory.deduplication',
|
||||
applyResults,
|
||||
isShortMemory = false
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [deduplicationResult, setDeduplicationResult] = useState<DeduplicationResult | null>(null)
|
||||
const [threshold, setThreshold] = useState(0.75) // 降低默认阈值以捕获更多相似记忆
|
||||
const [selectedListId, setSelectedListId] = useState<string | undefined>(undefined)
|
||||
const [selectedTopicId, setSelectedTopicId] = useState<string | undefined>(undefined)
|
||||
const [topicsList, setTopicsList] = useState<Topic[]>([])
|
||||
const [loadingTopics, setLoadingTopics] = useState(false)
|
||||
|
||||
// 获取记忆列表
|
||||
const memoryLists = useAppSelector((state) => state.memory?.memoryLists || [])
|
||||
const memories = useAppSelector((state) =>
|
||||
isShortMemory ? state.memory?.shortMemories || [] : state.memory?.memories || []
|
||||
)
|
||||
|
||||
// 加载有短期记忆的话题
|
||||
useEffect(() => {
|
||||
const loadTopics = async () => {
|
||||
try {
|
||||
setLoadingTopics(true)
|
||||
|
||||
// 获取短期记忆
|
||||
const shortMemories = store.getState().memory?.shortMemories || []
|
||||
|
||||
// 获取所有有短期记忆的话题ID
|
||||
const topicIds = Array.from(new Set(shortMemories.map((memory) => memory.topicId)))
|
||||
|
||||
if (topicIds.length > 0) {
|
||||
// 获取所有助手及其话题,确保我们使用与左侧列表相同的话题名称
|
||||
const assistants = store.getState().assistants?.assistants || []
|
||||
const allAssistantTopics = assistants.flatMap((assistant) => assistant.topics || [])
|
||||
|
||||
// 创建完整的话题列表
|
||||
const fullTopics: Topic[] = []
|
||||
|
||||
for (const topicId of topicIds) {
|
||||
// 首先尝试从助手的话题列表中找到完整的话题信息
|
||||
let topicInfo = allAssistantTopics.find((topic) => topic.id === topicId)
|
||||
|
||||
// 如果在助手话题中找不到,则尝试从数据库获取
|
||||
if (!topicInfo) {
|
||||
try {
|
||||
const dbTopic = await TopicManager.getTopic(topicId)
|
||||
if (dbTopic) {
|
||||
topicInfo = {
|
||||
id: dbTopic.id,
|
||||
assistantId: '',
|
||||
name: `话题 ${dbTopic.id.substring(0, 8)}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
messages: []
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to get topic name for ${topicId}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到了话题信息,添加到列表中
|
||||
if (topicInfo) {
|
||||
fullTopics.push(topicInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// 按更新时间排序,最新的在前
|
||||
const sortedTopics = fullTopics.sort((a, b) => {
|
||||
return new Date(b.updatedAt || b.createdAt).getTime() - new Date(a.updatedAt || a.createdAt).getTime()
|
||||
})
|
||||
|
||||
setTopicsList(sortedTopics)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load topics:', error)
|
||||
} finally {
|
||||
setLoadingTopics(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadTopics()
|
||||
}, [])
|
||||
|
||||
// 开始去重分析
|
||||
const handleDeduplication = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
if (isShortMemory) {
|
||||
// 短期记忆去重
|
||||
const result = await deduplicateAndMergeMemories(undefined, true, selectedTopicId)
|
||||
setDeduplicationResult(result)
|
||||
} else {
|
||||
// 长期记忆去重
|
||||
const result = await deduplicateAndMergeMemories(selectedListId, false)
|
||||
setDeduplicationResult(result)
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 应用去重结果
|
||||
const handleApplyResult = () => {
|
||||
if (!deduplicationResult) return
|
||||
|
||||
Modal.confirm({
|
||||
title: t(`${translationPrefix}.confirmApply`),
|
||||
content: t(`${translationPrefix}.confirmApplyContent`),
|
||||
onOk: async () => {
|
||||
try {
|
||||
if (applyResults) {
|
||||
// 使用自定义的应用函数
|
||||
applyResults(deduplicationResult)
|
||||
} else {
|
||||
// 使用默认的应用函数
|
||||
await applyDeduplicationResult(deduplicationResult, true, isShortMemory)
|
||||
}
|
||||
setDeduplicationResult(null)
|
||||
Modal.success({
|
||||
title: t(`${translationPrefix}.applySuccess`),
|
||||
content: t(`${translationPrefix}.applySuccessContent`)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[Memory Deduplication Panel] Error applying deduplication result:', error)
|
||||
Modal.error({
|
||||
title: t(`${translationPrefix}.applyError`) || '应用失败',
|
||||
content: t(`${translationPrefix}.applyErrorContent`) || '应用去重结果时发生错误,请重试'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取记忆内容 - 这个函数在renderItem中使用,确保没有删除错误
|
||||
const getMemoryContent = (index: string) => {
|
||||
const memoryIndex = parseInt(index) - 1
|
||||
if (memoryIndex >= 0 && memoryIndex < memories.length) {
|
||||
const memory = memories[memoryIndex]
|
||||
return {
|
||||
content: memory.content,
|
||||
category: 'category' in memory ? memory.category || '其他' : '其他'
|
||||
}
|
||||
}
|
||||
return { content: '', category: '' }
|
||||
}
|
||||
// 函数 getMemories 在第38行报错未使用,不是 getMemoryContent
|
||||
// 将删除报错的 getMemories 函数 (实际检查代码发现没有 getMemories 函数,可能之前已删除或误报,先跳过此文件)
|
||||
|
||||
// 渲染结果
|
||||
const renderResult = () => {
|
||||
if (!deduplicationResult) return null
|
||||
|
||||
if (deduplicationResult.similarGroups.length === 0) {
|
||||
return (
|
||||
<Empty
|
||||
description={t('settings.memory.deduplication.noSimilarMemories')}
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={5}>{t('settings.memory.deduplication.similarGroups')}</Title>
|
||||
<Collapse
|
||||
items={deduplicationResult.similarGroups.map((group) => ({
|
||||
key: group.groupId,
|
||||
label: (
|
||||
<Space>
|
||||
<Text strong>
|
||||
{t('settings.memory.deduplication.group')} {group.groupId}
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
({group.memoryIds.length} {t('settings.memory.deduplication.items')})
|
||||
</Text>
|
||||
{group.category && <Tag color="blue">{group.category}</Tag>}
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
<Card
|
||||
title={t('settings.memory.deduplication.originalMemories')}
|
||||
size="small"
|
||||
style={{ marginBottom: 16 }}>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={group.memoryIds}
|
||||
renderItem={(id) => {
|
||||
const memory = getMemoryContent(id)
|
||||
return (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
title={<Text code>{id}</Text>}
|
||||
description={
|
||||
<>
|
||||
<Tag color="cyan">{memory.category}</Tag>
|
||||
<Text>{memory.content}</Text>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title={t('settings.memory.deduplication.mergedResult')} size="small">
|
||||
<Paragraph>
|
||||
<Tag color="green">{group.category || t('settings.memory.deduplication.other')}</Tag>
|
||||
<Text strong>{group.mergedContent}</Text>
|
||||
</Paragraph>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}))}
|
||||
/>
|
||||
|
||||
<div style={{ marginTop: 16, textAlign: 'center' }}>
|
||||
<Button type="primary" icon={<CheckCircleOutlined />} onClick={handleApplyResult}>
|
||||
{t('settings.memory.deduplication.applyResults')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 切换折叠状态
|
||||
const toggleExpand = () => {
|
||||
setIsExpanded(!isExpanded)
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledCard>
|
||||
<CollapsibleHeader onClick={toggleExpand}>
|
||||
<HeaderContent>
|
||||
<Title level={5}>{title || t(`${translationPrefix}.title`)}</Title>
|
||||
{isExpanded ? <DownOutlined /> : <RightOutlined />}
|
||||
</HeaderContent>
|
||||
</CollapsibleHeader>
|
||||
|
||||
{isExpanded && (
|
||||
<CollapsibleContent>
|
||||
<Paragraph>{description || t(`${translationPrefix}.description`)}</Paragraph>
|
||||
|
||||
<ControlsContainer>
|
||||
{!isShortMemory ? (
|
||||
<div>
|
||||
<Text>{t(`${translationPrefix}.selectList`)}</Text>
|
||||
<Select
|
||||
value={selectedListId || 'all'}
|
||||
onChange={(e) => setSelectedListId(e.target.value === 'all' ? undefined : e.target.value)}>
|
||||
<option value="all">{t(`${translationPrefix}.allLists`)}</option>
|
||||
{memoryLists.map((list) => (
|
||||
<option key={list.id} value={list.id}>
|
||||
{list.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Text>{t(`${translationPrefix}.selectTopic`) || '选择话题'}</Text>
|
||||
<Select
|
||||
value={selectedTopicId || 'all'}
|
||||
onChange={(e) => setSelectedTopicId(e.target.value === 'all' ? undefined : e.target.value)}>
|
||||
<option value="all">{t('settings.memory.allTopics') || '所有话题'}</option>
|
||||
{loadingTopics ? (
|
||||
<option disabled>{t('settings.memory.loading') || '加载中...'}</option>
|
||||
) : topicsList.length > 0 ? (
|
||||
topicsList.map((topic) => (
|
||||
<option key={topic.id} value={topic.id}>
|
||||
{topic.name || `话题 ${topic.id.substring(0, 8)}`}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option disabled>{t('settings.memory.noTopics') || '没有话题'}</option>
|
||||
)}
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Text>
|
||||
{t(`${translationPrefix}.similarityThreshold`)}: {threshold}
|
||||
</Text>
|
||||
<Slider
|
||||
min={0.5}
|
||||
max={0.95}
|
||||
step={0.05}
|
||||
value={threshold}
|
||||
onChange={setThreshold}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
</div>
|
||||
</ControlsContainer>
|
||||
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<MergeCellsOutlined />}
|
||||
onClick={handleDeduplication}
|
||||
loading={isLoading}
|
||||
disabled={memories.length < 2}>
|
||||
{t(`${translationPrefix}.startAnalysis`)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<QuestionCircleOutlined />}
|
||||
onClick={() => {
|
||||
Modal.info({
|
||||
title: t(`${translationPrefix}.helpTitle`),
|
||||
content: (
|
||||
<div>
|
||||
<Paragraph>{t(`${translationPrefix}.helpContent1`)}</Paragraph>
|
||||
<Paragraph>{t(`${translationPrefix}.helpContent2`)}</Paragraph>
|
||||
<Paragraph>{t(`${translationPrefix}.helpContent3`)}</Paragraph>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}}>
|
||||
{t(`${translationPrefix}.help`)}
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
|
||||
{isLoading ? (
|
||||
<LoadingContainer>
|
||||
<Spin size="large" />
|
||||
<Text>{t(`${translationPrefix}.analyzing`)}</Text>
|
||||
</LoadingContainer>
|
||||
) : (
|
||||
<ResultContainer>{renderResult()}</ResultContainer>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
)}
|
||||
</StyledCard>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
margin-bottom: 24px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const CollapsibleHeader = styled.div`
|
||||
cursor: pointer;
|
||||
padding: 12px 16px;
|
||||
background-color: var(--color-background-secondary, #f5f5f5);
|
||||
border-bottom: 1px solid var(--color-border, #e8e8e8);
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-hover, #e6f7ff);
|
||||
}
|
||||
`
|
||||
|
||||
const HeaderContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const CollapsibleContent = styled.div`
|
||||
padding: 16px;
|
||||
`
|
||||
|
||||
const ControlsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
margin-bottom: 16px;
|
||||
`
|
||||
|
||||
const ButtonContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
`
|
||||
|
||||
const LoadingContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 0;
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const ResultContainer = styled.div`
|
||||
margin-top: 16px;
|
||||
`
|
||||
|
||||
// ApplyButtonContainer seems unused, removing it.
|
||||
// const ApplyButtonContainer = styled.div`
|
||||
// margin-top: 16px;
|
||||
// text-align: center;
|
||||
// `
|
||||
|
||||
const Select = styled.select`
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-text);
|
||||
`
|
||||
|
||||
export default MemoryDeduplicationPanel
|
||||
@@ -0,0 +1,319 @@
|
||||
import { DeleteOutlined, EditOutlined, ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import store from '@renderer/store'
|
||||
import {
|
||||
addMemoryList,
|
||||
deleteMemoryList,
|
||||
editMemoryList,
|
||||
MemoryList,
|
||||
saveLongTermMemoryData,
|
||||
setCurrentMemoryList,
|
||||
toggleMemoryListActive
|
||||
} from '@renderer/store/memory'
|
||||
import { Button, Empty, Input, List, Modal, Switch, Tooltip, Typography } from 'antd'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Title } = Typography
|
||||
const { confirm } = Modal
|
||||
|
||||
interface MemoryListManagerProps {
|
||||
onSelectList?: (listId: string) => void
|
||||
}
|
||||
|
||||
const MemoryListManager: React.FC<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 = async () => {
|
||||
if (!newListName.trim()) {
|
||||
return // 名称不能为空
|
||||
}
|
||||
|
||||
if (editingList) {
|
||||
// 编辑现有列表
|
||||
await dispatch(
|
||||
editMemoryList({
|
||||
id: editingList.id,
|
||||
name: newListName,
|
||||
description: newListDescription
|
||||
})
|
||||
)
|
||||
} else {
|
||||
// 添加新列表
|
||||
await dispatch(
|
||||
addMemoryList({
|
||||
name: newListName,
|
||||
description: newListDescription,
|
||||
isActive: false
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// 保存到长期记忆文件
|
||||
try {
|
||||
const state = store.getState().memory
|
||||
await dispatch(
|
||||
saveLongTermMemoryData({
|
||||
memoryLists: state.memoryLists,
|
||||
currentListId: state.currentListId
|
||||
})
|
||||
).unwrap()
|
||||
console.log('[MemoryListManager] Memory lists saved to file after edit')
|
||||
} catch (error) {
|
||||
console.error('[MemoryListManager] Failed to save memory lists after edit:', error)
|
||||
}
|
||||
|
||||
setIsModalVisible(false)
|
||||
setNewListName('')
|
||||
setNewListDescription('')
|
||||
setEditingList(null)
|
||||
}
|
||||
|
||||
// 处理模态框取消
|
||||
const handleCancel = () => {
|
||||
setIsModalVisible(false)
|
||||
setNewListName('')
|
||||
setNewListDescription('')
|
||||
setEditingList(null)
|
||||
}
|
||||
|
||||
// 删除记忆列表
|
||||
const handleDelete = (list: MemoryList) => {
|
||||
confirm({
|
||||
title: t('settings.memory.confirmDeleteList'),
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: t('settings.memory.confirmDeleteListContent', { name: list.name }),
|
||||
okText: t('common.delete'),
|
||||
okType: 'danger',
|
||||
cancelText: t('common.cancel'),
|
||||
async onOk() {
|
||||
dispatch(deleteMemoryList(list.id))
|
||||
|
||||
// 保存到长期记忆文件
|
||||
try {
|
||||
const state = store.getState().memory
|
||||
await dispatch(
|
||||
saveLongTermMemoryData({
|
||||
memoryLists: state.memoryLists,
|
||||
currentListId: state.currentListId
|
||||
})
|
||||
).unwrap()
|
||||
console.log('[MemoryListManager] Memory lists saved to file after delete')
|
||||
} catch (error) {
|
||||
console.error('[MemoryListManager] Failed to save memory lists after delete:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 切换列表激活状态
|
||||
const handleToggleActive = async (list: MemoryList, checked: boolean) => {
|
||||
dispatch(toggleMemoryListActive({ id: list.id, isActive: checked }))
|
||||
|
||||
// 保存到长期记忆文件
|
||||
try {
|
||||
const state = store.getState().memory
|
||||
await dispatch(
|
||||
saveLongTermMemoryData({
|
||||
memoryLists: state.memoryLists,
|
||||
currentListId: state.currentListId
|
||||
})
|
||||
).unwrap()
|
||||
console.log('[MemoryListManager] Memory lists saved to file after toggle active')
|
||||
} catch (error) {
|
||||
console.error('[MemoryListManager] Failed to save memory lists after toggle active:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择列表
|
||||
const handleSelectList = async (listId: string) => {
|
||||
dispatch(setCurrentMemoryList(listId))
|
||||
if (onSelectList) {
|
||||
onSelectList(listId)
|
||||
}
|
||||
|
||||
// 保存到长期记忆文件
|
||||
try {
|
||||
const state = store.getState().memory
|
||||
await dispatch(
|
||||
saveLongTermMemoryData({
|
||||
memoryLists: state.memoryLists,
|
||||
currentListId: state.currentListId
|
||||
})
|
||||
).unwrap()
|
||||
console.log('[MemoryListManager] Memory lists saved to file after select list')
|
||||
} catch (error) {
|
||||
console.error('[MemoryListManager] Failed to save memory lists after select list:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<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,65 @@
|
||||
import { DeleteOutlined, EditOutlined, TagOutlined } from '@ant-design/icons'
|
||||
import { Memory } from '@renderer/store/memory'
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
import { Button, Card, Tag, Tooltip, Typography } from 'antd'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface MemoryNodeProps {
|
||||
data: {
|
||||
memory: Memory
|
||||
onEdit: (id: string) => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
}
|
||||
|
||||
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,189 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
saveMemoryData,
|
||||
setDecayEnabled,
|
||||
setDecayRate,
|
||||
setFreshnessEnabled,
|
||||
setPriorityManagementEnabled,
|
||||
updateMemoryPriorities
|
||||
} from '@renderer/store/memory'
|
||||
import { Button, InputNumber, Slider, Switch, Tooltip } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const SliderContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
margin-right: 16px;
|
||||
`
|
||||
|
||||
const PriorityManagementSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// 获取相关状态
|
||||
const priorityManagementEnabled = useAppSelector((state) => state.memory.priorityManagementEnabled)
|
||||
const decayEnabled = useAppSelector((state) => state.memory.decayEnabled)
|
||||
const freshnessEnabled = useAppSelector((state) => state.memory.freshnessEnabled)
|
||||
const decayRate = useAppSelector((state) => state.memory.decayRate)
|
||||
|
||||
// 处理开关状态变化
|
||||
const handlePriorityManagementToggle = async (checked: boolean) => {
|
||||
dispatch(setPriorityManagementEnabled(checked))
|
||||
|
||||
// 保存设置
|
||||
try {
|
||||
await dispatch(saveMemoryData({ priorityManagementEnabled: checked })).unwrap()
|
||||
console.log('[PriorityManagementSettings] Priority management enabled setting saved:', checked)
|
||||
} catch (error) {
|
||||
console.error('[PriorityManagementSettings] Failed to save priority management enabled setting:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDecayToggle = async (checked: boolean) => {
|
||||
dispatch(setDecayEnabled(checked))
|
||||
|
||||
// 保存设置
|
||||
try {
|
||||
await dispatch(saveMemoryData({ decayEnabled: checked })).unwrap()
|
||||
console.log('[PriorityManagementSettings] Decay enabled setting saved:', checked)
|
||||
} catch (error) {
|
||||
console.error('[PriorityManagementSettings] Failed to save decay enabled setting:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFreshnessToggle = async (checked: boolean) => {
|
||||
dispatch(setFreshnessEnabled(checked))
|
||||
|
||||
// 保存设置
|
||||
try {
|
||||
await dispatch(saveMemoryData({ freshnessEnabled: checked })).unwrap()
|
||||
console.log('[PriorityManagementSettings] Freshness enabled setting saved:', checked)
|
||||
} catch (error) {
|
||||
console.error('[PriorityManagementSettings] Failed to save freshness enabled setting:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理衰减率变化
|
||||
const handleDecayRateChange = async (value: number | null) => {
|
||||
if (value !== null) {
|
||||
dispatch(setDecayRate(value))
|
||||
|
||||
// 保存设置
|
||||
try {
|
||||
await dispatch(saveMemoryData({ decayRate: value })).unwrap()
|
||||
console.log('[PriorityManagementSettings] Decay rate setting saved:', value)
|
||||
} catch (error) {
|
||||
console.error('[PriorityManagementSettings] Failed to save decay rate setting:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 手动更新记忆优先级
|
||||
const handleUpdatePriorities = () => {
|
||||
dispatch(updateMemoryPriorities())
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup>
|
||||
<SettingTitle>{t('settings.memory.priorityManagement.title') || '智能优先级与时效性管理'}</SettingTitle>
|
||||
<SettingHelpText>
|
||||
{t('settings.memory.priorityManagement.description') ||
|
||||
'智能管理记忆的优先级、衰减和鲜度,确保最重要和最相关的记忆优先显示。'}
|
||||
</SettingHelpText>
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.memory.priorityManagement.enable') || '启用智能优先级管理'}
|
||||
<Tooltip
|
||||
title={
|
||||
t('settings.memory.priorityManagement.enableTip') ||
|
||||
'启用后,系统将根据重要性、访问频率和时间因素自动排序记忆'
|
||||
}>
|
||||
<InfoCircleOutlined style={{ marginLeft: 8 }} />
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<Switch checked={priorityManagementEnabled} onChange={handlePriorityManagementToggle} />
|
||||
</SettingRow>
|
||||
|
||||
<SettingDivider />
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.memory.priorityManagement.decay') || '记忆衰减'}
|
||||
<Tooltip
|
||||
title={t('settings.memory.priorityManagement.decayTip') || '随着时间推移,未访问的记忆重要性会逐渐降低'}>
|
||||
<InfoCircleOutlined style={{ marginLeft: 8 }} />
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<Switch checked={decayEnabled} onChange={handleDecayToggle} disabled={!priorityManagementEnabled} />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.memory.priorityManagement.decayRate') || '衰减速率'}
|
||||
<Tooltip
|
||||
title={t('settings.memory.priorityManagement.decayRateTip') || '值越大,记忆衰减越快。0.05表示每天衰减5%'}>
|
||||
<InfoCircleOutlined style={{ marginLeft: 8 }} />
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<SliderContainer>
|
||||
<Slider
|
||||
min={0.01}
|
||||
max={0.2}
|
||||
step={0.01}
|
||||
value={decayRate}
|
||||
onChange={handleDecayRateChange}
|
||||
disabled={!priorityManagementEnabled || !decayEnabled}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</SliderContainer>
|
||||
<InputNumber
|
||||
min={0.01}
|
||||
max={0.2}
|
||||
step={0.01}
|
||||
value={decayRate}
|
||||
onChange={handleDecayRateChange}
|
||||
disabled={!priorityManagementEnabled || !decayEnabled}
|
||||
style={{ width: 70 }}
|
||||
/>
|
||||
</div>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.memory.priorityManagement.freshness') || '记忆鲜度'}
|
||||
<Tooltip
|
||||
title={
|
||||
t('settings.memory.priorityManagement.freshnessTip') ||
|
||||
'考虑记忆的创建时间和最后访问时间,优先显示较新的记忆'
|
||||
}>
|
||||
<InfoCircleOutlined style={{ marginLeft: 8 }} />
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<Switch checked={freshnessEnabled} onChange={handleFreshnessToggle} disabled={!priorityManagementEnabled} />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.memory.priorityManagement.updateNow') || '立即更新优先级'}
|
||||
<Tooltip title={t('settings.memory.priorityManagement.updateNowTip') || '手动更新所有记忆的优先级和鲜度评分'}>
|
||||
<InfoCircleOutlined style={{ marginLeft: 8 }} />
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<Button onClick={handleUpdatePriorities} disabled={!priorityManagementEnabled}>
|
||||
{t('settings.memory.priorityManagement.update') || '更新'}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default PriorityManagementSettings
|
||||
@@ -0,0 +1,145 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons'
|
||||
import { addShortMemoryItem } from '@renderer/services/MemoryService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import store from '@renderer/store'
|
||||
import { deleteShortMemory, setShortMemoryActive } from '@renderer/store/memory'
|
||||
import { Button, Empty, Input, List, Switch, Tooltip, Typography } from 'antd'
|
||||
import _ from 'lodash'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const { Title } = Typography
|
||||
// 不再需要确认对话框
|
||||
|
||||
const ShortMemoryManager = () => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// 获取当前话题ID
|
||||
const currentTopicId = useAppSelector((state) => state.messages?.currentTopic?.id)
|
||||
|
||||
// 获取短记忆状态
|
||||
const shortMemoryActive = useAppSelector((state) => state.memory?.shortMemoryActive || false)
|
||||
const shortMemories = useAppSelector((state) => {
|
||||
const allShortMemories = state.memory?.shortMemories || []
|
||||
// 只显示当前话题的短记忆
|
||||
return currentTopicId ? allShortMemories.filter((memory) => memory.topicId === currentTopicId) : []
|
||||
})
|
||||
|
||||
// 添加短记忆的状态
|
||||
const [newMemoryContent, setNewMemoryContent] = useState('')
|
||||
|
||||
// 切换短记忆功能激活状态
|
||||
const handleToggleActive = (checked: boolean) => {
|
||||
dispatch(setShortMemoryActive(checked))
|
||||
}
|
||||
|
||||
// 添加新的短记忆 - 使用防抖减少频繁更新
|
||||
const handleAddMemory = useCallback(
|
||||
_.debounce(() => {
|
||||
if (newMemoryContent.trim() && currentTopicId) {
|
||||
addShortMemoryItem(newMemoryContent.trim(), currentTopicId)
|
||||
setNewMemoryContent('') // 清空输入框
|
||||
}
|
||||
}, 300),
|
||||
[newMemoryContent, currentTopicId]
|
||||
)
|
||||
|
||||
// 删除短记忆 - 直接删除无需确认,使用节流避免频繁删除操作
|
||||
const handleDeleteMemory = useCallback(
|
||||
_.throttle(async (id: string) => {
|
||||
// 先从当前状态中获取要删除的记忆之外的所有记忆
|
||||
const state = store.getState().memory
|
||||
const filteredShortMemories = state.shortMemories.filter((memory) => memory.id !== id)
|
||||
|
||||
// 执行删除操作
|
||||
dispatch(deleteShortMemory(id))
|
||||
|
||||
// 直接使用 window.api.memory.saveData 方法保存过滤后的列表
|
||||
try {
|
||||
// 加载当前文件数据
|
||||
const currentData = await window.api.memory.loadData()
|
||||
|
||||
// 替换 shortMemories 数组
|
||||
const newData = {
|
||||
...currentData,
|
||||
shortMemories: filteredShortMemories
|
||||
}
|
||||
|
||||
// 使用 true 参数强制覆盖文件
|
||||
const result = await window.api.memory.saveData(newData, true)
|
||||
|
||||
if (result) {
|
||||
console.log(`[ShortMemoryManager] Successfully deleted short memory with ID ${id}`)
|
||||
// 移除消息提示,避免触发界面重新渲染
|
||||
} else {
|
||||
console.error(`[ShortMemoryManager] Failed to delete short memory with ID ${id}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ShortMemoryManager] Failed to delete short memory:', error)
|
||||
}
|
||||
}, 500),
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return (
|
||||
<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
|
||||
1327
src/renderer/src/pages/settings/MemorySettings/index.tsx
Normal file
1327
src/renderer/src/pages/settings/MemorySettings/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 />} />}
|
||||
@@ -155,6 +164,8 @@ const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
height: calc(100vh - var(--navbar-height)); /* 设置高度为视口高度减去导航栏高度 */
|
||||
overflow: hidden; /* 防止内容溢出 */
|
||||
`
|
||||
|
||||
const SettingMenus = styled.ul`
|
||||
@@ -164,6 +175,26 @@ const SettingMenus = styled.ul`
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
padding: 10px;
|
||||
user-select: none;
|
||||
overflow-y: auto; /* 允许菜单滚动 */
|
||||
|
||||
/* 添加滚动条样式 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
`
|
||||
|
||||
const MenuItemLink = styled(Link)`
|
||||
@@ -208,6 +239,26 @@ const SettingContent = styled.div`
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
overflow-y: auto; /* 添加滚动属性,允许内容滚动 */
|
||||
|
||||
/* 添加滚动条样式 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
`
|
||||
|
||||
export default SettingsPage
|
||||
|
||||
@@ -7,16 +7,58 @@ export const SettingContainer = styled.div<{ theme?: ThemeMode }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
min-height: calc(100vh - var(--navbar-height));
|
||||
height: auto;
|
||||
padding: 20px;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 75px;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto; /* 改为auto,只在需要时显示滚动条 */
|
||||
font-family: Ubuntu;
|
||||
background: ${(props) => (props.theme === 'dark' ? 'transparent' : 'var(--color-background-soft)')};
|
||||
|
||||
/* 添加滚动指示器 */
|
||||
&::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-primary);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&.scrollable::after {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 5px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-primary);
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
filterEmptyMessages,
|
||||
filterUserRoleStartMessages
|
||||
} from '@renderer/services/MessagesService'
|
||||
import store from '@renderer/store'
|
||||
import { getActiveServers } from '@renderer/store/mcp'
|
||||
import { Assistant, FileTypes, MCPToolResponse, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import { removeSpecialCharactersForTopicName } from '@renderer/utils'
|
||||
import { parseAndCallTools } from '@renderer/utils/mcp-tools'
|
||||
@@ -177,7 +179,7 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
|
||||
let systemPrompt = assistant.prompt
|
||||
if (mcpTools && mcpTools.length > 0) {
|
||||
systemPrompt = buildSystemPrompt(systemPrompt, mcpTools)
|
||||
systemPrompt = await buildSystemPrompt(systemPrompt, mcpTools, getActiveServers(store.getState()))
|
||||
}
|
||||
|
||||
const body: MessageCreateParamsNonStreaming = {
|
||||
@@ -473,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 = await applyMemoriesToPrompt(prompt)
|
||||
console.log(
|
||||
'[AnthropicProvider] Applied memories to prompt, length difference:',
|
||||
enhancedPrompt.length - prompt.length
|
||||
)
|
||||
|
||||
const message = await this.sdk.messages.create({
|
||||
model: model.id,
|
||||
system: prompt,
|
||||
system: enhancedPrompt,
|
||||
stream: false,
|
||||
max_tokens: 4096,
|
||||
messages: [
|
||||
|
||||
@@ -37,7 +37,15 @@ 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[]>
|
||||
@@ -94,10 +102,22 @@ export default abstract class BaseProvider {
|
||||
}
|
||||
|
||||
public async getMessageContent(message: Message) {
|
||||
if (isEmpty(message.content)) {
|
||||
if (isEmpty(message.content) && !message.referencedMessages?.length) {
|
||||
return message.content
|
||||
}
|
||||
|
||||
// 处理引用消息
|
||||
if (message.referencedMessages && message.referencedMessages.length > 0) {
|
||||
const refMsg = message.referencedMessages[0]
|
||||
const roleText = refMsg.role === 'user' ? '用户' : 'AI'
|
||||
const referencedContent = `===引用消息开始===\n角色: ${roleText}\n内容: ${refMsg.content}\n===引用消息结束===`
|
||||
// 如果消息内容为空,则直接返回引用内容
|
||||
if (isEmpty(message.content.trim())) {
|
||||
return referencedContent
|
||||
}
|
||||
return `${message.content}\n\n${referencedContent}`
|
||||
}
|
||||
|
||||
const webSearchReferences = await this.getWebSearchReferences(message)
|
||||
|
||||
if (!isEmpty(webSearchReferences)) {
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
filterUserRoleStartMessages
|
||||
} from '@renderer/services/MessagesService'
|
||||
import WebSearchService from '@renderer/services/WebSearchService'
|
||||
import store from '@renderer/store'
|
||||
import { getActiveServers } from '@renderer/store/mcp'
|
||||
import { Assistant, FileType, FileTypes, MCPToolResponse, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import { removeSpecialCharactersForTopicName } from '@renderer/utils'
|
||||
import { parseAndCallTools } from '@renderer/utils/mcp-tools'
|
||||
@@ -228,7 +230,11 @@ export default class GeminiProvider extends BaseProvider {
|
||||
let systemInstruction = assistant.prompt
|
||||
|
||||
if (mcpTools && mcpTools.length > 0) {
|
||||
systemInstruction = buildSystemPrompt(assistant.prompt || '', mcpTools)
|
||||
systemInstruction = await buildSystemPrompt(
|
||||
assistant.prompt || '',
|
||||
mcpTools,
|
||||
getActiveServers(store.getState())
|
||||
)
|
||||
}
|
||||
|
||||
// const tools = mcpToolsToGeminiTools(mcpTools)
|
||||
@@ -464,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 = await applyMemoriesToPrompt(prompt)
|
||||
console.log(
|
||||
'[GeminiProvider] Applied memories to prompt, length difference:',
|
||||
enhancedPrompt.length - prompt.length
|
||||
)
|
||||
|
||||
const systemMessage = { role: 'system', content: enhancedPrompt }
|
||||
|
||||
const geminiModel = this.sdk.getGenerativeModel(
|
||||
{
|
||||
@@ -480,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)
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
filterUserRoleStartMessages
|
||||
} from '@renderer/services/MessagesService'
|
||||
import store from '@renderer/store'
|
||||
import { getActiveServers } from '@renderer/store/mcp'
|
||||
import {
|
||||
Assistant,
|
||||
FileTypes,
|
||||
@@ -310,7 +311,17 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
const model = assistant.model || defaultModel
|
||||
const { contextCount, maxTokens, streamOutput } = getAssistantSettings(assistant)
|
||||
messages = addImageFileToContents(messages)
|
||||
let systemMessage = { role: 'system', content: assistant.prompt || '' }
|
||||
// 应用记忆功能到系统提示词
|
||||
const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService')
|
||||
// 获取当前话题ID
|
||||
const currentTopicId = messages.length > 0 ? messages[0].topicId : undefined
|
||||
const enhancedPrompt = await applyMemoriesToPrompt(assistant.prompt || '', currentTopicId)
|
||||
console.log(
|
||||
'[OpenAIProvider.completions] Applied memories to prompt, length difference:',
|
||||
enhancedPrompt.length - (assistant.prompt || '').length
|
||||
)
|
||||
|
||||
let systemMessage = { role: 'system', content: enhancedPrompt }
|
||||
if (isOpenAIoSeries(model)) {
|
||||
systemMessage = {
|
||||
role: 'developer',
|
||||
@@ -318,7 +329,11 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
}
|
||||
}
|
||||
if (mcpTools && mcpTools.length > 0) {
|
||||
systemMessage.content = buildSystemPrompt(systemMessage.content || '', mcpTools)
|
||||
systemMessage.content = await buildSystemPrompt(
|
||||
systemMessage.content || '',
|
||||
mcpTools,
|
||||
getActiveServers(store.getState())
|
||||
)
|
||||
}
|
||||
|
||||
const userMessages: ChatCompletionMessageParam[] = []
|
||||
@@ -539,12 +554,23 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void) {
|
||||
const defaultModel = getDefaultModel()
|
||||
const model = assistant.model || defaultModel
|
||||
|
||||
// 应用记忆功能到系统提示词
|
||||
const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService')
|
||||
// 获取当前话题ID
|
||||
const currentTopicId = message.topicId
|
||||
const enhancedPrompt = await applyMemoriesToPrompt(assistant.prompt || '', currentTopicId)
|
||||
console.log(
|
||||
'[OpenAIProvider.translate] Applied memories to prompt, length difference:',
|
||||
enhancedPrompt.length - (assistant.prompt || '').length
|
||||
)
|
||||
|
||||
const messages = message.content
|
||||
? [
|
||||
{ role: 'system', content: assistant.prompt },
|
||||
{ role: 'system', content: enhancedPrompt },
|
||||
{ role: 'user', content: message.content }
|
||||
]
|
||||
: [{ role: 'user', content: assistant.prompt }]
|
||||
: [{ role: 'user', content: enhancedPrompt }]
|
||||
|
||||
const isOpenAIReasoning = this.isOpenAIReasoning(model)
|
||||
|
||||
@@ -625,9 +651,25 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
return prev + (prev ? '\n' : '') + content
|
||||
}, '')
|
||||
|
||||
// 获取原始提示词
|
||||
const originalPrompt = getStoreSetting('topicNamingPrompt') || i18n.t('prompts.title')
|
||||
|
||||
// 应用记忆功能到系统提示词
|
||||
const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService')
|
||||
// 获取当前话题ID
|
||||
const currentTopicId = messages.length > 0 ? messages[0].topicId : undefined
|
||||
// 使用双重类型断言强制转换类型
|
||||
const enhancedPrompt = (await applyMemoriesToPrompt(originalPrompt as string, currentTopicId)) as unknown as string
|
||||
// 存储原始提示词长度
|
||||
const originalPromptLength = (originalPrompt as string).length
|
||||
console.log(
|
||||
'[OpenAIProvider.summaries] Applied memories to prompt, length difference:',
|
||||
enhancedPrompt.length - originalPromptLength
|
||||
)
|
||||
|
||||
const systemMessage = {
|
||||
role: 'system',
|
||||
content: getStoreSetting('topicNamingPrompt') || i18n.t('prompts.title')
|
||||
content: enhancedPrompt
|
||||
}
|
||||
|
||||
const userMessage = {
|
||||
@@ -696,18 +738,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 = (await applyMemoriesToPrompt(prompt as string)) as unknown as string
|
||||
// 存储原始提示词长度
|
||||
const promptLength = (prompt as string).length
|
||||
console.log('[OpenAIProvider] Applied memories to prompt, length difference:', enhancedPrompt.length - promptLength)
|
||||
|
||||
const response = await this.sdk.chat.completions.create({
|
||||
model: model.id,
|
||||
stream: false,
|
||||
messages: [
|
||||
{ role: 'system', content: prompt },
|
||||
{ role: 'system', content: enhancedPrompt },
|
||||
{ role: 'user', content }
|
||||
]
|
||||
})
|
||||
@@ -789,7 +859,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,16 @@ 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 ''
|
||||
}
|
||||
}
|
||||
|
||||
525
src/renderer/src/services/ContextualMemoryService.ts
Normal file
525
src/renderer/src/services/ContextualMemoryService.ts
Normal file
@@ -0,0 +1,525 @@
|
||||
// src/renderer/src/services/ContextualMemoryService.ts
|
||||
|
||||
import { TopicManager } from '@renderer/hooks/useTopic'
|
||||
import { fetchGenerate } from '@renderer/services/ApiService'
|
||||
import store from '@renderer/store'
|
||||
import { addMemoryRetrievalLatency } from '@renderer/store/memory'
|
||||
import { Message } from '@renderer/types'
|
||||
|
||||
import { vectorService } from './VectorService'
|
||||
|
||||
// 记忆项接口(从store/memory.ts导入)
|
||||
interface Memory {
|
||||
id: string
|
||||
content: string
|
||||
createdAt: string
|
||||
source?: string
|
||||
category?: string
|
||||
listId: string
|
||||
analyzedMessageIds?: string[]
|
||||
lastMessageId?: string
|
||||
topicId?: string
|
||||
vector?: number[]
|
||||
entities?: string[]
|
||||
keywords?: string[]
|
||||
importance?: number
|
||||
accessCount?: number
|
||||
lastAccessedAt?: string
|
||||
decayFactor?: number
|
||||
freshness?: number
|
||||
}
|
||||
|
||||
interface ShortMemory {
|
||||
id: string
|
||||
content: string
|
||||
createdAt: string
|
||||
topicId: string
|
||||
analyzedMessageIds?: string[]
|
||||
lastMessageId?: string
|
||||
vector?: number[]
|
||||
entities?: string[]
|
||||
keywords?: string[]
|
||||
importance?: number
|
||||
accessCount?: number
|
||||
lastAccessedAt?: string
|
||||
decayFactor?: number
|
||||
freshness?: number
|
||||
}
|
||||
|
||||
// 记忆推荐结果接口
|
||||
export interface MemoryRecommendation {
|
||||
memory: Memory | ShortMemory
|
||||
relevanceScore: number
|
||||
source: 'long-term' | 'short-term'
|
||||
matchReason?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* ContextualMemoryService 类负责实现上下文感知的记忆推荐和检索功能
|
||||
*/
|
||||
class ContextualMemoryService {
|
||||
/**
|
||||
* 基于当前对话上下文推荐相关记忆
|
||||
* @param messages - 当前对话的消息列表
|
||||
* @param topicId - 当前对话的话题ID
|
||||
* @param limit - 返回的最大记忆数量
|
||||
* @returns 推荐的记忆列表,按相关性排序
|
||||
*/
|
||||
async getContextualMemoryRecommendations(
|
||||
messages: Message[],
|
||||
topicId: string,
|
||||
limit: number = 5
|
||||
): Promise<MemoryRecommendation[]> {
|
||||
console.log(`[ContextualMemory] Getting contextual memory recommendations for topic ${topicId}`)
|
||||
|
||||
const startTime = performance.now()
|
||||
|
||||
try {
|
||||
// 获取当前状态
|
||||
const state = store.getState()
|
||||
const memoryState = state.memory
|
||||
|
||||
if (!memoryState) {
|
||||
console.log('[ContextualMemory] Memory state not available')
|
||||
return []
|
||||
}
|
||||
|
||||
// 检查记忆功能是否激活
|
||||
if (!memoryState.isActive && !memoryState.shortMemoryActive) {
|
||||
console.log('[ContextualMemory] Memory features are not active')
|
||||
return []
|
||||
}
|
||||
|
||||
// 获取最近的消息作为上下文
|
||||
const recentMessages = messages.slice(-5)
|
||||
if (recentMessages.length === 0) {
|
||||
console.log('[ContextualMemory] No recent messages available')
|
||||
return []
|
||||
}
|
||||
|
||||
// 构建上下文查询文本
|
||||
const contextQuery = this._buildContextQuery(recentMessages)
|
||||
console.log(`[ContextualMemory] Context query: ${contextQuery}`)
|
||||
|
||||
// 并行获取长期记忆和短期记忆的推荐
|
||||
const [longTermRecommendations, shortTermRecommendations] = await Promise.all([
|
||||
this._getLongTermMemoryRecommendations(contextQuery, topicId),
|
||||
this._getShortTermMemoryRecommendations(contextQuery, topicId)
|
||||
])
|
||||
|
||||
// 合并并排序推荐结果
|
||||
const allRecommendations = [...longTermRecommendations, ...shortTermRecommendations]
|
||||
|
||||
// 按相关性分数排序
|
||||
allRecommendations.sort((a, b) => b.relevanceScore - a.relevanceScore)
|
||||
|
||||
// 限制返回数量
|
||||
const limitedRecommendations = allRecommendations.slice(0, limit)
|
||||
|
||||
// 记录性能指标
|
||||
const endTime = performance.now()
|
||||
const latency = endTime - startTime
|
||||
store.dispatch(addMemoryRetrievalLatency(latency))
|
||||
|
||||
console.log(
|
||||
`[ContextualMemory] Found ${limitedRecommendations.length} recommendations in ${latency.toFixed(2)}ms`
|
||||
)
|
||||
|
||||
return limitedRecommendations
|
||||
} catch (error) {
|
||||
console.error('[ContextualMemory] Error getting contextual memory recommendations:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于当前对话主题自动提取相关记忆
|
||||
* @param topicId - 当前对话的话题ID
|
||||
* @param limit - 返回的最大记忆数量
|
||||
* @returns 与当前主题相关的记忆列表
|
||||
*/
|
||||
async getTopicRelatedMemories(topicId: string, limit: number = 10): Promise<MemoryRecommendation[]> {
|
||||
console.log(`[ContextualMemory] Getting topic-related memories for topic ${topicId}`)
|
||||
|
||||
try {
|
||||
// 获取当前状态
|
||||
const state = store.getState()
|
||||
const memoryState = state.memory
|
||||
const messagesState = state.messages
|
||||
|
||||
if (!memoryState || !messagesState) {
|
||||
console.log('[ContextualMemory] Required state not available')
|
||||
return []
|
||||
}
|
||||
|
||||
// 获取话题信息
|
||||
// 使用TopicManager获取话题
|
||||
let topicQuery = ''
|
||||
try {
|
||||
const topic = await TopicManager.getTopic(topicId)
|
||||
if (!topic) {
|
||||
console.log(`[ContextualMemory] Topic ${topicId} not found`)
|
||||
return []
|
||||
}
|
||||
|
||||
// 使用话题ID作为查询
|
||||
// 注意:TopicManager.getTopic返回的类型只有id和messages属性
|
||||
topicQuery = `Topic ${topicId}`
|
||||
if (!topicQuery.trim()) {
|
||||
console.log('[ContextualMemory] No topic information available for query')
|
||||
return []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[ContextualMemory] Error getting topic ${topicId}:`, error)
|
||||
return []
|
||||
}
|
||||
|
||||
console.log(`[ContextualMemory] Topic query: ${topicQuery}`)
|
||||
|
||||
// 并行获取长期记忆和短期记忆的推荐
|
||||
const [longTermRecommendations, shortTermRecommendations] = await Promise.all([
|
||||
this._getLongTermMemoryRecommendations(topicQuery, topicId),
|
||||
this._getShortTermMemoryRecommendations(topicQuery, topicId)
|
||||
])
|
||||
|
||||
// 合并并排序推荐结果
|
||||
const allRecommendations = [...longTermRecommendations, ...shortTermRecommendations]
|
||||
|
||||
// 按相关性分数排序
|
||||
allRecommendations.sort((a, b) => b.relevanceScore - a.relevanceScore)
|
||||
|
||||
// 限制返回数量
|
||||
const limitedRecommendations = allRecommendations.slice(0, limit)
|
||||
|
||||
console.log(`[ContextualMemory] Found ${limitedRecommendations.length} topic-related memories`)
|
||||
|
||||
return limitedRecommendations
|
||||
} catch (error) {
|
||||
console.error('[ContextualMemory] Error getting topic-related memories:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用语义搜索查找与查询相关的记忆
|
||||
* @param query - 搜索查询
|
||||
* @param limit - 返回的最大记忆数量
|
||||
* @returns 与查询相关的记忆列表
|
||||
*/
|
||||
async searchMemoriesBySemantics(query: string, limit: number = 10): Promise<MemoryRecommendation[]> {
|
||||
console.log(`[ContextualMemory] Semantic search for: ${query}`)
|
||||
|
||||
try {
|
||||
// 获取当前状态
|
||||
const state = store.getState()
|
||||
const memoryState = state.memory
|
||||
|
||||
if (!memoryState) {
|
||||
console.log('[ContextualMemory] Memory state not available')
|
||||
return []
|
||||
}
|
||||
|
||||
// 并行获取长期记忆和短期记忆的推荐
|
||||
const [longTermRecommendations, shortTermRecommendations] = await Promise.all([
|
||||
this._getLongTermMemoryRecommendations(query),
|
||||
this._getShortTermMemoryRecommendations(query)
|
||||
])
|
||||
|
||||
// 合并并排序推荐结果
|
||||
const allRecommendations = [...longTermRecommendations, ...shortTermRecommendations]
|
||||
|
||||
// 按相关性分数排序
|
||||
allRecommendations.sort((a, b) => b.relevanceScore - a.relevanceScore)
|
||||
|
||||
// 限制返回数量
|
||||
const limitedRecommendations = allRecommendations.slice(0, limit)
|
||||
|
||||
console.log(`[ContextualMemory] Found ${limitedRecommendations.length} memories matching query`)
|
||||
|
||||
return limitedRecommendations
|
||||
} catch (error) {
|
||||
console.error('[ContextualMemory] Error searching memories by semantics:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用AI分析当前对话上下文,提取关键信息并推荐相关记忆
|
||||
* @param messages - 当前对话的消息列表
|
||||
* @param limit - 返回的最大记忆数量
|
||||
* @returns 基于AI分析的相关记忆推荐
|
||||
*/
|
||||
async getAIEnhancedMemoryRecommendations(messages: Message[], limit: number = 5): Promise<MemoryRecommendation[]> {
|
||||
console.log('[ContextualMemory] Getting AI-enhanced memory recommendations')
|
||||
|
||||
try {
|
||||
// 获取当前状态
|
||||
const state = store.getState()
|
||||
const memoryState = state.memory
|
||||
|
||||
if (!memoryState) {
|
||||
console.log('[ContextualMemory] Memory state not available')
|
||||
return []
|
||||
}
|
||||
|
||||
// 获取分析模型
|
||||
const analyzeModel = memoryState.analyzeModel
|
||||
if (!analyzeModel) {
|
||||
console.log('[ContextualMemory] No analyze model set')
|
||||
return []
|
||||
}
|
||||
|
||||
// 获取最近的消息作为上下文
|
||||
const recentMessages = messages.slice(-10)
|
||||
if (recentMessages.length === 0) {
|
||||
console.log('[ContextualMemory] No recent messages available')
|
||||
return []
|
||||
}
|
||||
|
||||
// 构建对话内容
|
||||
const conversation = recentMessages.map((msg) => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n')
|
||||
|
||||
// 构建提示词
|
||||
const prompt = `
|
||||
请分析以下对话内容,提取出关键信息和主题,以便我可以找到相关的记忆。
|
||||
|
||||
请提供:
|
||||
1. 对话的主要主题
|
||||
2. 用户可能关心的关键信息点
|
||||
3. 可能与此对话相关的背景知识或上下文
|
||||
|
||||
请以简洁的关键词和短语形式回答,每行一个要点,不要使用编号或项目符号。
|
||||
|
||||
对话内容:
|
||||
${conversation}
|
||||
`
|
||||
|
||||
// 调用AI生成文本
|
||||
console.log('[ContextualMemory] Calling AI for context analysis...')
|
||||
const result = await fetchGenerate({
|
||||
prompt: prompt,
|
||||
content: conversation,
|
||||
modelId: analyzeModel
|
||||
})
|
||||
|
||||
if (!result || typeof result !== 'string' || result.trim() === '') {
|
||||
console.log('[ContextualMemory] No valid result from AI analysis')
|
||||
return []
|
||||
}
|
||||
|
||||
// 提取关键信息
|
||||
const keyPoints = result
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && !line.startsWith('#') && !line.startsWith('-'))
|
||||
|
||||
console.log('[ContextualMemory] Extracted key points:', keyPoints)
|
||||
|
||||
// 使用提取的关键信息作为查询
|
||||
const enhancedQuery = keyPoints.join(' ')
|
||||
|
||||
// 获取相关记忆
|
||||
return await this.searchMemoriesBySemantics(enhancedQuery, limit)
|
||||
} catch (error) {
|
||||
console.error('[ContextualMemory] Error getting AI-enhanced memory recommendations:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建上下文查询文本
|
||||
* @param messages - 消息列表
|
||||
* @returns 构建的查询文本
|
||||
* @private
|
||||
*/
|
||||
private _buildContextQuery(messages: Message[]): string {
|
||||
// 提取最近消息的内容
|
||||
const messageContents = messages.map((msg) => msg.content || '').filter((content) => content.trim() !== '')
|
||||
|
||||
// 如果没有有效内容,返回空字符串
|
||||
if (messageContents.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 合并消息内容,最多使用最近的3条消息
|
||||
return messageContents.slice(-3).join(' ')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取与查询相关的长期记忆推荐
|
||||
* @param query - 查询文本
|
||||
* @param topicId - 可选的话题ID,用于过滤记忆
|
||||
* @returns 长期记忆推荐列表
|
||||
* @private
|
||||
*/
|
||||
private async _getLongTermMemoryRecommendations(query: string, topicId?: string): Promise<MemoryRecommendation[]> {
|
||||
// 获取当前状态
|
||||
const state = store.getState()
|
||||
const memoryState = state.memory
|
||||
|
||||
// 检查长期记忆功能是否激活
|
||||
if (!memoryState || !memoryState.isActive) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 获取所有激活的记忆列表
|
||||
const activeListIds = memoryState.memoryLists.filter((list) => list.isActive).map((list) => list.id)
|
||||
|
||||
if (activeListIds.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 获取激活列表中的记忆
|
||||
const memories = memoryState.memories.filter((memory) => activeListIds.includes(memory.listId))
|
||||
|
||||
if (memories.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 使用向量服务查找相似记忆
|
||||
const results = await vectorService.findSimilarMemoriesToQuery(
|
||||
query,
|
||||
memories,
|
||||
20, // 获取更多结果,后续会进一步优化排序
|
||||
0.5 // 降低阈值以获取更多潜在相关记忆
|
||||
)
|
||||
|
||||
// 转换为推荐格式
|
||||
const recommendations: MemoryRecommendation[] = results.map((result) => ({
|
||||
memory: result.memory as Memory,
|
||||
relevanceScore: result.similarity,
|
||||
source: 'long-term',
|
||||
matchReason: '语义相似'
|
||||
}))
|
||||
|
||||
// 应用高级排序优化
|
||||
return this._optimizeRelevanceRanking(recommendations, query, topicId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取与查询相关的短期记忆推荐
|
||||
* @param query - 查询文本
|
||||
* @param topicId - 可选的话题ID,用于过滤记忆
|
||||
* @returns 短期记忆推荐列表
|
||||
* @private
|
||||
*/
|
||||
private async _getShortTermMemoryRecommendations(query: string, topicId?: string): Promise<MemoryRecommendation[]> {
|
||||
// 获取当前状态
|
||||
const state = store.getState()
|
||||
const memoryState = state.memory
|
||||
|
||||
// 检查短期记忆功能是否激活
|
||||
if (!memoryState || !memoryState.shortMemoryActive) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 获取短期记忆
|
||||
let shortMemories = memoryState.shortMemories
|
||||
|
||||
// 如果指定了话题ID,只获取该话题的短期记忆
|
||||
if (topicId) {
|
||||
shortMemories = shortMemories.filter((memory) => memory.topicId === topicId)
|
||||
}
|
||||
|
||||
if (shortMemories.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 使用向量服务查找相似记忆
|
||||
const results = await vectorService.findSimilarMemoriesToQuery(
|
||||
query,
|
||||
shortMemories,
|
||||
20, // 获取更多结果,后续会进一步优化排序
|
||||
0.5 // 降低阈值以获取更多潜在相关记忆
|
||||
)
|
||||
|
||||
// 转换为推荐格式
|
||||
const recommendations: MemoryRecommendation[] = results.map((result) => ({
|
||||
memory: result.memory as ShortMemory,
|
||||
relevanceScore: result.similarity,
|
||||
source: 'short-term',
|
||||
matchReason: '与当前对话相关'
|
||||
}))
|
||||
|
||||
// 应用高级排序优化
|
||||
return this._optimizeRelevanceRanking(recommendations, query, topicId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化记忆推荐的相关性排序
|
||||
* @param recommendations - 初始推荐列表
|
||||
* @param query - 查询文本
|
||||
* @param topicId - 可选的话题ID
|
||||
* @returns 优化排序后的推荐列表
|
||||
* @private
|
||||
*/
|
||||
private _optimizeRelevanceRanking(
|
||||
recommendations: MemoryRecommendation[],
|
||||
query: string,
|
||||
topicId?: string
|
||||
): MemoryRecommendation[] {
|
||||
if (recommendations.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 获取当前状态
|
||||
const state = store.getState()
|
||||
const memoryState = state.memory
|
||||
|
||||
// 应用多因素排序优化
|
||||
return recommendations
|
||||
.map((rec) => {
|
||||
const memory = rec.memory
|
||||
let adjustedScore = rec.relevanceScore
|
||||
|
||||
// 1. 考虑记忆的重要性
|
||||
if (memory.importance && memoryState.priorityManagementEnabled) {
|
||||
adjustedScore *= 1 + memory.importance * 0.5 // 重要性最多提升50%的分数
|
||||
}
|
||||
|
||||
// 2. 考虑记忆的鲜度
|
||||
if (memory.freshness && memoryState.freshnessEnabled) {
|
||||
adjustedScore *= 1 + memory.freshness * 0.3 // 鲜度最多提升30%的分数
|
||||
}
|
||||
|
||||
// 3. 考虑记忆的衰减因子
|
||||
if (memory.decayFactor && memoryState.decayEnabled) {
|
||||
adjustedScore *= memory.decayFactor // 直接应用衰减因子
|
||||
}
|
||||
|
||||
// 4. 如果记忆与当前话题相关,提高分数
|
||||
if (topicId && memory.topicId === topicId) {
|
||||
adjustedScore *= 1.2 // 提高20%的分数
|
||||
}
|
||||
|
||||
// 5. 考虑访问频率,常用的记忆可能更相关
|
||||
if (memory.accessCount && memory.accessCount > 0) {
|
||||
// 访问次数越多,提升越大,但有上限
|
||||
const accessBoost = Math.min(memory.accessCount / 10, 0.2) // 最多提升20%
|
||||
adjustedScore *= 1 + accessBoost
|
||||
}
|
||||
|
||||
// 6. 考虑关键词匹配
|
||||
if (memory.keywords && memory.keywords.length > 0) {
|
||||
const queryLower = query.toLowerCase()
|
||||
const keywordMatches = memory.keywords.filter((keyword) => queryLower.includes(keyword.toLowerCase())).length
|
||||
|
||||
if (keywordMatches > 0) {
|
||||
// 关键词匹配越多,提升越大
|
||||
const keywordBoost = Math.min(keywordMatches * 0.1, 0.3) // 最多提升30%
|
||||
adjustedScore *= 1 + keywordBoost
|
||||
}
|
||||
}
|
||||
|
||||
// 返回调整后的推荐
|
||||
return {
|
||||
...rec,
|
||||
relevanceScore: adjustedScore
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.relevanceScore - a.relevanceScore) // 按调整后的分数重新排序
|
||||
}
|
||||
}
|
||||
|
||||
// 导出 ContextualMemoryService 的单例
|
||||
export const contextualMemoryService = new ContextualMemoryService()
|
||||
239
src/renderer/src/services/HistoricalContextService.ts
Normal file
239
src/renderer/src/services/HistoricalContextService.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
// src/renderer/src/services/HistoricalContextService.ts
|
||||
import { TopicManager } from '@renderer/hooks/useTopic'
|
||||
import { fetchGenerate } from '@renderer/services/ApiService'
|
||||
import store from '@renderer/store'
|
||||
import { ShortMemory } from '@renderer/store/memory'
|
||||
import { Message } from '@renderer/types'
|
||||
|
||||
/**
|
||||
* 分析当前对话并决定是否需要调用历史对话
|
||||
* @param topicId 当前话题ID
|
||||
* @param recentMessageCount 要分析的最近消息数量
|
||||
* @param returnIdOnly 是否只返回话题ID而不获取完整内容(用于调试)
|
||||
* @returns 如果需要历史上下文,返回历史对话内容;否则返回null
|
||||
*/
|
||||
export const analyzeAndSelectHistoricalContext = async (
|
||||
topicId: string,
|
||||
recentMessageCount: number = 8,
|
||||
returnIdOnly: boolean = false
|
||||
): Promise<{ content: string; sourceTopicId: string } | null> => {
|
||||
try {
|
||||
// 1. 获取设置,检查功能是否启用
|
||||
const state = store.getState()
|
||||
const isEnabled = state.settings?.enableHistoricalContext ?? false
|
||||
|
||||
if (!isEnabled) {
|
||||
console.log('[HistoricalContext] Feature is disabled')
|
||||
return null
|
||||
}
|
||||
|
||||
// 2. 获取最近的消息
|
||||
const recentMessages = await getRecentMessages(topicId, recentMessageCount)
|
||||
if (!recentMessages || recentMessages.length === 0) {
|
||||
console.log('[HistoricalContext] No recent messages found')
|
||||
return null
|
||||
}
|
||||
|
||||
// 3. 获取所有短期记忆(已分析的对话)
|
||||
const shortMemories = state.memory?.shortMemories || []
|
||||
if (shortMemories.length === 0) {
|
||||
console.log('[HistoricalContext] No short memories available')
|
||||
return null
|
||||
}
|
||||
|
||||
// 4. 使用快速模型分析是否需要历史上下文
|
||||
const analysisResult = await analyzeNeedForHistoricalContext(recentMessages, shortMemories)
|
||||
if (!analysisResult.needsHistoricalContext) {
|
||||
console.log('[HistoricalContext] Analysis indicates no need for historical context')
|
||||
return null
|
||||
}
|
||||
|
||||
// 5. 如果需要历史上下文,获取原始对话内容
|
||||
if (analysisResult.selectedTopicId) {
|
||||
// 如果只需要返回ID,则不获取完整内容(用于调试)
|
||||
if (returnIdOnly) {
|
||||
return {
|
||||
content: `话题ID: ${analysisResult.selectedTopicId}\n原因: ${analysisResult.reason || '相关历史对话'}`,
|
||||
sourceTopicId: analysisResult.selectedTopicId
|
||||
}
|
||||
}
|
||||
|
||||
// 正常情况下,获取完整对话内容
|
||||
const dialogContent = await getOriginalDialogContent(analysisResult.selectedTopicId)
|
||||
if (dialogContent) {
|
||||
return {
|
||||
content: dialogContent,
|
||||
sourceTopicId: analysisResult.selectedTopicId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('[HistoricalContext] Error analyzing and selecting historical context:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定话题的最近消息
|
||||
*/
|
||||
const getRecentMessages = async (topicId: string, count: number): Promise<Message[]> => {
|
||||
try {
|
||||
// 先尝试从Redux store获取
|
||||
const state = store.getState()
|
||||
let messages: Message[] = []
|
||||
|
||||
if (state.messages?.messagesByTopic && state.messages.messagesByTopic[topicId]) {
|
||||
messages = state.messages.messagesByTopic[topicId]
|
||||
} else {
|
||||
// 如果Redux store中没有,从数据库获取
|
||||
const topicMessages = await TopicManager.getTopicMessages(topicId)
|
||||
if (topicMessages && topicMessages.length > 0) {
|
||||
messages = topicMessages
|
||||
}
|
||||
}
|
||||
|
||||
// 返回最近的count条消息
|
||||
return messages.slice(-count)
|
||||
} catch (error) {
|
||||
console.error('[HistoricalContext] Error getting recent messages:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析是否需要历史上下文
|
||||
*/
|
||||
const analyzeNeedForHistoricalContext = async (
|
||||
recentMessages: Message[],
|
||||
shortMemories: ShortMemory[]
|
||||
): Promise<{ needsHistoricalContext: boolean; selectedTopicId?: string; reason?: string }> => {
|
||||
try {
|
||||
// 准备分析提示词
|
||||
const messagesContent = recentMessages
|
||||
.map((msg) => `${msg.role === 'user' ? '用户' : 'AI'}: ${msg.content}`)
|
||||
.join('\n')
|
||||
|
||||
const memoriesContent = shortMemories
|
||||
.map((memory) => `话题ID: ${memory.topicId}\n内容: ${memory.content}`)
|
||||
.join('\n\n')
|
||||
|
||||
const prompt = `
|
||||
你是一个专门分析对话上下文的助手,你的任务是判断当前对话是否需要引用历史对话来提供更完整、更连贯的回答。
|
||||
|
||||
最近的对话内容:
|
||||
${messagesContent}
|
||||
|
||||
可用的历史对话摘要:
|
||||
${memoriesContent}
|
||||
|
||||
请仔细分析用户的问题和可用的历史对话摘要。考虑以下因素:
|
||||
|
||||
1. 用户当前问题是否与历史对话中的任何主题相关
|
||||
2. 历史对话中是否包含可能对当前问题有帮助的信息
|
||||
3. 引用历史对话是否能使回答更全面、更个性化
|
||||
4. 即使用户没有直接提及历史内容,但如果历史对话中有相关信息,也应考虑引用
|
||||
|
||||
请积极地寻找可能的联系,即使联系不是非常明显的。如果有任何可能相关的历史对话,请倾向于引用它。
|
||||
|
||||
请回答以下问题:
|
||||
1. 是否需要引用历史对话来更好地回答用户的问题?(是/否)
|
||||
2. 如果需要,请指出最相关的历史对话的话题ID。
|
||||
3. 详细解释为什么需要引用这个历史对话,以及它如何与当前问题相关。
|
||||
|
||||
请按以下JSON格式回答,不要添加任何其他文本:
|
||||
{
|
||||
"needsHistoricalContext": true/false,
|
||||
"selectedTopicId": "话题ID或null",
|
||||
"reason": "详细解释为什么需要或不需要引用历史对话"
|
||||
}
|
||||
`
|
||||
|
||||
// 获取分析模型
|
||||
const state = store.getState()
|
||||
// 优先使用历史对话上下文分析模型,如果没有设置,则使用短期记忆分析模型或长期记忆分析模型
|
||||
const analyzeModel =
|
||||
state.memory?.historicalContextAnalyzeModel || state.memory?.shortMemoryAnalyzeModel || state.memory?.analyzeModel
|
||||
|
||||
if (!analyzeModel) {
|
||||
console.log('[HistoricalContext] No analyze model set')
|
||||
return { needsHistoricalContext: false }
|
||||
}
|
||||
|
||||
// 调用模型进行分析
|
||||
console.log('[HistoricalContext] Calling AI model for analysis...')
|
||||
const result = await fetchGenerate({
|
||||
prompt,
|
||||
content: '',
|
||||
modelId: analyzeModel
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
console.log('[HistoricalContext] No result from AI analysis')
|
||||
return { needsHistoricalContext: false }
|
||||
}
|
||||
|
||||
// 解析结果
|
||||
try {
|
||||
// 尝试直接解析JSON
|
||||
const parsedResult = JSON.parse(result)
|
||||
return {
|
||||
needsHistoricalContext: parsedResult.needsHistoricalContext === true,
|
||||
selectedTopicId: parsedResult.selectedTopicId || undefined,
|
||||
reason: parsedResult.reason
|
||||
}
|
||||
} catch (parseError) {
|
||||
// 如果直接解析失败,尝试从文本中提取JSON
|
||||
const jsonMatch = result.match(/\{[\s\S]*\}/)
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
const extractedJson = JSON.parse(jsonMatch[0])
|
||||
return {
|
||||
needsHistoricalContext: extractedJson.needsHistoricalContext === true,
|
||||
selectedTopicId: extractedJson.selectedTopicId || undefined,
|
||||
reason: extractedJson.reason
|
||||
}
|
||||
} catch (extractError) {
|
||||
console.error('[HistoricalContext] Failed to extract JSON from result:', extractError)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果都失败了,尝试简单的文本分析
|
||||
const needsContext = result.toLowerCase().includes('true') && !result.toLowerCase().includes('false')
|
||||
const topicIdMatch = result.match(/selectedTopicId["\s:]+([^"\s,}]+)/)
|
||||
const reasonMatch = result.match(/reason["\s:]+"([^"]+)"/) || result.match(/reason["\s:]+([^,}\s]+)/)
|
||||
|
||||
return {
|
||||
needsHistoricalContext: needsContext,
|
||||
selectedTopicId: topicIdMatch ? topicIdMatch[1] : undefined,
|
||||
reason: reasonMatch ? reasonMatch[1] : undefined
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[HistoricalContext] Error analyzing need for historical context:', error)
|
||||
return { needsHistoricalContext: false }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始对话内容
|
||||
*/
|
||||
const getOriginalDialogContent = async (topicId: string): Promise<string | null> => {
|
||||
try {
|
||||
// 获取话题的原始消息
|
||||
const messages = await TopicManager.getTopicMessages(topicId)
|
||||
if (!messages || messages.length === 0) {
|
||||
console.log(`[HistoricalContext] No messages found for topic ${topicId}`)
|
||||
return null
|
||||
}
|
||||
|
||||
// 格式化对话内容
|
||||
const dialogContent = messages.map((msg) => `${msg.role === 'user' ? '用户' : 'AI'}: ${msg.content}`).join('\n\n')
|
||||
|
||||
return dialogContent
|
||||
} catch (error) {
|
||||
console.error('[HistoricalContext] Error getting original dialog content:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
341
src/renderer/src/services/MemoryDeduplicationService.ts
Normal file
341
src/renderer/src/services/MemoryDeduplicationService.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
// 记忆去重与合并服务
|
||||
import { fetchGenerate } from '@renderer/services/ApiService'
|
||||
import store from '@renderer/store'
|
||||
import {
|
||||
addMemory,
|
||||
addShortMemory,
|
||||
deleteMemory,
|
||||
deleteShortMemory,
|
||||
saveLongTermMemoryData,
|
||||
saveMemoryData
|
||||
} from '@renderer/store/memory'
|
||||
|
||||
// 记忆去重与合并的结果接口
|
||||
export interface DeduplicationResult {
|
||||
similarGroups: {
|
||||
groupId: string
|
||||
memoryIds: string[]
|
||||
mergedContent: string
|
||||
category?: string
|
||||
importance?: number // 新增重要性评分
|
||||
keywords?: string[] // 新增关键词
|
||||
}[]
|
||||
independentMemories: string[]
|
||||
rawResponse: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析记忆库中的相似记忆,提供智能合并建议
|
||||
* @param listId 可选的列表ID,如果不提供则处理所有列表
|
||||
* @param isShortMemory 是否处理短期记忆
|
||||
* @param topicId 当处理短期记忆时,可选的话题ID
|
||||
* @returns 去重分析结果
|
||||
*/
|
||||
export const deduplicateAndMergeMemories = async (
|
||||
listId?: string,
|
||||
isShortMemory: boolean = false,
|
||||
topicId?: string
|
||||
): Promise<DeduplicationResult | null> => {
|
||||
// 获取需要处理的记忆
|
||||
const state = store.getState()
|
||||
|
||||
let targetMemories: any[] = []
|
||||
|
||||
if (isShortMemory) {
|
||||
// 处理短期记忆
|
||||
const shortMemories = state.memory?.shortMemories || []
|
||||
targetMemories = topicId ? shortMemories.filter((memory) => memory.topicId === topicId) : shortMemories
|
||||
} else {
|
||||
// 处理长期记忆
|
||||
const memories = state.memory?.memories || []
|
||||
targetMemories = listId ? memories.filter((memory) => memory.listId === listId) : memories
|
||||
}
|
||||
|
||||
if (targetMemories.length < 2) {
|
||||
console.log('[Memory Deduplication] Not enough memories to deduplicate')
|
||||
return null
|
||||
}
|
||||
|
||||
const memoryType = isShortMemory ? 'short memories' : 'memories'
|
||||
console.log(`[Memory Deduplication] Starting deduplication for ${targetMemories.length} ${memoryType}`)
|
||||
|
||||
// 构建去重提示词
|
||||
const memoriesToCheck = targetMemories
|
||||
.map((memory, index) => {
|
||||
if (isShortMemory) {
|
||||
return `${index + 1}. 短期记忆: ${memory.content}`
|
||||
} else {
|
||||
return `${index + 1}. ${memory.category || '其他'}: ${memory.content}`
|
||||
}
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
const prompt = `
|
||||
请仔细分析以下记忆项,识别语义相似或包含重复信息的条目,并提供智能合并建议。
|
||||
|
||||
相似度判断标准:
|
||||
1. 语义相似:即使表述不同,但表达相同或非常相似的意思
|
||||
2. 内容重叠:一个记忆项包含另一个记忆项的大部分信息
|
||||
3. 主题相同:描述同一个主题或事件的不同方面
|
||||
|
||||
记忆项列表:
|
||||
${memoriesToCheck}
|
||||
|
||||
例如,以下记忆应被视为相似:
|
||||
- "用户喜欢简洁的界面设计"和"用户偏好简单直观的UI"
|
||||
- "用户正在开发一个网站项目"和"用户在进行网站开发工作"
|
||||
- "用户正在准备完成一个项目"和"用户正在进行一个项目的工作"
|
||||
|
||||
请按以下格式返回结果:
|
||||
1. 识别出的相似组:
|
||||
- 组1: [记忆项编号,如"1,5,8"] - 合并建议: "合并后的内容" - 分类: "最合适的分类"
|
||||
- 组2: [记忆项编号] - 合并建议: "合并后的内容" - 分类: "最合适的分类"
|
||||
...
|
||||
|
||||
2. 独立记忆项: [不需要合并的记忆项编号]
|
||||
|
||||
合并建议要求:
|
||||
- 保留所有非重复的有价值信息
|
||||
- 使用简洁清晰的语言
|
||||
- 确保合并后的内容比原始记忆更加全面和准确
|
||||
- 如果记忆项之间有细微差异,请在合并内容中保留这些差异
|
||||
|
||||
如果没有发现相似记忆,请返回"未发现相似记忆"。
|
||||
`
|
||||
|
||||
try {
|
||||
// 使用AI模型进行去重分析
|
||||
const analyzeModel = state.memory?.analyzeModel
|
||||
if (!analyzeModel) {
|
||||
console.log('[Memory Deduplication] No analyze model set')
|
||||
return null
|
||||
}
|
||||
|
||||
console.log('[Memory Deduplication] Calling AI model for analysis...')
|
||||
const result = await fetchGenerate({
|
||||
prompt: prompt,
|
||||
content: memoriesToCheck,
|
||||
modelId: analyzeModel
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
console.log('[Memory Deduplication] No result from AI analysis')
|
||||
return null
|
||||
}
|
||||
|
||||
console.log('[Memory Deduplication] Analysis result:', result)
|
||||
|
||||
// 解析结果
|
||||
const similarGroups: DeduplicationResult['similarGroups'] = []
|
||||
const independentMemories: string[] = []
|
||||
|
||||
// 检查是否没有发现相似记忆
|
||||
if (result.includes('未发现相似记忆')) {
|
||||
console.log('[Memory Deduplication] No similar memories found')
|
||||
return {
|
||||
similarGroups: [],
|
||||
independentMemories: targetMemories.map((_, index) => String(index + 1)),
|
||||
rawResponse: result
|
||||
}
|
||||
}
|
||||
|
||||
// 解析相似组
|
||||
const similarGroupsMatch = result.match(/1\.\s*识别出的相似组:([\s\S]*?)(?=2\.\s*独立记忆项:|$)/i)
|
||||
if (similarGroupsMatch && similarGroupsMatch[1]) {
|
||||
const groupsText = similarGroupsMatch[1].trim()
|
||||
// 更新正则表达式以匹配新的格式,包括重要性和关键词
|
||||
const groupRegex =
|
||||
/-\s*组(\d+)?:\s*\[([\d,\s]+)\]\s*-\s*合并建议:\s*"([^"]+)"\s*-\s*分类:\s*"([^"]+)"\s*(?:-\s*重要性:\s*"([^"]+)")?\s*(?:-\s*关键词:\s*"([^"]+)")?/g
|
||||
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = groupRegex.exec(groupsText)) !== null) {
|
||||
const groupId = match[1] || String(similarGroups.length + 1)
|
||||
const memoryIndices = match[2].split(',').map((s: string) => s.trim())
|
||||
const mergedContent = match[3].trim()
|
||||
const category = match[4]?.trim()
|
||||
const importance = match[5] ? parseFloat(match[5].trim()) : undefined
|
||||
const keywords = match[6]
|
||||
? match[6]
|
||||
.trim()
|
||||
.split(',')
|
||||
.map((k: string) => k.trim())
|
||||
: undefined
|
||||
|
||||
similarGroups.push({
|
||||
groupId,
|
||||
memoryIds: memoryIndices,
|
||||
mergedContent,
|
||||
category,
|
||||
importance,
|
||||
keywords
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 解析独立记忆项
|
||||
const independentMatch = result.match(/2\.\s*独立记忆项:\s*\[([\d,\s]+)\]/i)
|
||||
if (independentMatch && independentMatch[1]) {
|
||||
independentMemories.push(...independentMatch[1].split(',').map((s: string) => s.trim()))
|
||||
}
|
||||
|
||||
console.log('[Memory Deduplication] Parsed result:', { similarGroups, independentMemories })
|
||||
|
||||
return {
|
||||
similarGroups,
|
||||
independentMemories,
|
||||
rawResponse: result
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Memory Deduplication] Error during deduplication:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 已在顶部导入saveMemoryData和saveLongTermMemoryData
|
||||
|
||||
/**
|
||||
* 应用去重结果,合并相似记忆
|
||||
* @param result 去重分析结果
|
||||
* @param autoApply 是否自动应用合并结果
|
||||
* @param isShortMemory 是否处理短期记忆
|
||||
*/
|
||||
export const applyDeduplicationResult = async (
|
||||
result: DeduplicationResult,
|
||||
autoApply: boolean = false,
|
||||
isShortMemory: boolean = false
|
||||
) => {
|
||||
if (!result || !result.similarGroups || result.similarGroups.length === 0) {
|
||||
console.log('[Memory Deduplication] No similar groups to apply')
|
||||
return
|
||||
}
|
||||
|
||||
const state = store.getState()
|
||||
const memories = isShortMemory ? state.memory?.shortMemories || [] : state.memory?.memories || []
|
||||
|
||||
// 处理每个相似组
|
||||
for (const group of result.similarGroups) {
|
||||
// 获取组中的记忆
|
||||
const memoryIndices = group.memoryIds.map((id) => parseInt(id) - 1)
|
||||
const groupMemories = memoryIndices.map((index) => memories[index]).filter(Boolean)
|
||||
|
||||
if (groupMemories.length < 2) continue
|
||||
|
||||
// 获取第一个记忆的列表ID和其他属性
|
||||
const firstMemory = groupMemories[0]
|
||||
|
||||
// 收集所有已分析过的消息ID
|
||||
const allAnalyzedMessageIds = new Set<string>()
|
||||
groupMemories.forEach((memory) => {
|
||||
if (memory.analyzedMessageIds) {
|
||||
memory.analyzedMessageIds.forEach((id) => allAnalyzedMessageIds.add(id))
|
||||
}
|
||||
})
|
||||
|
||||
// 找出最新的lastMessageId
|
||||
let lastMessageId: string | undefined
|
||||
groupMemories.forEach((memory) => {
|
||||
if (memory.lastMessageId) {
|
||||
if (!lastMessageId || new Date(memory.createdAt) > new Date(lastMessageId)) {
|
||||
lastMessageId = memory.lastMessageId
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 找出所有关联的话题ID
|
||||
const topicIds = new Set<string>()
|
||||
groupMemories.forEach((memory) => {
|
||||
if (memory.topicId) {
|
||||
topicIds.add(memory.topicId)
|
||||
}
|
||||
})
|
||||
|
||||
// 如果自动应用,则添加合并后的记忆并删除原记忆
|
||||
if (autoApply) {
|
||||
if (isShortMemory) {
|
||||
// 处理短期记忆
|
||||
// 添加合并后的短期记忆
|
||||
const topicId = topicIds.size === 1 ? Array.from(topicIds)[0] : undefined
|
||||
if (topicId) {
|
||||
store.dispatch(
|
||||
addShortMemory({
|
||||
content: group.mergedContent,
|
||||
topicId: topicId,
|
||||
analyzedMessageIds: Array.from(allAnalyzedMessageIds),
|
||||
lastMessageId: lastMessageId,
|
||||
importance: group.importance, // 添加重要性评分
|
||||
keywords: group.keywords // 添加关键词
|
||||
})
|
||||
)
|
||||
|
||||
// 删除原短期记忆
|
||||
for (const memory of groupMemories) {
|
||||
store.dispatch(deleteShortMemory(memory.id))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 处理长期记忆
|
||||
// 安全地获取 listId 和 category,因为它们只存在于 Memory 类型
|
||||
const listId = 'listId' in firstMemory ? firstMemory.listId : undefined
|
||||
const memoryCategory = 'category' in firstMemory ? firstMemory.category : undefined
|
||||
|
||||
// 添加合并后的记忆
|
||||
store.dispatch(
|
||||
addMemory({
|
||||
content: group.mergedContent,
|
||||
source: '自动合并',
|
||||
category: group.category || memoryCategory || '其他', // 使用安全获取的 category
|
||||
listId: listId, // 使用安全获取的 listId
|
||||
analyzedMessageIds: Array.from(allAnalyzedMessageIds),
|
||||
lastMessageId: lastMessageId,
|
||||
topicId: topicIds.size === 1 ? Array.from(topicIds)[0] : undefined,
|
||||
importance: group.importance, // 添加重要性评分
|
||||
keywords: group.keywords // 添加关键词
|
||||
})
|
||||
)
|
||||
|
||||
// 删除原记忆
|
||||
for (const memory of groupMemories) {
|
||||
store.dispatch(deleteMemory(memory.id))
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Memory Deduplication] Applied group ${group.groupId}: merged ${groupMemories.length} memories`)
|
||||
}
|
||||
}
|
||||
|
||||
// 合并完成后,将更改保存到文件
|
||||
if (autoApply) {
|
||||
try {
|
||||
// 获取最新的状态
|
||||
const currentState = store.getState().memory
|
||||
|
||||
// 保存到文件
|
||||
if (isShortMemory) {
|
||||
// 短期记忆使用saveMemoryData
|
||||
await store
|
||||
.dispatch(
|
||||
saveMemoryData({
|
||||
shortMemories: currentState.shortMemories
|
||||
})
|
||||
)
|
||||
.unwrap()
|
||||
console.log('[Memory Deduplication] Short memories saved to file after merging')
|
||||
} else {
|
||||
// 长期记忆使用saveLongTermMemoryData
|
||||
await store
|
||||
.dispatch(
|
||||
saveLongTermMemoryData({
|
||||
memories: currentState.memories,
|
||||
memoryLists: currentState.memoryLists,
|
||||
currentListId: currentState.currentListId,
|
||||
analyzeModel: currentState.analyzeModel
|
||||
})
|
||||
)
|
||||
.unwrap()
|
||||
console.log('[Memory Deduplication] Long-term memories saved to file after merging')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Memory Deduplication] Failed to save memory data after merging:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
1600
src/renderer/src/services/MemoryService.ts
Normal file
1600
src/renderer/src/services/MemoryService.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { DEFAULT_CONTEXTCOUNT } from '@renderer/config/constant'
|
||||
import db from '@renderer/databases'
|
||||
import { getTopicById } from '@renderer/hooks/useTopic'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { fetchMessagesSummary } from '@renderer/services/ApiService'
|
||||
@@ -200,6 +201,40 @@ export function getMessageModelId(message: Message) {
|
||||
return message?.model?.id || message.modelId
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据消息ID查找消息
|
||||
* @param messageId 消息ID
|
||||
* @returns 找到的消息,如果未找到则返回null
|
||||
*/
|
||||
export async function findMessageById(messageId: string): Promise<Message | null> {
|
||||
console.log(`[findMessageById] 正在查找消息ID: ${messageId}`)
|
||||
|
||||
try {
|
||||
// 获取所有话题
|
||||
const topics = await db.topics.toArray()
|
||||
console.log(`[findMessageById] 找到 ${topics.length} 个话题`)
|
||||
|
||||
// 遍历所有话题,查找消息
|
||||
for (const topic of topics) {
|
||||
if (!topic.messages || topic.messages.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const message = topic.messages.find((msg) => msg.id === messageId)
|
||||
if (message) {
|
||||
console.log(`[findMessageById] 在话题 ${topic.id} 中找到消息`)
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[findMessageById] 未找到消息ID: ${messageId}`)
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error(`[findMessageById] 查找消息时出错:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function resetAssistantMessage(message: Message, model?: Model): Message {
|
||||
return {
|
||||
...message,
|
||||
|
||||
260
src/renderer/src/services/VectorService.ts
Normal file
260
src/renderer/src/services/VectorService.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
// src/renderer/src/services/VectorService.ts
|
||||
|
||||
// 导入Memory和ShortMemory接口
|
||||
interface Memory {
|
||||
id: string
|
||||
content: string
|
||||
createdAt: string
|
||||
source?: string
|
||||
category?: string
|
||||
listId: string
|
||||
analyzedMessageIds?: string[]
|
||||
lastMessageId?: string
|
||||
topicId?: string
|
||||
vectorRepresentation?: number[]
|
||||
entities?: string[]
|
||||
keywords?: string[]
|
||||
importance?: number
|
||||
accessCount?: number
|
||||
lastAccessedAt?: string
|
||||
}
|
||||
|
||||
interface ShortMemory {
|
||||
id: string
|
||||
content: string
|
||||
createdAt: string
|
||||
topicId: string
|
||||
analyzedMessageIds?: string[]
|
||||
lastMessageId?: string
|
||||
vectorRepresentation?: number[]
|
||||
entities?: string[]
|
||||
keywords?: string[]
|
||||
importance?: number
|
||||
}
|
||||
// TODO: Import necessary API clients or libraries for vector embedding (e.g., OpenAI)
|
||||
|
||||
/**
|
||||
* 计算两个向量之间的余弦相似度
|
||||
* @param vecA - 第一个向量
|
||||
* @param vecB - 第二个向量
|
||||
* @returns 余弦相似度值 (-1 到 1)
|
||||
*/
|
||||
function cosineSimilarity(vecA: number[], vecB: number[]): number {
|
||||
if (!vecA || !vecB || vecA.length !== vecB.length || vecA.length === 0) {
|
||||
// console.error('Invalid vectors for cosine similarity calculation.', vecA, vecB)
|
||||
return 0 // 或者抛出错误,取决于错误处理策略
|
||||
}
|
||||
|
||||
let dotProduct = 0.0
|
||||
let normA = 0.0
|
||||
let normB = 0.0
|
||||
for (let i = 0; i < vecA.length; i++) {
|
||||
dotProduct += vecA[i] * vecB[i]
|
||||
normA += vecA[i] * vecA[i]
|
||||
normB += vecB[i] * vecB[i]
|
||||
}
|
||||
|
||||
if (normA === 0 || normB === 0) {
|
||||
// console.warn('Zero vector encountered in cosine similarity calculation.')
|
||||
return 0 // 避免除以零
|
||||
}
|
||||
|
||||
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB))
|
||||
}
|
||||
|
||||
// 简单的内存缓存来存储向量表示
|
||||
const vectorCache = new Map<string, number[]>()
|
||||
|
||||
/**
|
||||
* VectorService 类负责处理记忆内容的向量化和相似度计算
|
||||
*/
|
||||
class VectorService {
|
||||
/**
|
||||
* 获取给定文本的向量表示。
|
||||
* 优先从缓存获取,否则调用API生成。
|
||||
* @param text - 需要向量化的文本
|
||||
* @param modelId - 使用的向量化模型ID (TODO: 需要从设置或状态中获取)
|
||||
* @returns 文本的向量表示 (number[]) 或 null (如果失败)
|
||||
*/
|
||||
async getVector(text: string, modelId: string = 'text-embedding-ada-002'): Promise<number[] | null> {
|
||||
if (!text || text.trim() === '') {
|
||||
return null
|
||||
}
|
||||
|
||||
const cacheKey = `${modelId}:${text}`
|
||||
if (vectorCache.has(cacheKey)) {
|
||||
return vectorCache.get(cacheKey)!
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: 实现调用向量化API的逻辑
|
||||
console.log(`[VectorService] Requesting vector for text (length: ${text.length})...`)
|
||||
// 示例: const response = await openai.embeddings.create({ model: modelId, input: text });
|
||||
// const vector = response?.data?.[0]?.embedding;
|
||||
|
||||
// --- 占位符逻辑 ---
|
||||
// 实际应调用 API 获取向量
|
||||
// 这里生成一个随机向量作为占位符,维度需与模型一致
|
||||
const placeholderVector = Array.from({ length: 1536 }, () => Math.random() * 2 - 1) // 假设 ada-002 是 1536 维
|
||||
const vector = placeholderVector
|
||||
// --- 占位符结束 ---
|
||||
|
||||
if (vector) {
|
||||
vectorCache.set(cacheKey, vector)
|
||||
console.log(`[VectorService] Vector obtained and cached for text (length: ${text.length}).`)
|
||||
return vector
|
||||
} else {
|
||||
console.error('[VectorService] Failed to get vector embedding.')
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[VectorService] Error getting vector embedding:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保一个记忆项具有向量表示。
|
||||
* 如果没有,则尝试生成并更新。
|
||||
* @param memory - 记忆项 (Memory 或 ShortMemory)
|
||||
* @returns 更新后的记忆项 (如果成功生成向量) 或原记忆项
|
||||
*/
|
||||
async ensureVectorRepresentation(memory: Memory | ShortMemory): Promise<Memory | ShortMemory> {
|
||||
if (memory.vectorRepresentation && memory.vectorRepresentation.length > 0) {
|
||||
return memory // 已经有向量了
|
||||
}
|
||||
|
||||
// 从状态或设置中获取 vectorizeModel
|
||||
const vectorizeModel = 'text-embedding-ada-002' // 暂时硬编码
|
||||
const vector = await this.getVector(memory.content, vectorizeModel)
|
||||
|
||||
if (vector) {
|
||||
return { ...memory, vectorRepresentation: vector }
|
||||
}
|
||||
|
||||
return memory // 无法生成向量,返回原样
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个记忆项之间的语义相似度。
|
||||
* @param memoryA - 第一个记忆项
|
||||
* @param memoryB - 第二个记忆项
|
||||
* @returns 相似度分数 (0 到 1) 或 0 (如果无法计算)
|
||||
*/
|
||||
async calculateSimilarity(memoryA: Memory | ShortMemory, memoryB: Memory | ShortMemory): Promise<number> {
|
||||
try {
|
||||
const memoryAWithVector = await this.ensureVectorRepresentation(memoryA)
|
||||
const memoryBWithVector = await this.ensureVectorRepresentation(memoryB)
|
||||
|
||||
if (
|
||||
memoryAWithVector.vectorRepresentation &&
|
||||
memoryBWithVector.vectorRepresentation &&
|
||||
memoryAWithVector.vectorRepresentation.length > 0 &&
|
||||
memoryBWithVector.vectorRepresentation.length > 0
|
||||
) {
|
||||
const similarity = cosineSimilarity(
|
||||
memoryAWithVector.vectorRepresentation,
|
||||
memoryBWithVector.vectorRepresentation
|
||||
)
|
||||
// 将余弦相似度 (-1 到 1) 映射到 0 到 1 范围 (可选,但通常更直观)
|
||||
return (similarity + 1) / 2
|
||||
} else {
|
||||
// console.warn('[VectorService] Could not calculate similarity due to missing vectors.')
|
||||
return 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[VectorService] Error calculating similarity:', error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找与给定记忆最相似的记忆项列表。
|
||||
* @param targetMemory - 目标记忆项
|
||||
* @param candidates - 候选记忆项列表
|
||||
* @param topN - 返回最相似的 N 个结果
|
||||
* @param threshold - 相似度阈值 (0 到 1)
|
||||
* @returns 最相似的记忆项列表及其相似度分数
|
||||
*/
|
||||
async findSimilarMemories(
|
||||
targetMemory: Memory | ShortMemory,
|
||||
candidates: (Memory | ShortMemory)[],
|
||||
topN: number = 5,
|
||||
threshold: number = 0.7 // 默认阈值
|
||||
): Promise<{ memory: Memory | ShortMemory; similarity: number }[]> {
|
||||
const targetMemoryWithVector = await this.ensureVectorRepresentation(targetMemory)
|
||||
|
||||
if (!targetMemoryWithVector.vectorRepresentation || targetMemoryWithVector.vectorRepresentation.length === 0) {
|
||||
console.warn('[VectorService] Target memory has no vector representation. Cannot find similar memories.')
|
||||
return []
|
||||
}
|
||||
|
||||
const results: { memory: Memory | ShortMemory; similarity: number }[] = []
|
||||
|
||||
for (const candidate of candidates) {
|
||||
// 排除目标记忆自身
|
||||
if (candidate.id === targetMemory.id) {
|
||||
continue
|
||||
}
|
||||
|
||||
const similarity = await this.calculateSimilarity(targetMemoryWithVector, candidate)
|
||||
if (similarity >= threshold) {
|
||||
results.push({ memory: candidate, similarity })
|
||||
}
|
||||
}
|
||||
|
||||
// 按相似度降序排序
|
||||
results.sort((a, b) => b.similarity - a.similarity)
|
||||
|
||||
// 返回前 N 个结果
|
||||
return results.slice(0, topN)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算查询文本与一组记忆项的相似度。
|
||||
* @param queryText - 查询文本
|
||||
* @param candidates - 候选记忆项列表
|
||||
* @param topN - 返回最相似的 N 个结果
|
||||
* @param threshold - 相似度阈值 (0 到 1)
|
||||
* @returns 最相似的记忆项列表及其相似度分数
|
||||
*/
|
||||
async findSimilarMemoriesToQuery(
|
||||
queryText: string,
|
||||
candidates: (Memory | ShortMemory)[],
|
||||
topN: number = 10,
|
||||
threshold: number = 0.7
|
||||
): Promise<{ memory: Memory | ShortMemory; similarity: number }[]> {
|
||||
const queryVector = await this.getVector(queryText)
|
||||
if (!queryVector) {
|
||||
console.warn('[VectorService] Could not get vector for query text. Cannot find similar memories.')
|
||||
return []
|
||||
}
|
||||
|
||||
const results: { memory: Memory | ShortMemory; similarity: number }[] = []
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const candidateWithVector = await this.ensureVectorRepresentation(candidate)
|
||||
if (candidateWithVector.vectorRepresentation && candidateWithVector.vectorRepresentation.length > 0) {
|
||||
const similarity = cosineSimilarity(queryVector, candidateWithVector.vectorRepresentation)
|
||||
const normalizedSimilarity = (similarity + 1) / 2 // 归一化到 0-1
|
||||
if (normalizedSimilarity >= threshold) {
|
||||
results.push({ memory: candidate, similarity: normalizedSimilarity })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.sort((a, b) => b.similarity - a.similarity)
|
||||
return results.slice(0, topN)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空向量缓存
|
||||
*/
|
||||
clearCache(): void {
|
||||
vectorCache.clear()
|
||||
console.log('[VectorService] Vector cache cleared.')
|
||||
}
|
||||
}
|
||||
|
||||
// 导出 VectorService 的单例
|
||||
export const vectorService = new VectorService()
|
||||
@@ -10,6 +10,7 @@ import copilot from './copilot'
|
||||
import knowledge from './knowledge'
|
||||
import llm from './llm'
|
||||
import mcp from './mcp'
|
||||
import memory from './memory' // Removed import of memoryPersistenceMiddleware
|
||||
import messagesReducer from './messages'
|
||||
import migrate from './migrate'
|
||||
import minapps from './minapps'
|
||||
@@ -35,6 +36,7 @@ const rootReducer = combineReducers({
|
||||
websearch,
|
||||
mcp,
|
||||
copilot,
|
||||
memory,
|
||||
messages: messagesReducer
|
||||
})
|
||||
|
||||
@@ -42,8 +44,8 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 95,
|
||||
blacklist: ['runtime', 'messages'],
|
||||
version: 96,
|
||||
blacklist: ['runtime', 'messages', 'memory'],
|
||||
migrate
|
||||
},
|
||||
rootReducer
|
||||
@@ -57,7 +59,7 @@ const store = configureStore({
|
||||
serializableCheck: {
|
||||
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
|
||||
}
|
||||
})
|
||||
}) // Removed concat of memoryPersistenceMiddleware
|
||||
},
|
||||
devTools: true
|
||||
})
|
||||
|
||||
@@ -97,6 +97,14 @@ export const builtinMCPServers: MCPServer[] = [
|
||||
type: 'inMemory',
|
||||
description: '实现文件系统操作的模型上下文协议(MCP)的 Node.js 服务器',
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
name: '@cherry/simpleremember',
|
||||
type: 'inMemory',
|
||||
description:
|
||||
'自动记忆工具,功能跟上面的记忆工具差不多。这个记忆会自动应用到对话中,无需显式调用。适合记住用户偏好、项目背景等长期有用信息.可以跨对话。',
|
||||
isActive: true
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
1218
src/renderer/src/store/memory.ts
Normal file
1218
src/renderer/src/store/memory.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface SettingsState {
|
||||
clickAssistantToShowTopic: boolean
|
||||
autoCheckUpdate: boolean
|
||||
renderInputMessageAsMarkdown: boolean
|
||||
enableHistoricalContext: boolean // 是否启用历史对话上下文功能
|
||||
codeShowLineNumbers: boolean
|
||||
codeCollapsible: boolean
|
||||
codeWrappable: boolean
|
||||
@@ -153,6 +154,7 @@ export const initialState: SettingsState = {
|
||||
clickAssistantToShowTopic: true,
|
||||
autoCheckUpdate: true,
|
||||
renderInputMessageAsMarkdown: false,
|
||||
enableHistoricalContext: false, // 默认禁用历史对话上下文功能
|
||||
codeShowLineNumbers: false,
|
||||
codeCollapsible: false,
|
||||
codeWrappable: false,
|
||||
@@ -306,6 +308,10 @@ const settingsSlice = createSlice({
|
||||
setRenderInputMessageAsMarkdown: (state, action: PayloadAction<boolean>) => {
|
||||
state.renderInputMessageAsMarkdown = action.payload
|
||||
},
|
||||
|
||||
setEnableHistoricalContext: (state, action: PayloadAction<boolean>) => {
|
||||
state.enableHistoricalContext = action.payload
|
||||
},
|
||||
setClickAssistantToShowTopic: (state, action: PayloadAction<boolean>) => {
|
||||
state.clickAssistantToShowTopic = action.payload
|
||||
},
|
||||
@@ -512,6 +518,7 @@ export const {
|
||||
setPasteLongTextAsFile,
|
||||
setAutoCheckUpdate,
|
||||
setRenderInputMessageAsMarkdown,
|
||||
setEnableHistoricalContext,
|
||||
setClickAssistantToShowTopic,
|
||||
setWebdavHost,
|
||||
setWebdavUser,
|
||||
|
||||
@@ -71,6 +71,13 @@ export type Message = {
|
||||
useful?: boolean
|
||||
error?: Record<string, any>
|
||||
enabledMCPs?: MCPServer[]
|
||||
// 引用消息
|
||||
referencedMessages?: {
|
||||
id: string
|
||||
content: string
|
||||
role: 'user' | 'assistant'
|
||||
createdAt: string
|
||||
}[]
|
||||
metadata?: {
|
||||
// Gemini
|
||||
groundingMetadata?: GroundingMetadata
|
||||
|
||||
@@ -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,12 +147,55 @@ ${availableTools}
|
||||
</tools>`
|
||||
}
|
||||
|
||||
export const buildSystemPrompt = (userSystemPrompt: string, tools: MCPTool[]): string => {
|
||||
if (tools && tools.length > 0) {
|
||||
return SYSTEM_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', userSystemPrompt)
|
||||
.replace('{{ TOOL_USE_EXAMPLES }}', ToolUseExamples)
|
||||
.replace('{{ AVAILABLE_TOOLS }}', AvailableTools(tools))
|
||||
import { applyMemoriesToPrompt } from '@renderer/services/MemoryService'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
|
||||
import { getRememberedMemories } from './remember-utils'
|
||||
export const buildSystemPrompt = async (
|
||||
userSystemPrompt: string,
|
||||
tools: MCPTool[],
|
||||
mcpServers: MCPServer[] = []
|
||||
): Promise<string> => {
|
||||
// 获取MCP记忆
|
||||
let mcpMemoriesPrompt = ''
|
||||
try {
|
||||
mcpMemoriesPrompt = await getRememberedMemories(mcpServers)
|
||||
} catch (error) {
|
||||
console.error('Error getting MCP memories:', error)
|
||||
}
|
||||
|
||||
return userSystemPrompt
|
||||
}
|
||||
// 获取内置记忆
|
||||
let appMemoriesPrompt = ''
|
||||
try {
|
||||
// 应用内置记忆功能
|
||||
console.log('[Prompt] Applying app memories to prompt')
|
||||
// 直接将用户系统提示词传递给 applyMemoriesToPrompt,让它添加记忆
|
||||
appMemoriesPrompt = await applyMemoriesToPrompt(userSystemPrompt)
|
||||
console.log('[Prompt] App memories prompt length:', appMemoriesPrompt.length - userSystemPrompt.length)
|
||||
} catch (error) {
|
||||
console.error('Error applying app memories:', error)
|
||||
// 如果应用 Redux 记忆失败,至少保留原始用户提示
|
||||
appMemoriesPrompt = userSystemPrompt
|
||||
}
|
||||
|
||||
// 添加记忆工具的使用说明
|
||||
// 合并所有提示词
|
||||
// 注意:appMemoriesPrompt 已经包含 userSystemPrompt,所以不需要再次添加
|
||||
// 合并 app 记忆(已包含 user prompt)和 mcp 记忆
|
||||
const enhancedPrompt = appMemoriesPrompt + (mcpMemoriesPrompt ? `\n\n${mcpMemoriesPrompt}` : '')
|
||||
|
||||
let finalPrompt: string
|
||||
if (tools && tools.length > 0) {
|
||||
console.log('[Prompt] Final prompt with tools:', { promptLength: enhancedPrompt.length })
|
||||
// Break down the chained replace calls to potentially help the parser
|
||||
const availableToolsString = AvailableTools(tools)
|
||||
let tempPrompt = SYSTEM_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', enhancedPrompt)
|
||||
tempPrompt = tempPrompt.replace('{{ TOOL_USE_EXAMPLES }}', ToolUseExamples)
|
||||
finalPrompt = tempPrompt.replace('{{ AVAILABLE_TOOLS }}', availableToolsString)
|
||||
} else {
|
||||
console.log('[Prompt] Final prompt without tools:', { promptLength: enhancedPrompt.length })
|
||||
finalPrompt = enhancedPrompt // Assign enhancedPrompt when no tools are present
|
||||
}
|
||||
// Single return point for the function
|
||||
return finalPrompt
|
||||
} // Closing brace for the buildSystemPrompt function moved here
|
||||
|
||||
68
src/renderer/src/utils/remember-utils.ts
Normal file
68
src/renderer/src/utils/remember-utils.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// src/renderer/src/utils/remember-utils.ts
|
||||
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)
|
||||
|
||||
if (!rememberServer) {
|
||||
console.log('[SimpleRemember] Server not found or not active')
|
||||
return ''
|
||||
}
|
||||
|
||||
console.log('[SimpleRemember] Found server:', rememberServer.name, 'isActive:', rememberServer.isActive)
|
||||
|
||||
// 调用get_memories工具
|
||||
try {
|
||||
console.log('[SimpleRemember] Calling get_memories tool...')
|
||||
const response = await window.api.mcp.callTool({
|
||||
server: rememberServer,
|
||||
name: 'get_memories',
|
||||
args: {}
|
||||
})
|
||||
|
||||
console.log('[SimpleRemember] get_memories response:', response)
|
||||
|
||||
if (response.isError) {
|
||||
console.error('[SimpleRemember] Error getting memories:', response)
|
||||
return ''
|
||||
}
|
||||
|
||||
// 解析记忆
|
||||
// 根据MCP规范,工具返回的是content数组,而不是data
|
||||
let memories = []
|
||||
if (response.content && response.content.length > 0 && response.content[0].text) {
|
||||
try {
|
||||
memories = JSON.parse(response.content[0].text)
|
||||
} catch (parseError) {
|
||||
console.error('[SimpleRemember] Failed to parse memories JSON:', parseError)
|
||||
return ''
|
||||
}
|
||||
} else if (response.data) {
|
||||
// 兼容旧版本的返回格式
|
||||
memories = response.data
|
||||
}
|
||||
|
||||
console.log('[SimpleRemember] Parsed memories:', memories)
|
||||
|
||||
if (!Array.isArray(memories) || memories.length === 0) {
|
||||
console.log('[SimpleRemember] No memories found or invalid format')
|
||||
return ''
|
||||
}
|
||||
|
||||
// 构建记忆提示词
|
||||
// Add explicit type for memory item in map function
|
||||
const memoryPrompt = memories.map((memory: { content: string }) => `- ${memory.content}`).join('\n')
|
||||
console.log('[SimpleRemember] Generated memory prompt:', memoryPrompt)
|
||||
|
||||
return `\n\n用户的记忆:\n${memoryPrompt}`
|
||||
} catch (toolError) {
|
||||
console.error('[SimpleRemember] Error calling get_memories tool:', toolError)
|
||||
return ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SimpleRemember] Error in getRememberedMemories:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
527
yarn.lock
527
yarn.lock
@@ -3304,6 +3304,38 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-array@npm:*":
|
||||
version: 3.2.1
|
||||
resolution: "@types/d3-array@npm:3.2.1"
|
||||
checksum: 10c0/38bf2c778451f4b79ec81a2288cb4312fe3d6449ecdf562970cc339b60f280f31c93a024c7ff512607795e79d3beb0cbda123bb07010167bce32927f71364bca
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-axis@npm:*":
|
||||
version: 3.0.6
|
||||
resolution: "@types/d3-axis@npm:3.0.6"
|
||||
dependencies:
|
||||
"@types/d3-selection": "npm:*"
|
||||
checksum: 10c0/d756d42360261f44d8eefd0950c5bb0a4f67a46dd92069da3f723ac36a1e8cb2b9ce6347d836ef19d5b8aef725dbcf8fdbbd6cfbff676ca4b0642df2f78b599a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-brush@npm:*":
|
||||
version: 3.0.6
|
||||
resolution: "@types/d3-brush@npm:3.0.6"
|
||||
dependencies:
|
||||
"@types/d3-selection": "npm:*"
|
||||
checksum: 10c0/fd6e2ac7657a354f269f6b9c58451ffae9d01b89ccb1eb6367fd36d635d2f1990967215ab498e0c0679ff269429c57fad6a2958b68f4d45bc9f81d81672edc01
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-chord@npm:*":
|
||||
version: 3.0.6
|
||||
resolution: "@types/d3-chord@npm:3.0.6"
|
||||
checksum: 10c0/c5a25eb5389db01e63faec0c5c2ec7cc41c494e9b3201630b494c4e862a60f1aa83fabbc33a829e7e1403941e3c30d206c741559b14406ac2a4239cfdf4b4c17
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-color@npm:*":
|
||||
version: 3.1.3
|
||||
resolution: "@types/d3-color@npm:3.1.3"
|
||||
@@ -3311,7 +3343,31 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-drag@npm:^3.0.7":
|
||||
"@types/d3-contour@npm:*":
|
||||
version: 3.0.6
|
||||
resolution: "@types/d3-contour@npm:3.0.6"
|
||||
dependencies:
|
||||
"@types/d3-array": "npm:*"
|
||||
"@types/geojson": "npm:*"
|
||||
checksum: 10c0/e7d83e94719af4576ceb5ac7f277c5806f83ba6c3631744ae391cffc3641f09dfa279470b83053cd0b2acd6784e8749c71141d05bdffa63ca58ffb5b31a0f27c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-delaunay@npm:*":
|
||||
version: 6.0.4
|
||||
resolution: "@types/d3-delaunay@npm:6.0.4"
|
||||
checksum: 10c0/d154a8864f08c4ea23ecb9bdabcef1c406a25baa8895f0cb08a0ed2799de0d360e597552532ce7086ff0cdffa8f3563f9109d18f0191459d32bb620a36939123
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-dispatch@npm:*":
|
||||
version: 3.0.6
|
||||
resolution: "@types/d3-dispatch@npm:3.0.6"
|
||||
checksum: 10c0/405eb7d0ec139fbf72fa6a43b0f3ca8a1f913bb2cb38f607827e63fca8d4393f021f32f3b96b33c93ddbd37789453a0b3624f14f504add5308fd9aec8a46dda0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-drag@npm:*, @types/d3-drag@npm:^3.0.7":
|
||||
version: 3.0.7
|
||||
resolution: "@types/d3-drag@npm:3.0.7"
|
||||
dependencies:
|
||||
@@ -3320,6 +3376,59 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-dsv@npm:*":
|
||||
version: 3.0.7
|
||||
resolution: "@types/d3-dsv@npm:3.0.7"
|
||||
checksum: 10c0/c0f01da862465594c8a28278b51c850af3b4239cc22b14fd1a19d7a98f93d94efa477bf59d8071beb285dca45bf614630811451e18e7c52add3a0abfee0a1871
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-ease@npm:*":
|
||||
version: 3.0.2
|
||||
resolution: "@types/d3-ease@npm:3.0.2"
|
||||
checksum: 10c0/aff5a1e572a937ee9bff6465225d7ba27d5e0c976bd9eacdac2e6f10700a7cb0c9ea2597aff6b43a6ed850a3210030870238894a77ec73e309b4a9d0333f099c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-fetch@npm:*":
|
||||
version: 3.0.7
|
||||
resolution: "@types/d3-fetch@npm:3.0.7"
|
||||
dependencies:
|
||||
"@types/d3-dsv": "npm:*"
|
||||
checksum: 10c0/3d147efa52a26da1a5d40d4d73e6cebaaa964463c378068062999b93ea3731b27cc429104c21ecbba98c6090e58ef13429db6399238c5e3500162fb3015697a0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-force@npm:*":
|
||||
version: 3.0.10
|
||||
resolution: "@types/d3-force@npm:3.0.10"
|
||||
checksum: 10c0/c82b459079a106b50e346c9b79b141f599f2fc4f598985a5211e72c7a2e20d35bd5dc6e91f306b323c8bfa325c02c629b1645f5243f1c6a55bd51bc85cccfa92
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-format@npm:*":
|
||||
version: 3.0.4
|
||||
resolution: "@types/d3-format@npm:3.0.4"
|
||||
checksum: 10c0/3ac1600bf9061a59a228998f7cd3f29e85cbf522997671ba18d4d84d10a2a1aff4f95aceb143fa9960501c3ec351e113fc75884e6a504ace44dc1744083035ee
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-geo@npm:*":
|
||||
version: 3.1.0
|
||||
resolution: "@types/d3-geo@npm:3.1.0"
|
||||
dependencies:
|
||||
"@types/geojson": "npm:*"
|
||||
checksum: 10c0/3745a93439038bb5b0b38facf435f7079812921d46406f5d38deaee59e90084ff742443c7ea0a8446df81a0d81eaf622fe7068cf4117a544bd4aa3b2dc182f88
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-hierarchy@npm:*":
|
||||
version: 3.1.7
|
||||
resolution: "@types/d3-hierarchy@npm:3.1.7"
|
||||
checksum: 10c0/873711737d6b8e7b6f1dda0bcd21294a48f75024909ae510c5d2c21fad2e72032e0958def4d9f68319d3aaac298ad09c49807f8bfc87a145a82693b5208613c7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-interpolate@npm:*":
|
||||
version: 3.0.4
|
||||
resolution: "@types/d3-interpolate@npm:3.0.4"
|
||||
@@ -3329,6 +3438,50 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-path@npm:*":
|
||||
version: 3.1.1
|
||||
resolution: "@types/d3-path@npm:3.1.1"
|
||||
checksum: 10c0/2c36eb31ebaf2ce4712e793fd88087117976f7c4ed69cc2431825f999c8c77cca5cea286f3326432b770739ac6ccd5d04d851eb65e7a4dbcc10c982b49ad2c02
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-polygon@npm:*":
|
||||
version: 3.0.2
|
||||
resolution: "@types/d3-polygon@npm:3.0.2"
|
||||
checksum: 10c0/f46307bb32b6c2aef8c7624500e0f9b518de8f227ccc10170b869dc43e4c542560f6c8d62e9f087fac45e198d6e4b623e579c0422e34c85baf56717456d3f439
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-quadtree@npm:*":
|
||||
version: 3.0.6
|
||||
resolution: "@types/d3-quadtree@npm:3.0.6"
|
||||
checksum: 10c0/7eaa0a4d404adc856971c9285e1c4ab17e9135ea669d847d6db7e0066126a28ac751864e7ce99c65d526e130f56754a2e437a1617877098b3bdcc3ef23a23616
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-random@npm:*":
|
||||
version: 3.0.3
|
||||
resolution: "@types/d3-random@npm:3.0.3"
|
||||
checksum: 10c0/5f4fea40080cd6d4adfee05183d00374e73a10c530276a6455348983dda341003a251def28565a27c25d9cf5296a33e870e397c9d91ff83fb7495a21c96b6882
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-scale-chromatic@npm:*":
|
||||
version: 3.1.0
|
||||
resolution: "@types/d3-scale-chromatic@npm:3.1.0"
|
||||
checksum: 10c0/93c564e02d2e97a048e18fe8054e4a935335da6ab75a56c3df197beaa87e69122eef0dfbeb7794d4a444a00e52e3123514ee27cec084bd21f6425b7037828cc2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-scale@npm:*":
|
||||
version: 4.0.9
|
||||
resolution: "@types/d3-scale@npm:4.0.9"
|
||||
dependencies:
|
||||
"@types/d3-time": "npm:*"
|
||||
checksum: 10c0/4ac44233c05cd50b65b33ecb35d99fdf07566bcdbc55bc1306b2f27d1c5134d8c560d356f2c8e76b096e9125ffb8d26d95f78d56e210d1c542cb255bdf31d6c8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-selection@npm:*, @types/d3-selection@npm:^3.0.10":
|
||||
version: 3.0.11
|
||||
resolution: "@types/d3-selection@npm:3.0.11"
|
||||
@@ -3336,7 +3489,37 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-transition@npm:^3.0.8":
|
||||
"@types/d3-shape@npm:*":
|
||||
version: 3.1.7
|
||||
resolution: "@types/d3-shape@npm:3.1.7"
|
||||
dependencies:
|
||||
"@types/d3-path": "npm:*"
|
||||
checksum: 10c0/38e59771c1c4c83b67aa1f941ce350410522a149d2175832fdc06396b2bb3b2c1a2dd549e0f8230f9f24296ee5641a515eaf10f55ee1ef6c4f83749e2dd7dcfd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-time-format@npm:*":
|
||||
version: 4.0.3
|
||||
resolution: "@types/d3-time-format@npm:4.0.3"
|
||||
checksum: 10c0/9ef5e8e2b96b94799b821eed5d61a3d432c7903247966d8ad951b8ce5797fe46554b425cb7888fa5bf604b4663c369d7628c0328ffe80892156671c58d1a7f90
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-time@npm:*":
|
||||
version: 3.0.4
|
||||
resolution: "@types/d3-time@npm:3.0.4"
|
||||
checksum: 10c0/6d9e2255d63f7a313a543113920c612e957d70da4fb0890931da6c2459010291b8b1f95e149a538500c1c99e7e6c89ffcce5554dd29a31ff134a38ea94b6d174
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-timer@npm:*":
|
||||
version: 3.0.2
|
||||
resolution: "@types/d3-timer@npm:3.0.2"
|
||||
checksum: 10c0/c644dd9571fcc62b1aa12c03bcad40571553020feeb5811f1d8a937ac1e65b8a04b759b4873aef610e28b8714ac71c9885a4d6c127a048d95118f7e5b506d9e1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-transition@npm:*, @types/d3-transition@npm:^3.0.8":
|
||||
version: 3.0.9
|
||||
resolution: "@types/d3-transition@npm:3.0.9"
|
||||
dependencies:
|
||||
@@ -3345,7 +3528,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-zoom@npm:^3.0.8":
|
||||
"@types/d3-zoom@npm:*, @types/d3-zoom@npm:^3.0.8":
|
||||
version: 3.0.8
|
||||
resolution: "@types/d3-zoom@npm:3.0.8"
|
||||
dependencies:
|
||||
@@ -3355,6 +3538,44 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3@npm:^7":
|
||||
version: 7.4.3
|
||||
resolution: "@types/d3@npm:7.4.3"
|
||||
dependencies:
|
||||
"@types/d3-array": "npm:*"
|
||||
"@types/d3-axis": "npm:*"
|
||||
"@types/d3-brush": "npm:*"
|
||||
"@types/d3-chord": "npm:*"
|
||||
"@types/d3-color": "npm:*"
|
||||
"@types/d3-contour": "npm:*"
|
||||
"@types/d3-delaunay": "npm:*"
|
||||
"@types/d3-dispatch": "npm:*"
|
||||
"@types/d3-drag": "npm:*"
|
||||
"@types/d3-dsv": "npm:*"
|
||||
"@types/d3-ease": "npm:*"
|
||||
"@types/d3-fetch": "npm:*"
|
||||
"@types/d3-force": "npm:*"
|
||||
"@types/d3-format": "npm:*"
|
||||
"@types/d3-geo": "npm:*"
|
||||
"@types/d3-hierarchy": "npm:*"
|
||||
"@types/d3-interpolate": "npm:*"
|
||||
"@types/d3-path": "npm:*"
|
||||
"@types/d3-polygon": "npm:*"
|
||||
"@types/d3-quadtree": "npm:*"
|
||||
"@types/d3-random": "npm:*"
|
||||
"@types/d3-scale": "npm:*"
|
||||
"@types/d3-scale-chromatic": "npm:*"
|
||||
"@types/d3-selection": "npm:*"
|
||||
"@types/d3-shape": "npm:*"
|
||||
"@types/d3-time": "npm:*"
|
||||
"@types/d3-time-format": "npm:*"
|
||||
"@types/d3-timer": "npm:*"
|
||||
"@types/d3-transition": "npm:*"
|
||||
"@types/d3-zoom": "npm:*"
|
||||
checksum: 10c0/a9c6d65b13ef3b42c87f2a89ea63a6d5640221869f97d0657b0cb2f1dac96a0f164bf5605643c0794e0de3aa2bf05df198519aaf15d24ca135eb0e8bd8a9d879
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.6":
|
||||
version: 4.1.12
|
||||
resolution: "@types/debug@npm:4.1.12"
|
||||
@@ -3406,6 +3627,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/geojson@npm:*":
|
||||
version: 7946.0.16
|
||||
resolution: "@types/geojson@npm:7946.0.16"
|
||||
checksum: 10c0/1ff24a288bd5860b766b073ead337d31d73bdc715e5b50a2cee5cb0af57a1ed02cc04ef295f5fa68dc40fe3e4f104dd31282b2b818a5ba3231bc1001ba084e3c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/hast@npm:^3.0.0, @types/hast@npm:^3.0.4":
|
||||
version: 3.0.4
|
||||
resolution: "@types/hast@npm:3.0.4"
|
||||
@@ -3925,6 +4153,7 @@ __metadata:
|
||||
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch"
|
||||
"@tryfabric/martian": "npm:^1.2.4"
|
||||
"@types/adm-zip": "npm:^0"
|
||||
"@types/d3": "npm:^7"
|
||||
"@types/diff": "npm:^7"
|
||||
"@types/fs-extra": "npm:^11"
|
||||
"@types/lodash": "npm:^4.17.5"
|
||||
@@ -3947,6 +4176,7 @@ __metadata:
|
||||
babel-plugin-styled-components: "npm:^2.1.4"
|
||||
browser-image-compression: "npm:^2.0.2"
|
||||
color: "npm:^5.0.0"
|
||||
d3: "npm:^7.9.0"
|
||||
dayjs: "npm:^1.11.11"
|
||||
dexie: "npm:^4.0.8"
|
||||
dexie-react-hooks: "npm:^1.1.7"
|
||||
@@ -5405,6 +5635,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:7":
|
||||
version: 7.2.0
|
||||
resolution: "commander@npm:7.2.0"
|
||||
checksum: 10c0/8d690ff13b0356df7e0ebbe6c59b4712f754f4b724d4f473d3cc5b3fdcf978e3a5dc3078717858a2ceb50b0f84d0660a7f22a96cdc50fb877d0c9bb31593d23a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:9.2.0":
|
||||
version: 9.2.0
|
||||
resolution: "commander@npm:9.2.0"
|
||||
@@ -5688,21 +5925,77 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-color@npm:1 - 3":
|
||||
"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:2.5.0 - 3, d3-array@npm:3, d3-array@npm:^3.2.0":
|
||||
version: 3.2.4
|
||||
resolution: "d3-array@npm:3.2.4"
|
||||
dependencies:
|
||||
internmap: "npm:1 - 2"
|
||||
checksum: 10c0/08b95e91130f98c1375db0e0af718f4371ccacef7d5d257727fe74f79a24383e79aba280b9ffae655483ffbbad4fd1dec4ade0119d88c4749f388641c8bf8c50
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-axis@npm:3":
|
||||
version: 3.0.0
|
||||
resolution: "d3-axis@npm:3.0.0"
|
||||
checksum: 10c0/a271e70ba1966daa5aaf6a7f959ceca3e12997b43297e757c7b945db2e1ead3c6ee226f2abcfa22abbd4e2e28bd2b71a0911794c4e5b911bbba271328a582c78
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-brush@npm:3":
|
||||
version: 3.0.0
|
||||
resolution: "d3-brush@npm:3.0.0"
|
||||
dependencies:
|
||||
d3-dispatch: "npm:1 - 3"
|
||||
d3-drag: "npm:2 - 3"
|
||||
d3-interpolate: "npm:1 - 3"
|
||||
d3-selection: "npm:3"
|
||||
d3-transition: "npm:3"
|
||||
checksum: 10c0/07baf00334c576da2f68a91fc0da5732c3a5fa19bd3d7aed7fd24d1d674a773f71a93e9687c154176f7246946194d77c48c2d8fed757f5dcb1a4740067ec50a8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-chord@npm:3":
|
||||
version: 3.0.1
|
||||
resolution: "d3-chord@npm:3.0.1"
|
||||
dependencies:
|
||||
d3-path: "npm:1 - 3"
|
||||
checksum: 10c0/baa6013914af3f4fe1521f0d16de31a38eb8a71d08ff1dec4741f6f45a828661e5cd3935e39bd14e3032bdc78206c283ca37411da21d46ec3cfc520be6e7a7ce
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-color@npm:1 - 3, d3-color@npm:3":
|
||||
version: 3.1.0
|
||||
resolution: "d3-color@npm:3.1.0"
|
||||
checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-dispatch@npm:1 - 3":
|
||||
"d3-contour@npm:4":
|
||||
version: 4.0.2
|
||||
resolution: "d3-contour@npm:4.0.2"
|
||||
dependencies:
|
||||
d3-array: "npm:^3.2.0"
|
||||
checksum: 10c0/98bc5fbed6009e08707434a952076f39f1cd6ed8b9288253cc3e6a3286e4e80c63c62d84954b20e64bf6e4ededcc69add54d3db25e990784a59c04edd3449032
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-delaunay@npm:6":
|
||||
version: 6.0.4
|
||||
resolution: "d3-delaunay@npm:6.0.4"
|
||||
dependencies:
|
||||
delaunator: "npm:5"
|
||||
checksum: 10c0/57c3aecd2525664b07c4c292aa11cf49b2752c0cf3f5257f752999399fe3c592de2d418644d79df1f255471eec8057a9cc0c3062ed7128cb3348c45f69597754
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-dispatch@npm:1 - 3, d3-dispatch@npm:3":
|
||||
version: 3.0.1
|
||||
resolution: "d3-dispatch@npm:3.0.1"
|
||||
checksum: 10c0/6eca77008ce2dc33380e45d4410c67d150941df7ab45b91d116dbe6d0a3092c0f6ac184dd4602c796dc9e790222bad3ff7142025f5fd22694efe088d1d941753
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-drag@npm:2 - 3, d3-drag@npm:^3.0.0":
|
||||
"d3-drag@npm:2 - 3, d3-drag@npm:3, d3-drag@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "d3-drag@npm:3.0.0"
|
||||
dependencies:
|
||||
@@ -5712,14 +6005,78 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-ease@npm:1 - 3":
|
||||
"d3-dsv@npm:1 - 3, d3-dsv@npm:3":
|
||||
version: 3.0.1
|
||||
resolution: "d3-dsv@npm:3.0.1"
|
||||
dependencies:
|
||||
commander: "npm:7"
|
||||
iconv-lite: "npm:0.6"
|
||||
rw: "npm:1"
|
||||
bin:
|
||||
csv2json: bin/dsv2json.js
|
||||
csv2tsv: bin/dsv2dsv.js
|
||||
dsv2dsv: bin/dsv2dsv.js
|
||||
dsv2json: bin/dsv2json.js
|
||||
json2csv: bin/json2dsv.js
|
||||
json2dsv: bin/json2dsv.js
|
||||
json2tsv: bin/json2dsv.js
|
||||
tsv2csv: bin/dsv2dsv.js
|
||||
tsv2json: bin/dsv2json.js
|
||||
checksum: 10c0/10e6af9e331950ed258f34ab49ac1b7060128ef81dcf32afc790bd1f7e8c3cc2aac7f5f875250a83f21f39bb5925fbd0872bb209f8aca32b3b77d32bab8a65ab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-ease@npm:1 - 3, d3-ease@npm:3":
|
||||
version: 3.0.1
|
||||
resolution: "d3-ease@npm:3.0.1"
|
||||
checksum: 10c0/fec8ef826c0cc35cda3092c6841e07672868b1839fcaf556e19266a3a37e6bc7977d8298c0fcb9885e7799bfdcef7db1baaba9cd4dcf4bc5e952cf78574a88b0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-interpolate@npm:1 - 3":
|
||||
"d3-fetch@npm:3":
|
||||
version: 3.0.1
|
||||
resolution: "d3-fetch@npm:3.0.1"
|
||||
dependencies:
|
||||
d3-dsv: "npm:1 - 3"
|
||||
checksum: 10c0/4f467a79bf290395ac0cbb5f7562483f6a18668adc4c8eb84c9d3eff048b6f6d3b6f55079ba1ebf1908dabe000c941d46be447f8d78453b2dad5fb59fb6aa93b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-force@npm:3":
|
||||
version: 3.0.0
|
||||
resolution: "d3-force@npm:3.0.0"
|
||||
dependencies:
|
||||
d3-dispatch: "npm:1 - 3"
|
||||
d3-quadtree: "npm:1 - 3"
|
||||
d3-timer: "npm:1 - 3"
|
||||
checksum: 10c0/220a16a1a1ac62ba56df61028896e4b52be89c81040d20229c876efc8852191482c233f8a52bb5a4e0875c321b8e5cb6413ef3dfa4d8fe79eeb7d52c587f52cf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-format@npm:1 - 3, d3-format@npm:3":
|
||||
version: 3.1.0
|
||||
resolution: "d3-format@npm:3.1.0"
|
||||
checksum: 10c0/049f5c0871ebce9859fc5e2f07f336b3c5bfff52a2540e0bac7e703fce567cd9346f4ad1079dd18d6f1e0eaa0599941c1810898926f10ac21a31fd0a34b4aa75
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-geo@npm:3":
|
||||
version: 3.1.1
|
||||
resolution: "d3-geo@npm:3.1.1"
|
||||
dependencies:
|
||||
d3-array: "npm:2.5.0 - 3"
|
||||
checksum: 10c0/d32270dd2dc8ac3ea63e8805d63239c4c8ec6c0d339d73b5e5a30a87f8f54db22a78fb434369799465eae169503b25f9a107c642c8a16c32a3285bc0e6d8e8c1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-hierarchy@npm:3":
|
||||
version: 3.1.2
|
||||
resolution: "d3-hierarchy@npm:3.1.2"
|
||||
checksum: 10c0/6dcdb480539644aa7fc0d72dfc7b03f99dfbcdf02714044e8c708577e0d5981deb9d3e99bbbb2d26422b55bcc342ac89a0fa2ea6c9d7302e2fc0951dd96f89cf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-interpolate@npm:1 - 3, d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:3":
|
||||
version: 3.0.1
|
||||
resolution: "d3-interpolate@npm:3.0.1"
|
||||
dependencies:
|
||||
@@ -5728,6 +6085,57 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-path@npm:1 - 3, d3-path@npm:3, d3-path@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "d3-path@npm:3.1.0"
|
||||
checksum: 10c0/dc1d58ec87fa8319bd240cf7689995111a124b141428354e9637aa83059eb12e681f77187e0ada5dedfce346f7e3d1f903467ceb41b379bfd01cd8e31721f5da
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-polygon@npm:3":
|
||||
version: 3.0.1
|
||||
resolution: "d3-polygon@npm:3.0.1"
|
||||
checksum: 10c0/e236aa7f33efa9a4072907af7dc119f85b150a0716759d4fe5f12f62573018264a6cbde8617fbfa6944a7ae48c1c0c8d3f39ae72e11f66dd471e9b5e668385df
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-quadtree@npm:1 - 3, d3-quadtree@npm:3":
|
||||
version: 3.0.1
|
||||
resolution: "d3-quadtree@npm:3.0.1"
|
||||
checksum: 10c0/18302d2548bfecaef788152397edec95a76400fd97d9d7f42a089ceb68d910f685c96579d74e3712d57477ed042b056881b47cd836a521de683c66f47ce89090
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-random@npm:3":
|
||||
version: 3.0.1
|
||||
resolution: "d3-random@npm:3.0.1"
|
||||
checksum: 10c0/987a1a1bcbf26e6cf01fd89d5a265b463b2cea93560fc17d9b1c45e8ed6ff2db5924601bcceb808de24c94133f000039eb7fa1c469a7a844ccbf1170cbb25b41
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-scale-chromatic@npm:3":
|
||||
version: 3.1.0
|
||||
resolution: "d3-scale-chromatic@npm:3.1.0"
|
||||
dependencies:
|
||||
d3-color: "npm:1 - 3"
|
||||
d3-interpolate: "npm:1 - 3"
|
||||
checksum: 10c0/9a3f4671ab0b971f4a411b42180d7cf92bfe8e8584e637ce7e698d705e18d6d38efbd20ec64f60cc0dfe966c20d40fc172565bc28aaa2990c0a006360eed91af
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-scale@npm:4":
|
||||
version: 4.0.2
|
||||
resolution: "d3-scale@npm:4.0.2"
|
||||
dependencies:
|
||||
d3-array: "npm:2.10.0 - 3"
|
||||
d3-format: "npm:1 - 3"
|
||||
d3-interpolate: "npm:1.2.0 - 3"
|
||||
d3-time: "npm:2.1.1 - 3"
|
||||
d3-time-format: "npm:2 - 4"
|
||||
checksum: 10c0/65d9ad8c2641aec30ed5673a7410feb187a224d6ca8d1a520d68a7d6eac9d04caedbff4713d1e8545be33eb7fec5739983a7ab1d22d4e5ad35368c6729d362f1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "d3-selection@npm:3.0.0"
|
||||
@@ -5735,14 +6143,41 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-timer@npm:1 - 3":
|
||||
"d3-shape@npm:3":
|
||||
version: 3.2.0
|
||||
resolution: "d3-shape@npm:3.2.0"
|
||||
dependencies:
|
||||
d3-path: "npm:^3.1.0"
|
||||
checksum: 10c0/f1c9d1f09926daaf6f6193ae3b4c4b5521e81da7d8902d24b38694517c7f527ce3c9a77a9d3a5722ad1e3ff355860b014557b450023d66a944eabf8cfde37132
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-time-format@npm:2 - 4, d3-time-format@npm:4":
|
||||
version: 4.1.0
|
||||
resolution: "d3-time-format@npm:4.1.0"
|
||||
dependencies:
|
||||
d3-time: "npm:1 - 3"
|
||||
checksum: 10c0/735e00fb25a7fd5d418fac350018713ae394eefddb0d745fab12bbff0517f9cdb5f807c7bbe87bb6eeb06249662f8ea84fec075f7d0cd68609735b2ceb29d206
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3, d3-time@npm:3":
|
||||
version: 3.1.0
|
||||
resolution: "d3-time@npm:3.1.0"
|
||||
dependencies:
|
||||
d3-array: "npm:2 - 3"
|
||||
checksum: 10c0/a984f77e1aaeaa182679b46fbf57eceb6ebdb5f67d7578d6f68ef933f8eeb63737c0949991618a8d29472dbf43736c7d7f17c452b2770f8c1271191cba724ca1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-timer@npm:1 - 3, d3-timer@npm:3":
|
||||
version: 3.0.1
|
||||
resolution: "d3-timer@npm:3.0.1"
|
||||
checksum: 10c0/d4c63cb4bb5461d7038aac561b097cd1c5673969b27cbdd0e87fa48d9300a538b9e6f39b4a7f0e3592ef4f963d858c8a9f0e92754db73116770856f2fc04561a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-transition@npm:2 - 3":
|
||||
"d3-transition@npm:2 - 3, d3-transition@npm:3":
|
||||
version: 3.0.1
|
||||
resolution: "d3-transition@npm:3.0.1"
|
||||
dependencies:
|
||||
@@ -5757,7 +6192,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-zoom@npm:^3.0.0":
|
||||
"d3-zoom@npm:3, d3-zoom@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "d3-zoom@npm:3.0.0"
|
||||
dependencies:
|
||||
@@ -5770,6 +6205,44 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3@npm:^7.9.0":
|
||||
version: 7.9.0
|
||||
resolution: "d3@npm:7.9.0"
|
||||
dependencies:
|
||||
d3-array: "npm:3"
|
||||
d3-axis: "npm:3"
|
||||
d3-brush: "npm:3"
|
||||
d3-chord: "npm:3"
|
||||
d3-color: "npm:3"
|
||||
d3-contour: "npm:4"
|
||||
d3-delaunay: "npm:6"
|
||||
d3-dispatch: "npm:3"
|
||||
d3-drag: "npm:3"
|
||||
d3-dsv: "npm:3"
|
||||
d3-ease: "npm:3"
|
||||
d3-fetch: "npm:3"
|
||||
d3-force: "npm:3"
|
||||
d3-format: "npm:3"
|
||||
d3-geo: "npm:3"
|
||||
d3-hierarchy: "npm:3"
|
||||
d3-interpolate: "npm:3"
|
||||
d3-path: "npm:3"
|
||||
d3-polygon: "npm:3"
|
||||
d3-quadtree: "npm:3"
|
||||
d3-random: "npm:3"
|
||||
d3-scale: "npm:4"
|
||||
d3-scale-chromatic: "npm:3"
|
||||
d3-selection: "npm:3"
|
||||
d3-shape: "npm:3"
|
||||
d3-time: "npm:3"
|
||||
d3-time-format: "npm:4"
|
||||
d3-timer: "npm:3"
|
||||
d3-transition: "npm:3"
|
||||
d3-zoom: "npm:3"
|
||||
checksum: 10c0/3dd9c08c73cfaa69c70c49e603c85e049c3904664d9c79a1a52a0f52795828a1ff23592dc9a7b2257e711d68a615472a13103c212032f38e016d609796e087e8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dashdash@npm:^1.12.0":
|
||||
version: 1.14.1
|
||||
resolution: "dashdash@npm:1.14.1"
|
||||
@@ -6052,6 +6525,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"delaunator@npm:5":
|
||||
version: 5.0.1
|
||||
resolution: "delaunator@npm:5.0.1"
|
||||
dependencies:
|
||||
robust-predicates: "npm:^3.0.2"
|
||||
checksum: 10c0/3d7ea4d964731c5849af33fec0a271bc6753487b331fd7d43ccb17d77834706e1c383e6ab8fda0032da955e7576d1083b9603cdaf9cbdfd6b3ebd1fb8bb675a5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"delay@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "delay@npm:6.0.0"
|
||||
@@ -8961,7 +9443,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2":
|
||||
"iconv-lite@npm:0.6, iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2":
|
||||
version: 0.6.3
|
||||
resolution: "iconv-lite@npm:0.6.3"
|
||||
dependencies:
|
||||
@@ -9103,6 +9585,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"internmap@npm:1 - 2":
|
||||
version: 2.0.3
|
||||
resolution: "internmap@npm:2.0.3"
|
||||
checksum: 10c0/8cedd57f07bbc22501516fbfc70447f0c6812871d471096fad9ea603516eacc2137b633633daf432c029712df0baefd793686388ddf5737e3ea15074b877f7ed
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"invert-kv@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "invert-kv@npm:1.0.0"
|
||||
@@ -14426,6 +14915,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"robust-predicates@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "robust-predicates@npm:3.0.2"
|
||||
checksum: 10c0/4ecd53649f1c2d49529c85518f2fa69ffb2f7a4453f7fd19c042421c7b4d76c3efb48bc1c740c8f7049346d7cb58cf08ee0c9adaae595cc23564d360adb1fde4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rollup-plugin-visualizer@npm:^5.12.0":
|
||||
version: 5.14.0
|
||||
resolution: "rollup-plugin-visualizer@npm:5.14.0"
|
||||
@@ -14547,6 +15043,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rw@npm:1":
|
||||
version: 1.3.3
|
||||
resolution: "rw@npm:1.3.3"
|
||||
checksum: 10c0/b1e1ef37d1e79d9dc7050787866e30b6ddcb2625149276045c262c6b4d53075ddc35f387a856a8e76f0d0df59f4cd58fe24707e40797ebee66e542b840ed6a53
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0":
|
||||
version: 5.2.1
|
||||
resolution: "safe-buffer@npm:5.2.1"
|
||||
|
||||
280
敏感信息过滤功能实现方案.txt
Normal file
280
敏感信息过滤功能实现方案.txt
Normal file
@@ -0,0 +1,280 @@
|
||||
# 敏感信息过滤功能实现方案(修改版)
|
||||
|
||||
## 需求分析
|
||||
|
||||
用户希望增加一个按钮,控制记忆功能是否过滤密钥等安全敏感信息。当开启过滤功能时,分析模型会过滤掉密钥等敏感信息;关闭则不过滤。此功能对于保护用户隐私和敏感数据至关重要。
|
||||
|
||||
## 实现思路
|
||||
|
||||
1. 在Redux状态中添加一个新的状态属性`filterSensitiveInfo`
|
||||
2. 在设置界面中添加一个开关按钮,默认为开启状态
|
||||
3. 修改分析函数,根据`filterSensitiveInfo`状态添加过滤指令
|
||||
4. 添加日志记录,跟踪过滤状态的变化
|
||||
|
||||
## 修改文件
|
||||
|
||||
### 1. 修改 src/renderer/src/store/memory.ts
|
||||
|
||||
```typescript
|
||||
// 在 MemoryState 接口中添加
|
||||
export interface MemoryState {
|
||||
// 其他属性...
|
||||
filterSensitiveInfo: boolean // 是否过滤敏感信息
|
||||
}
|
||||
|
||||
// 在 initialState 中添加
|
||||
const initialState: MemoryState = {
|
||||
// 其他属性...
|
||||
filterSensitiveInfo: true, // 默认启用敏感信息过滤
|
||||
}
|
||||
|
||||
// 添加新的 action creator
|
||||
setFilterSensitiveInfo: (state, action: PayloadAction<boolean>) => {
|
||||
state.filterSensitiveInfo = action.payload
|
||||
},
|
||||
|
||||
// 导出 action
|
||||
export const {
|
||||
// 其他 actions...
|
||||
setFilterSensitiveInfo,
|
||||
} = memorySlice.actions
|
||||
|
||||
// 修改 saveMemoryData 函数,确保 filterSensitiveInfo 设置也被保存
|
||||
const completeData = {
|
||||
// 基本设置
|
||||
isActive: memoryData.isActive !== undefined ? memoryData.isActive : state.isActive,
|
||||
shortMemoryActive: memoryData.shortMemoryActive !== undefined ? memoryData.shortMemoryActive : state.shortMemoryActive,
|
||||
autoAnalyze: memoryData.autoAnalyze !== undefined ? memoryData.autoAnalyze : state.autoAnalyze,
|
||||
filterSensitiveInfo: memoryData.filterSensitiveInfo !== undefined ? memoryData.filterSensitiveInfo : state.filterSensitiveInfo,
|
||||
|
||||
// 其他属性...
|
||||
}
|
||||
|
||||
// 同样修改 saveLongTermMemoryData 函数
|
||||
const completeData = {
|
||||
// 基本设置
|
||||
isActive: memoryData.isActive !== undefined ? memoryData.isActive : state.isActive,
|
||||
autoAnalyze: memoryData.autoAnalyze !== undefined ? memoryData.autoAnalyze : state.autoAnalyze,
|
||||
filterSensitiveInfo: memoryData.filterSensitiveInfo !== undefined ? memoryData.filterSensitiveInfo : state.filterSensitiveInfo,
|
||||
|
||||
// 其他属性...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 修改 src/renderer/src/pages/settings/MemorySettings/index.tsx
|
||||
|
||||
```typescript
|
||||
// 导入 InfoCircleOutlined 图标
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
InfoCircleOutlined,
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
UnorderedListOutlined
|
||||
} from '@ant-design/icons'
|
||||
|
||||
// 导入 setFilterSensitiveInfo action
|
||||
import {
|
||||
addMemory,
|
||||
clearMemories,
|
||||
deleteMemory,
|
||||
editMemory,
|
||||
setAnalyzeModel,
|
||||
setAnalyzing,
|
||||
setAutoAnalyze,
|
||||
setFilterSensitiveInfo,
|
||||
setMemoryActive,
|
||||
setShortMemoryAnalyzeModel,
|
||||
saveMemoryData,
|
||||
saveLongTermMemoryData,
|
||||
saveAllMemorySettings
|
||||
} from '@renderer/store/memory'
|
||||
|
||||
// 从 Redux 获取 filterSensitiveInfo 状态
|
||||
const filterSensitiveInfo = useAppSelector((state) => state.memory?.filterSensitiveInfo ?? true) // 默认启用敏感信息过滤
|
||||
|
||||
// 添加处理切换敏感信息过滤的函数
|
||||
const handleToggleFilterSensitiveInfo = async (checked: boolean) => {
|
||||
dispatch(setFilterSensitiveInfo(checked))
|
||||
console.log('[Memory Settings] Filter sensitive info set:', checked)
|
||||
|
||||
// 使用Redux Thunk保存到JSON文件
|
||||
try {
|
||||
await dispatch(saveMemoryData({ filterSensitiveInfo: checked })).unwrap()
|
||||
console.log('[Memory Settings] Filter sensitive info saved to file successfully:', checked)
|
||||
} catch (error) {
|
||||
console.error('[Memory Settings] Failed to save filter sensitive info to file:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 在短期记忆设置中添加开关按钮
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.memory.filterSensitiveInfo') || '过滤敏感信息'}
|
||||
<Tooltip title={t('settings.memory.filterSensitiveInfoTip') || '启用后,记忆功能将不会提取API密钥、密码等敏感信息'}>
|
||||
<InfoCircleOutlined style={{ marginLeft: 8 }} />
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<Switch checked={filterSensitiveInfo} onChange={handleToggleFilterSensitiveInfo} disabled={!isActive} />
|
||||
</SettingRow>
|
||||
|
||||
// 在长期记忆设置中也添加相同的开关按钮
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.memory.filterSensitiveInfo') || '过滤敏感信息'}
|
||||
<Tooltip title={t('settings.memory.filterSensitiveInfoTip') || '启用后,记忆功能将不会提取API密钥、密码等敏感信息'}>
|
||||
<InfoCircleOutlined style={{ marginLeft: 8 }} />
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<Switch checked={filterSensitiveInfo} onChange={handleToggleFilterSensitiveInfo} disabled={!isActive} />
|
||||
</SettingRow>
|
||||
```
|
||||
|
||||
### 3. 修改 src/renderer/src/services/MemoryService.ts
|
||||
|
||||
```typescript
|
||||
// 修改 analyzeConversation 函数
|
||||
const analyzeConversation = async (
|
||||
conversation: string,
|
||||
modelId: string,
|
||||
customPrompt?: string
|
||||
): Promise<Array<{ content: string; category: string }>> => {
|
||||
try {
|
||||
// 获取当前的过滤敏感信息设置
|
||||
const filterSensitiveInfo = store.getState().memory?.filterSensitiveInfo ?? true
|
||||
|
||||
// 使用自定义提示词或默认提示词
|
||||
let basePrompt =
|
||||
customPrompt ||
|
||||
`
|
||||
请分析对话内容,提取出重要的用户偏好、习惯、需求和背景信息,这些信息在未来的对话中可能有用。
|
||||
|
||||
将每条信息分类并按以下格式返回:
|
||||
类别: 信息内容
|
||||
|
||||
类别应该是以下几种之一:
|
||||
- 用户偏好:用户喜好、喜欢的事物、风格等
|
||||
- 技术需求:用户的技术相关需求、开发偏好等
|
||||
- 个人信息:用户的背景、经历等个人信息
|
||||
- 交互偏好:用户喜欢的交流方式、沟通风格等
|
||||
- 其他:不属于以上类别的重要信息
|
||||
|
||||
请确保每条信息都是简洁、准确的。如果没有找到重要信息,请返回空字符串。
|
||||
`
|
||||
|
||||
// 如果启用了敏感信息过滤,添加相关指令
|
||||
if (filterSensitiveInfo) {
|
||||
basePrompt += `
|
||||
## 安全提示:
|
||||
请注意不要提取任何敏感信息,包括但不限于:
|
||||
- API密钥、访问令牌或其他凭证
|
||||
- 密码或密码提示
|
||||
- 私人联系方式(如电话号码、邮箱地址)
|
||||
- 个人身份信息(如身份证号、社保号)
|
||||
- 银行账户或支付信息
|
||||
- 私密的个人或商业信息
|
||||
|
||||
如果发现此类信息,请完全忽略,不要以任何形式记录或提取。
|
||||
`
|
||||
}
|
||||
|
||||
// 其余代码保持不变...
|
||||
}
|
||||
}
|
||||
|
||||
// 修改 analyzeAndAddShortMemories 函数
|
||||
export const analyzeAndAddShortMemories = async (topicId: string) => {
|
||||
// 其他代码...
|
||||
|
||||
try {
|
||||
// 获取当前的过滤敏感信息设置
|
||||
const filterSensitiveInfo = store.getState().memory?.filterSensitiveInfo ?? true
|
||||
|
||||
// 构建短期记忆分析提示词
|
||||
let prompt = `
|
||||
请对以下对话内容进行非常详细的分析和总结,提取对当前对话至关重要的上下文信息。请注意,这个分析将用于生成短期记忆,帮助AI理解当前对话的完整上下文。
|
||||
|
||||
分析要求:
|
||||
1. 非常详细地总结用户的每一句话中表达的关键信息、需求和意图
|
||||
2. 全面分析AI回复中的重要内容和对用户问题的解决方案
|
||||
3. 详细记录对话中的重要事实、数据、代码示例和具体细节
|
||||
4. 清晰捕捉对话的逻辑发展、转折点和关键决策
|
||||
5. 提取对理解当前对话上下文必不可少的信息
|
||||
6. 记录用户提出的具体问题和关注点
|
||||
7. 捕捉用户在对话中表达的偏好、困惑和反馈
|
||||
8. 记录对话中提到的文件、路径、变量名等具体技术细节
|
||||
`
|
||||
|
||||
// 如果启用了敏感信息过滤,添加相关指令
|
||||
if (filterSensitiveInfo) {
|
||||
prompt += `
|
||||
9. 请注意不要提取任何敏感信息,包括但不限于:
|
||||
- API密钥、访问令牌或其他凭证
|
||||
- 密码或密码提示
|
||||
- 私人联系方式(如电话号码、邮箱地址)
|
||||
- 个人身份信息(如身份证号、社保号)
|
||||
- 银行账户或支付信息
|
||||
- 私密的个人或商业信息
|
||||
如果发现此类信息,请完全忽略,不要以任何形式记录或提取。
|
||||
`
|
||||
}
|
||||
|
||||
// 其余代码保持不变...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 修改 src/renderer/src/i18n/locales/zh-cn.json 和 en-us.json
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": {
|
||||
"memory": {
|
||||
"filterSensitiveInfo": "过滤敏感信息",
|
||||
"filterSensitiveInfoTip": "启用后,记忆功能将不会提取API密钥、密码等敏感信息"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": {
|
||||
"memory": {
|
||||
"filterSensitiveInfo": "Filter Sensitive Information",
|
||||
"filterSensitiveInfoTip": "When enabled, memory function will not extract API keys, passwords, or other sensitive information"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实现效果
|
||||
|
||||
这些修改后,用户将能够通过开关按钮控制记忆功能是否过滤敏感信息:
|
||||
|
||||
1. 当开启过滤功能时(默认状态),分析模型会被明确指示不要提取API密钥、密码等敏感信息
|
||||
2. 当关闭过滤功能时,分析模型会正常提取所有信息,包括可能的敏感信息
|
||||
|
||||
开关按钮会出现在短期记忆和长期记忆设置中,用户可以根据需要随时切换。设置会被保存到配置文件中,确保应用重启后设置仍然生效。
|
||||
|
||||
## 思考过程
|
||||
|
||||
1. **状态管理**:首先考虑如何在Redux中添加新的状态属性,并确保它能够被正确保存和加载。
|
||||
|
||||
2. **UI设计**:在设置界面中添加开关按钮,并提供提示信息,帮助用户理解这个功能的作用。
|
||||
|
||||
3. **提示词修改**:根据开关状态修改分析提示词,添加不要提取敏感信息的指令。这是实现过滤功能的核心部分。
|
||||
|
||||
4. **国际化支持**:添加相关的翻译键值对,确保功能在不同语言环境下都能正常使用。
|
||||
|
||||
5. **持久化**:确保设置能够被正确保存到配置文件中,并在应用重启后加载。
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 这个功能只能在一定程度上防止敏感信息被提取,但不能完全保证。如果用户在对话中明确提到了敏感信息,AI模型可能仍然会提取部分内容。
|
||||
|
||||
2. 建议用户在讨论敏感信息时,最好暂时关闭记忆功能,或者在对话中避免提及敏感信息。
|
||||
|
||||
3. 这个功能只影响新分析的对话内容,已经存储的记忆不会受到影响。如果用户想要清除可能包含敏感信息的记忆,需要手动删除这些记忆。
|
||||
Reference in New Issue
Block a user