diff --git a/package.json b/package.json index eb3eee3f28..a83d8417f8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/main/index.ts b/src/main/index.ts index 511b5a5220..ddd41fceb0 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,3 +1,5 @@ +import './services/MemoryFileService' + import { electronApp, optimizer } from '@electron-toolkit/utils' import { replaceDevtoolsFont } from '@main/utils/windowUtil' import { IpcChannel } from '@shared/IpcChannel' @@ -7,7 +9,6 @@ import Logger from 'electron-log' import { registerIpc } from './ipc' import { configManager } from './services/ConfigManager' -import './services/MemoryFileService' import mcpService from './services/MCPService' import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient' import { registerShortcuts } from './services/ShortcutService' diff --git a/src/main/ipc.ts b/src/main/ipc.ts index b16cfa3faa..74da530cc4 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -1,3 +1,5 @@ +import './services/MemoryFileService' + import fs from 'node:fs' import { isMac, isWin } from '@main/constant' @@ -18,7 +20,7 @@ import FileStorage from './services/FileStorage' import { GeminiService } from './services/GeminiService' import KnowledgeService from './services/KnowledgeService' import mcpService from './services/MCPService' -import './services/MemoryFileService' +import { memoryFileService } from './services/MemoryFileService' import * as NutstoreService from './services/NutstoreService' import ObsidianVaultService from './services/ObsidianVaultService' import { ProxyConfig, proxyManager } from './services/ProxyManager' @@ -26,7 +28,6 @@ import { searchService } from './services/SearchService' import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService' import { TrayService } from './services/TrayService' import { windowService } from './services/WindowService' -import { memoryFileService } from './services/MemoryFileService' import { getResourcePath } from './utils' import { decrypt, encrypt } from './utils/aes' import { getConfigDir, getFilesDir } from './utils/file' diff --git a/src/main/services/BackupManager.ts b/src/main/services/BackupManager.ts index 01bc299c45..174b73f8cf 100644 --- a/src/main/services/BackupManager.ts +++ b/src/main/services/BackupManager.ts @@ -8,9 +8,9 @@ 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' -import { getConfigDir } from '../utils/file' class BackupManager { private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp') diff --git a/src/main/services/MemoryFileService.ts b/src/main/services/MemoryFileService.ts index 41fd33691b..75d83a1850 100644 --- a/src/main/services/MemoryFileService.ts +++ b/src/main/services/MemoryFileService.ts @@ -1,7 +1,8 @@ +import log from 'electron-log' import { promises as fs } from 'fs' import path from 'path' + import { getConfigDir } from '../utils/file' -import log from 'electron-log' // 定义记忆文件路径 const memoryDataPath = path.join(getConfigDir(), 'memory-data.json') @@ -30,11 +31,13 @@ export class MemoryFileService { // 文件不存在,创建默认文件 log.info('Memory data file does not exist, creating default file') const defaultData = { - memoryLists: [{ - id: 'default', - name: '默认列表', - isActive: true - }], + memoryLists: [ + { + id: 'default', + name: '默认列表', + isActive: true + } + ], shortMemories: [], analyzeModel: 'gpt-3.5-turbo', shortMemoryAnalyzeModel: 'gpt-3.5-turbo', @@ -150,13 +153,15 @@ export class MemoryFileService { 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 - }], + memoryLists: [ + { + id: 'default', + name: '默认列表', + isActive: true, + createdAt: now, + updatedAt: now + } + ], memories: [], currentListId: 'default', analyzeModel: 'gpt-3.5-turbo' diff --git a/src/preload/index.ts b/src/preload/index.ts index 1310c458ee..f766eec989 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -183,7 +183,8 @@ const api = { 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) + saveLongTermData: (data: any, forceOverwrite: boolean = false) => + ipcRenderer.invoke(IpcChannel.LongTermMemory_SaveData, data, forceOverwrite) } } diff --git a/src/renderer/src/assets/styles/markdown.scss b/src/renderer/src/assets/styles/markdown.scss index e24569b0f2..ee1e6da4ea 100644 --- a/src/renderer/src/assets/styles/markdown.scss +++ b/src/renderer/src/assets/styles/markdown.scss @@ -1,6 +1,7 @@ .markdown { color: var(--color-text); line-height: 1.6; + -webkit-user-select: text; user-select: text; word-break: break-word; diff --git a/src/renderer/src/components/MemoryProvider.tsx b/src/renderer/src/components/MemoryProvider.tsx index 34ebaf8d25..54b3e6103d 100644 --- a/src/renderer/src/components/MemoryProvider.tsx +++ b/src/renderer/src/components/MemoryProvider.tsx @@ -3,24 +3,24 @@ import { useAppDispatch, useAppSelector } from '@renderer/store' import store from '@renderer/store' import { clearShortMemories, - loadMemoryData, loadLongTermMemoryData, - setCurrentMemoryList, - setMemoryActive, - setShortMemoryActive, - setAutoAnalyze, + loadMemoryData, setAdaptiveAnalysisEnabled, - setAnalysisFrequency, setAnalysisDepth, + setAnalysisFrequency, + setAutoAnalyze, + setAutoRecommendMemories, + setContextualRecommendationEnabled, + setCurrentMemoryList, + setDecayEnabled, + setDecayRate, + setFreshnessEnabled, setInterestTrackingEnabled, + setMemoryActive, setMonitoringEnabled, setPriorityManagementEnabled, - setDecayEnabled, - setFreshnessEnabled, - setDecayRate, - setContextualRecommendationEnabled, - setAutoRecommendMemories, - setRecommendationThreshold + setRecommendationThreshold, + setShortMemoryActive } from '@renderer/store/memory' import { FC, ReactNode, useEffect, useRef } from 'react' @@ -76,33 +76,38 @@ const MemoryProvider: FC = ({ children }) => { if (data.autoAnalyze !== undefined) dispatch(setAutoAnalyze(data.autoAnalyze)) // 自适应分析相关 - if (data.adaptiveAnalysisEnabled !== undefined) dispatch(setAdaptiveAnalysisEnabled(data.adaptiveAnalysisEnabled)) + 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.interestTrackingEnabled !== undefined) + dispatch(setInterestTrackingEnabled(data.interestTrackingEnabled)) // 性能监控相关 if (data.monitoringEnabled !== undefined) dispatch(setMonitoringEnabled(data.monitoringEnabled)) // 智能优先级与时效性管理相关 - if (data.priorityManagementEnabled !== undefined) dispatch(setPriorityManagementEnabled(data.priorityManagementEnabled)) + 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.contextualRecommendationEnabled !== undefined) + dispatch(setContextualRecommendationEnabled(data.contextualRecommendationEnabled)) if (data.autoRecommendMemories !== undefined) dispatch(setAutoRecommendMemories(data.autoRecommendMemories)) - if (data.recommendationThreshold !== undefined) dispatch(setRecommendationThreshold(data.recommendationThreshold)) + 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 => { + .catch((error) => { console.error('[MemoryProvider] Error loading short-term memory data:', error) }) @@ -117,7 +122,7 @@ const MemoryProvider: FC = ({ children }) => { const state = store.getState().memory if (!state.currentListId && state.memoryLists && state.memoryLists.length > 0) { // 先尝试找到一个isActive为true的列表 - const activeList = state.memoryLists.find(list => list.isActive) + const activeList = state.memoryLists.find((list) => list.isActive) if (activeList) { console.log('[MemoryProvider] Auto-selecting active memory list:', activeList.name) dispatch(setCurrentMemoryList(activeList.id)) @@ -132,7 +137,7 @@ const MemoryProvider: FC = ({ children }) => { console.log('[MemoryProvider] No long-term memory data loaded or loading failed') } }) - .catch(error => { + .catch((error) => { console.error('[MemoryProvider] Error loading long-term memory data:', error) }) }, [dispatch]) @@ -189,9 +194,9 @@ const MemoryProvider: FC = ({ children }) => { const state = store.getState().memory if (state.memoryLists && state.memoryLists.length > 0) { // 如果没有选中的记忆列表,或者选中的列表不存在 - if (!state.currentListId || !state.memoryLists.some(list => list.id === state.currentListId)) { + if (!state.currentListId || !state.memoryLists.some((list) => list.id === state.currentListId)) { // 先尝试找到一个isActive为true的列表 - const activeList = state.memoryLists.find(list => list.isActive) + const activeList = state.memoryLists.find((list) => list.isActive) if (activeList) { console.log('[MemoryProvider] Setting active memory list:', activeList.name) dispatch(setCurrentMemoryList(activeList.id)) diff --git a/src/renderer/src/components/Popups/ShortMemoryPopup.tsx b/src/renderer/src/components/Popups/ShortMemoryPopup.tsx index a32687fdce..61e1be7dbb 100644 --- a/src/renderer/src/components/Popups/ShortMemoryPopup.tsx +++ b/src/renderer/src/components/Popups/ShortMemoryPopup.tsx @@ -5,9 +5,9 @@ import { addShortMemoryItem, analyzeAndAddShortMemories } from '@renderer/servic import { useAppDispatch, useAppSelector } from '@renderer/store' import store from '@renderer/store' import { deleteShortMemory } from '@renderer/store/memory' -import { Button, Card, Col, Empty, Input, List, Modal, Row, Statistic, Tooltip, message } from 'antd' -import { useState, useCallback } from 'react' +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' @@ -49,79 +49,88 @@ const PopupContainer: React.FC = ({ topicId, resolve }) => { const [isAnalyzing, setIsAnalyzing] = useState(false) // 添加新的短记忆 - 使用防抖减少频繁更新 - const handleAddMemory = useCallback(_.debounce(() => { - if (newMemoryContent.trim() && topicId) { - addShortMemoryItem(newMemoryContent.trim(), topicId) - setNewMemoryContent('') // 清空输入框 - } - }, 300), [newMemoryContent, topicId]) + 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 + 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') || '未发现新的重要信息或所有信息已存在' + 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) } - } 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]) + }, 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) + const handleDeleteMemory = useCallback( + _.throttle(async (id: string) => { + // 先从当前状态中获取要删除的记忆之外的所有记忆 + const state = store.getState().memory + const filteredShortMemories = state.shortMemories.filter((memory) => memory.id !== id) - // 执行删除操作 - dispatch(deleteShortMemory(id)) + // 执行删除操作 + dispatch(deleteShortMemory(id)) - // 直接使用 window.api.memory.saveData 方法保存过滤后的列表 - try { - // 加载当前文件数据 - const currentData = await window.api.memory.loadData() + // 直接使用 window.api.memory.saveData 方法保存过滤后的列表 + try { + // 加载当前文件数据 + const currentData = await window.api.memory.loadData() - // 替换 shortMemories 数组 - const newData = { - ...currentData, - shortMemories: filteredShortMemories - } + // 替换 shortMemories 数组 + const newData = { + ...currentData, + shortMemories: filteredShortMemories + } - // 使用 true 参数强制覆盖文件 - const result = await window.api.memory.saveData(newData, true) + // 使用 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}`) + 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') || '删除失败') } - } catch (error) { - console.error('[ShortMemoryPopup] Failed to delete short memory:', error) - message.error(t('settings.memory.deleteError') || '删除失败') - } - }, 500), [dispatch, t]) + }, 500), + [dispatch, t] + ) const onClose = () => { setOpen(false) @@ -157,7 +166,10 @@ const PopupContainer: React.FC = ({ topicId, resolve }) => { disabled={!shortMemoryActive || !newMemoryContent.trim() || !topicId}> {t('settings.memory.addShortMemory')} - diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 86d60b08c5..5f024604f6 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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", @@ -1055,6 +1061,8 @@ "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", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 5476f63f6f..89f6ea759b 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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 地址", @@ -1060,6 +1066,8 @@ "startingAnalysis": "开始分析...", "cannotAnalyze": "无法分析,请检查设置", "resetAnalyzingState": "重置分析状态", + "filterSensitiveInfo": "过滤敏感信息", + "filterSensitiveInfoTip": "启用后,记忆功能将不会提取API密钥、密码等敏感信息", "resetLongTermMemory": "重置分析标记", "resetLongTermMemorySuccess": "长期记忆分析标记已重置", "resetLongTermMemoryNoChange": "没有需要重置的分析标记", diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 232b66a2df..ae9806d97d 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -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 = ({ 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 = ({ assistant: _assistant, setActiveTopic, topic }) = const handleKeyDown = (event: React.KeyboardEvent) => { 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 = ({ assistant: _assistant, setActiveTopic, topic }) = const onChange = (e: React.ChangeEvent) => { 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 = ({ 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() diff --git a/src/renderer/src/pages/home/Markdown/Markdown.tsx b/src/renderer/src/pages/home/Markdown/Markdown.tsx index eb96421ea5..cb1d8bf859 100644 --- a/src/renderer/src/pages/home/Markdown/Markdown.tsx +++ b/src/renderer/src/pages/home/Markdown/Markdown.tsx @@ -61,18 +61,18 @@ const Markdown: FC = ({ message }) => { think: (props: any) => { // 将think标签内容渲染为带样式的div return ( -
-
- 思考过程: -
+
+
思考过程:
{props.children}
) diff --git a/src/renderer/src/pages/home/Messages/Message.tsx b/src/renderer/src/pages/home/Messages/Message.tsx index 9da880f17b..c5185da9dc 100644 --- a/src/renderer/src/pages/home/Messages/Message.tsx +++ b/src/renderer/src/pages/home/Messages/Message.tsx @@ -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 = ({ 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 = ({ .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 = ({ {contextMenuPosition && (
@@ -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; diff --git a/src/renderer/src/pages/home/Messages/MessageContent.tsx b/src/renderer/src/pages/home/Messages/MessageContent.tsx index a0c6ea21bf..1780406c04 100644 --- a/src/renderer/src/pages/home/Messages/MessageContent.tsx +++ b/src/renderer/src/pages/home/Messages/MessageContent.tsx @@ -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 = ({ message: _message, model }) => { {message.mentions?.map((model) => {'@' + model.name})} - - + {message.referencedMessages && message.referencedMessages.length > 0 && ( +
+ {message.referencedMessages.map((refMsg, index) => ( + + + {t('message.referenced_message')}{' '} + {message.referencedMessages && message.referencedMessages.length > 1 + ? `(${index + 1}/${message.referencedMessages.length})` + : ''} + + {refMsg.role === 'user' ? t('common.you') : 'AI'} +
+ ), + extra: ( + { + e.stopPropagation() + navigator.clipboard.writeText(refMsg.id) + window.message.success({ + content: t('message.id_copied') || '消息ID已复制', + key: 'copy-reference-id' + }) + }}> + ID: {refMsg.id} + + ), + children: ( +
+
{refMsg.content}
+
+
+ ) + } + ]} + /> + ))} +
+ )} + + {/* 兼容旧版本的referencedMessage */} + {!message.referencedMessages && (message as any).referencedMessage && ( + + {t('message.referenced_message')} + + {(message as any).referencedMessage.role === 'user' ? t('common.you') : 'AI'} + +
+ ), + extra: ( + { + 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} + + ), + children: ( +
+
{(message as any).referencedMessage.content}
+
+
+ ) + } + ]} + /> + )} +
+ + +
{message.metadata?.generateImage && } {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) diff --git a/src/renderer/src/pages/home/Messages/MessageTokens.tsx b/src/renderer/src/pages/home/Messages/MessageTokens.tsx index 8a782447b8..caf69284e3 100644 --- a/src/renderer/src/pages/home/Messages/MessageTokens.tsx +++ b/src/renderer/src/pages/home/Messages/MessageTokens.tsx @@ -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 ( + + +
{message.role === 'user' ? t('common.you') : 'AI'}
+
{message.content}
+
{new Date(message.createdAt).toLocaleString()}
+
+
+ ) +} + +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(null) + + // 渲染引用消息弹窗 + const renderReferenceModal = () => { + return + } 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
} @@ -18,7 +95,16 @@ const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({ if (message.role === 'user') { return ( - Tokens: {message?.usage?.total_tokens} + Tokens: {message?.usage?.total_tokens} + + diff --git a/src/renderer/src/pages/settings/MemorySettings/HistoricalContextSettings.tsx b/src/renderer/src/pages/settings/MemorySettings/HistoricalContextSettings.tsx index 2d11b9f829..91b9d3b4c5 100644 --- a/src/renderer/src/pages/settings/MemorySettings/HistoricalContextSettings.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/HistoricalContextSettings.tsx @@ -44,7 +44,7 @@ const HistoricalContextSettings: FC = () => { // 遍历所有服务商的模型找到匹配的模型 for (const provider of Object.values(providers)) { - const model = provider.models.find(m => m.id === historicalContextAnalyzeModel) + const model = provider.models.find((m) => m.id === historicalContextAnalyzeModel) if (model) { return `${model.name} | ${provider.name}` } @@ -57,15 +57,17 @@ const HistoricalContextSettings: FC = () => { {t('settings.memory.historicalContext.title') || '历史对话上下文'} - {t('settings.memory.historicalContext.description') || - '允许AI在需要时自动引用历史对话,以提供更连贯的回答。'} + {t('settings.memory.historicalContext.description') || '允许AI在需要时自动引用历史对话,以提供更连贯的回答。'} {t('settings.memory.historicalContext.enable') || '启用历史对话上下文'} - + @@ -75,8 +77,11 @@ const HistoricalContextSettings: FC = () => { {t('settings.memory.analyzeModel') || '分析模型'} - + @@ -86,7 +91,7 @@ const HistoricalContextSettings: FC = () => { 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) + const model = provider.models.find((m) => m.id === historicalContextAnalyzeModel) if (model) { currentModel = model break @@ -99,8 +104,7 @@ const HistoricalContextSettings: FC = () => { handleModelChange(selectedModel.id) } }} - style={{ width: 300 }} - > + style={{ width: 300 }}> {historicalContextAnalyzeModel ? getSelectedModelName() : t('settings.memory.selectModel') || '选择模型'} diff --git a/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx b/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx index c68f5bb137..7a7f83b441 100644 --- a/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/MemoryListManager.tsx @@ -6,9 +6,9 @@ import { deleteMemoryList, editMemoryList, MemoryList, + saveLongTermMemoryData, setCurrentMemoryList, - toggleMemoryListActive, - saveLongTermMemoryData + toggleMemoryListActive } from '@renderer/store/memory' import { Button, Empty, Input, List, Modal, Switch, Tooltip, Typography } from 'antd' import React, { useState } from 'react' @@ -76,10 +76,12 @@ const MemoryListManager: React.FC = ({ onSelectList }) = // 保存到长期记忆文件 try { const state = store.getState().memory - await dispatch(saveLongTermMemoryData({ - memoryLists: state.memoryLists, - currentListId: state.currentListId - })).unwrap() + 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) @@ -114,10 +116,12 @@ const MemoryListManager: React.FC = ({ onSelectList }) = // 保存到长期记忆文件 try { const state = store.getState().memory - await dispatch(saveLongTermMemoryData({ - memoryLists: state.memoryLists, - currentListId: state.currentListId - })).unwrap() + 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) @@ -133,10 +137,12 @@ const MemoryListManager: React.FC = ({ onSelectList }) = // 保存到长期记忆文件 try { const state = store.getState().memory - await dispatch(saveLongTermMemoryData({ - memoryLists: state.memoryLists, - currentListId: state.currentListId - })).unwrap() + 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) @@ -153,10 +159,12 @@ const MemoryListManager: React.FC = ({ onSelectList }) = // 保存到长期记忆文件 try { const state = store.getState().memory - await dispatch(saveLongTermMemoryData({ - memoryLists: state.memoryLists, - currentListId: state.currentListId - })).unwrap() + 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) diff --git a/src/renderer/src/pages/settings/MemorySettings/PriorityManagementSettings.tsx b/src/renderer/src/pages/settings/MemorySettings/PriorityManagementSettings.tsx index d2d2807117..497b2d96b8 100644 --- a/src/renderer/src/pages/settings/MemorySettings/PriorityManagementSettings.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/PriorityManagementSettings.tsx @@ -101,8 +101,11 @@ const PriorityManagementSettings: FC = () => { {t('settings.memory.priorityManagement.enable') || '启用智能优先级管理'} - + @@ -114,23 +117,19 @@ const PriorityManagementSettings: FC = () => { {t('settings.memory.priorityManagement.decay') || '记忆衰减'} - + - + {t('settings.memory.priorityManagement.decayRate') || '衰减速率'} - + @@ -161,30 +160,25 @@ const PriorityManagementSettings: FC = () => { {t('settings.memory.priorityManagement.freshness') || '记忆鲜度'} - + - + {t('settings.memory.priorityManagement.updateNow') || '立即更新优先级'} - + - diff --git a/src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx b/src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx index be6d9a4ed1..3a439b2b42 100644 --- a/src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/ShortMemoryManager.tsx @@ -4,8 +4,8 @@ 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 { useState, useCallback } from 'react' import _ from 'lodash' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' const { Title } = Typography @@ -35,46 +35,52 @@ const ShortMemoryManager = () => { } // 添加新的短记忆 - 使用防抖减少频繁更新 - const handleAddMemory = useCallback(_.debounce(() => { - if (newMemoryContent.trim() && currentTopicId) { - addShortMemoryItem(newMemoryContent.trim(), currentTopicId) - setNewMemoryContent('') // 清空输入框 - } - }, 300), [newMemoryContent, currentTopicId]) + 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) + const handleDeleteMemory = useCallback( + _.throttle(async (id: string) => { + // 先从当前状态中获取要删除的记忆之外的所有记忆 + const state = store.getState().memory + const filteredShortMemories = state.shortMemories.filter((memory) => memory.id !== id) - // 执行删除操作 - dispatch(deleteShortMemory(id)) + // 执行删除操作 + dispatch(deleteShortMemory(id)) - // 直接使用 window.api.memory.saveData 方法保存过滤后的列表 - try { - // 加载当前文件数据 - const currentData = await window.api.memory.loadData() + // 直接使用 window.api.memory.saveData 方法保存过滤后的列表 + try { + // 加载当前文件数据 + const currentData = await window.api.memory.loadData() - // 替换 shortMemories 数组 - const newData = { - ...currentData, - shortMemories: filteredShortMemories + // 替换 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) } - - // 使用 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]) + }, 500), + [dispatch] + ) return (
diff --git a/src/renderer/src/pages/settings/MemorySettings/index.tsx b/src/renderer/src/pages/settings/MemorySettings/index.tsx index 00f52e4a51..487126c898 100644 --- a/src/renderer/src/pages/settings/MemorySettings/index.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/index.tsx @@ -2,6 +2,7 @@ import { AppstoreOutlined, DeleteOutlined, EditOutlined, + InfoCircleOutlined, PlusOutlined, SearchOutlined, UnorderedListOutlined @@ -9,7 +10,11 @@ import { import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' import { useTheme } from '@renderer/context/ThemeProvider' import { TopicManager } from '@renderer/hooks/useTopic' -import { analyzeAndAddShortMemories, resetLongTermMemoryAnalyzedMessageIds, useMemoryService } from '@renderer/services/MemoryService' +import { + analyzeAndAddShortMemories, + resetLongTermMemoryAnalyzedMessageIds, + useMemoryService +} from '@renderer/services/MemoryService' import { useAppDispatch, useAppSelector } from '@renderer/store' import store from '@renderer/store' // Import store for direct access import { @@ -17,14 +22,15 @@ import { clearMemories, deleteMemory, editMemory, + saveAllMemorySettings, + saveLongTermMemoryData, + saveMemoryData, setAnalyzeModel, setAnalyzing, setAutoAnalyze, + setFilterSensitiveInfo, setMemoryActive, - setShortMemoryAnalyzeModel, - saveMemoryData, - saveLongTermMemoryData, - saveAllMemorySettings + setShortMemoryAnalyzeModel } from '@renderer/store/memory' import { Topic } from '@renderer/types' import { Button, Empty, Input, List, message, Modal, Pagination, Radio, Select, Switch, Tabs, Tag, Tooltip } from 'antd' @@ -42,12 +48,12 @@ import { SettingTitle } from '..' import CollapsibleShortMemoryManager from './CollapsibleShortMemoryManager' +import ContextualRecommendationSettings from './ContextualRecommendationSettings' +import HistoricalContextSettings from './HistoricalContextSettings' import MemoryDeduplicationPanel from './MemoryDeduplicationPanel' import MemoryListManager from './MemoryListManager' import MemoryMindMap from './MemoryMindMap' import PriorityManagementSettings from './PriorityManagementSettings' -import ContextualRecommendationSettings from './ContextualRecommendationSettings' -import HistoricalContextSettings from './HistoricalContextSettings' const MemorySettings: FC = () => { const { t } = useTranslation() @@ -61,6 +67,7 @@ const MemorySettings: FC = () => { const currentListId = useAppSelector((state) => state.memory?.currentListId || null) const isActive = useAppSelector((state) => state.memory?.isActive || false) const autoAnalyze = useAppSelector((state) => state.memory?.autoAnalyze || false) + const filterSensitiveInfo = useAppSelector((state) => state.memory?.filterSensitiveInfo ?? true) // 默认启用敏感信息过滤 const analyzeModel = useAppSelector((state) => state.memory?.analyzeModel || null) const shortMemoryAnalyzeModel = useAppSelector((state) => state.memory?.shortMemoryAnalyzeModel || null) const isAnalyzing = useAppSelector((state) => state.memory?.isAnalyzing || false) @@ -72,7 +79,7 @@ const MemorySettings: FC = () => { const models = useMemo(() => { // 只获取已启用的提供商的模型 return providers - .filter(provider => provider.enabled) // 只保留已启用的提供商 + .filter((provider) => provider.enabled) // 只保留已启用的提供商 .flatMap((provider) => provider.models || []) }, [providers]) @@ -177,20 +184,22 @@ const MemorySettings: FC = () => { const handleDeleteMemory = async (id: string) => { // 先从当前状态中获取要删除的记忆之外的所有记忆 const state = store.getState().memory - const filteredMemories = state.memories.filter(memory => memory.id !== id) + const filteredMemories = state.memories.filter((memory) => memory.id !== id) // 执行删除操作 dispatch(deleteMemory(id)) // 保存到长期记忆文件,并强制覆盖 try { - await dispatch(saveLongTermMemoryData({ - memories: filteredMemories, // 直接使用过滤后的数组,而不是使用当前状态 - memoryLists: state.memoryLists, - currentListId: state.currentListId, - analyzeModel: state.analyzeModel, - forceOverwrite: true // 强制覆盖文件,而不是尝试合并 - })).unwrap() + await dispatch( + saveLongTermMemoryData({ + memories: filteredMemories, // 直接使用过滤后的数组,而不是使用当前状态 + memoryLists: state.memoryLists, + currentListId: state.currentListId, + analyzeModel: state.analyzeModel, + forceOverwrite: true // 强制覆盖文件,而不是尝试合并 + }) + ).unwrap() console.log('[Memory Settings] Long-term memories saved to file after deletion (force overwrite)') message.success(t('settings.memory.deleteSuccess')) @@ -225,13 +234,15 @@ const MemorySettings: FC = () => { try { // 直接传递空数组作为 memories,确保完全清空 const state = store.getState().memory - await dispatch(saveLongTermMemoryData({ - memories: [], // 直接使用空数组,而不是使用当前状态 - memoryLists: state.memoryLists, - currentListId: state.currentListId, - analyzeModel: state.analyzeModel, - forceOverwrite: true // 强制覆盖文件,而不是合并 - })).unwrap() + await dispatch( + saveLongTermMemoryData({ + memories: [], // 直接使用空数组,而不是使用当前状态 + memoryLists: state.memoryLists, + currentListId: state.currentListId, + analyzeModel: state.analyzeModel, + forceOverwrite: true // 强制覆盖文件,而不是合并 + }) + ).unwrap() console.log('[Memory Settings] Long-term memories saved to file after clearing (force overwrite)') } catch (error) { console.error('[Memory Settings] Failed to save long-term memory data after clearing:', error) @@ -250,6 +261,20 @@ const MemorySettings: FC = () => { dispatch(setAutoAnalyze(checked)) } + // 处理切换敏感信息过滤 + const handleToggleFilterSensitiveInfo = async (checked: boolean) => { + dispatch(setFilterSensitiveInfo(checked)) + console.log('[Memory Settings] Filter sensitive info set:', checked) + + // 使用Redux Thunk保存到JSON文件 + try { + await dispatch(saveMemoryData({ filterSensitiveInfo: checked })).unwrap() + console.log('[Memory Settings] Filter sensitive info saved to file successfully:', checked) + } catch (error) { + console.error('[Memory Settings] Failed to save filter sensitive info to file:', error) + } + } + // 处理选择长期记忆分析模型 const handleSelectModel = async (modelId: string) => { dispatch(setAnalyzeModel(modelId)) @@ -337,7 +362,7 @@ const MemorySettings: FC = () => { // 遍历所有服务商的模型找到匹配的模型 for (const provider of Object.values(providers)) { - const model = provider.models.find(m => m.id === analyzeModel) + const model = provider.models.find((m) => m.id === analyzeModel) if (model) { return `${model.name} | ${provider.name}` } @@ -352,7 +377,7 @@ const MemorySettings: FC = () => { // 遍历所有服务商的模型找到匹配的模型 for (const provider of Object.values(providers)) { - const model = provider.models.find(m => m.id === shortMemoryAnalyzeModel) + const model = provider.models.find((m) => m.id === shortMemoryAnalyzeModel) if (model) { return `${model.name} | ${provider.name}` } @@ -547,6 +572,23 @@ const MemorySettings: FC = () => { {t('settings.memory.enableAutoAnalyze')} + + + {t('settings.memory.filterSensitiveInfo') || '过滤敏感信息'} + + + + + + {/* 短期记忆分析模型选择 */} @@ -559,7 +601,7 @@ const MemorySettings: FC = () => { let currentModel: { id: string; provider: string; name: string; group: string } | undefined if (shortMemoryAnalyzeModel) { for (const provider of Object.values(providers)) { - const model = provider.models.find(m => m.id === shortMemoryAnalyzeModel) + const model = provider.models.find((m) => m.id === shortMemoryAnalyzeModel) if (model) { currentModel = model break @@ -573,9 +615,10 @@ const MemorySettings: FC = () => { } }} style={{ width: 300 }} - disabled={!isActive} - > - {shortMemoryAnalyzeModel ? getSelectedShortMemoryModelName() : t('settings.memory.selectModel') || '选择模型'} + disabled={!isActive}> + {shortMemoryAnalyzeModel + ? getSelectedShortMemoryModelName() + : t('settings.memory.selectModel') || '选择模型'} @@ -664,13 +707,11 @@ const MemorySettings: FC = () => { {t('settings.memory.saveAllSettings') || '保存所有设置'} - {t('settings.memory.saveAllSettingsDescription') || '将所有记忆功能的设置保存到文件中,确保应用重启后设置仍然生效。'} + {t('settings.memory.saveAllSettingsDescription') || + '将所有记忆功能的设置保存到文件中,确保应用重启后设置仍然生效。'} - @@ -706,6 +747,23 @@ const MemorySettings: FC = () => { {t('settings.memory.enableAutoAnalyze')} + + + {t('settings.memory.filterSensitiveInfo') || '过滤敏感信息'} + + + + + + {/* 长期记忆分析模型选择 */} @@ -716,7 +774,7 @@ const MemorySettings: FC = () => { let currentModel: { id: string; provider: string; name: string; group: string } | undefined if (analyzeModel) { for (const provider of Object.values(providers)) { - const model = provider.models.find(m => m.id === analyzeModel) + const model = provider.models.find((m) => m.id === analyzeModel) if (model) { currentModel = model break @@ -730,8 +788,7 @@ const MemorySettings: FC = () => { } }} style={{ width: 300 }} - disabled={!isActive} - > + disabled={!isActive}> {analyzeModel ? getSelectedModelName() : t('settings.memory.selectModel') || '选择模型'} @@ -868,71 +925,75 @@ const MemorySettings: FC = () => { {viewMode === 'list' ? ( memories.length > 0 && isActive ? (
- (currentListId ? memory.listId === currentListId : true)) + .filter((memory) => categoryFilter === null || memory.category === categoryFilter) + .slice((currentPage - 1) * pageSize, currentPage * pageSize)} + renderItem={(memory) => ( + +
+ } + description={ + + {new Date(memory.createdAt).toLocaleString()} + {memory.source && {memory.source}} + + } + /> + + )} + /> + {/* 分页组件 */} + {memories .filter((memory) => (currentListId ? memory.listId === currentListId : true)) - .filter((memory) => categoryFilter === null || memory.category === categoryFilter) - .slice((currentPage - 1) * pageSize, currentPage * pageSize)} - renderItem={(memory) => ( - -
- } - description={ - - {new Date(memory.createdAt).toLocaleString()} - {memory.source && {memory.source}} - + .filter((memory) => categoryFilter === null || memory.category === categoryFilter).length > + pageSize && ( + + setCurrentPage(page)} + total={ + memories + .filter((memory) => (currentListId ? memory.listId === currentListId : true)) + .filter((memory) => categoryFilter === null || memory.category === categoryFilter) + .length } + pageSize={pageSize} + size="small" + showSizeChanger={false} /> - + )} - /> - {/* 分页组件 */} - {memories - .filter((memory) => (currentListId ? memory.listId === currentListId : true)) - .filter((memory) => categoryFilter === null || memory.category === categoryFilter).length > pageSize && ( - - setCurrentPage(page)} - total={memories - .filter((memory) => (currentListId ? memory.listId === currentListId : true)) - .filter((memory) => categoryFilter === null || memory.category === categoryFilter).length} - pageSize={pageSize} - size="small" - showSizeChanger={false} - /> - - )}
) : ( diff --git a/src/renderer/src/providers/AiProvider/AnthropicProvider.ts b/src/renderer/src/providers/AiProvider/AnthropicProvider.ts index d1e1d611cb..8f0d325cd1 100644 --- a/src/renderer/src/providers/AiProvider/AnthropicProvider.ts +++ b/src/renderer/src/providers/AiProvider/AnthropicProvider.ts @@ -502,7 +502,7 @@ export default class AnthropicProvider extends BaseProvider { // 应用记忆功能到系统提示词 const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService') - const enhancedPrompt = applyMemoriesToPrompt(prompt) + const enhancedPrompt = await applyMemoriesToPrompt(prompt) console.log( '[AnthropicProvider] Applied memories to prompt, length difference:', enhancedPrompt.length - prompt.length diff --git a/src/renderer/src/providers/AiProvider/BaseProvider.ts b/src/renderer/src/providers/AiProvider/BaseProvider.ts index c61e10e43f..2b21ec460f 100644 --- a/src/renderer/src/providers/AiProvider/BaseProvider.ts +++ b/src/renderer/src/providers/AiProvider/BaseProvider.ts @@ -102,10 +102,22 @@ export default abstract class BaseProvider { } public async getMessageContent(message: Message) { - if (isEmpty(message.content)) { + if (isEmpty(message.content) && !message.referencedMessages?.length) { return message.content } + // 处理引用消息 + if (message.referencedMessages && message.referencedMessages.length > 0) { + const refMsg = message.referencedMessages[0] + const roleText = refMsg.role === 'user' ? '用户' : 'AI' + const referencedContent = `===引用消息开始===\n角色: ${roleText}\n内容: ${refMsg.content}\n===引用消息结束===` + // 如果消息内容为空,则直接返回引用内容 + if (isEmpty(message.content.trim())) { + return referencedContent + } + return `${message.content}\n\n${referencedContent}` + } + const webSearchReferences = await this.getWebSearchReferences(message) if (!isEmpty(webSearchReferences)) { diff --git a/src/renderer/src/providers/AiProvider/GeminiProvider.ts b/src/renderer/src/providers/AiProvider/GeminiProvider.ts index 7897ecee96..f731d4575a 100644 --- a/src/renderer/src/providers/AiProvider/GeminiProvider.ts +++ b/src/renderer/src/providers/AiProvider/GeminiProvider.ts @@ -497,7 +497,7 @@ export default class GeminiProvider extends BaseProvider { // 应用记忆功能到系统提示词 const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService') - const enhancedPrompt = applyMemoriesToPrompt(prompt) + const enhancedPrompt = await applyMemoriesToPrompt(prompt) console.log( '[GeminiProvider] Applied memories to prompt, length difference:', enhancedPrompt.length - prompt.length diff --git a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts index 0905dbb494..f9205dd295 100644 --- a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts +++ b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts @@ -659,7 +659,7 @@ export default class OpenAIProvider extends BaseProvider { // 获取当前话题ID const currentTopicId = messages.length > 0 ? messages[0].topicId : undefined // 使用双重类型断言强制转换类型 - const enhancedPrompt = await applyMemoriesToPrompt(originalPrompt as string, currentTopicId) as unknown as string + const enhancedPrompt = (await applyMemoriesToPrompt(originalPrompt as string, currentTopicId)) as unknown as string // 存储原始提示词长度 const originalPromptLength = (originalPrompt as string).length console.log( @@ -768,7 +768,7 @@ export default class OpenAIProvider extends BaseProvider { // 应用记忆功能到系统提示词 const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService') // 使用双重类型断言强制转换类型 - const enhancedPrompt = await applyMemoriesToPrompt(prompt as string) as unknown as string + 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) diff --git a/src/renderer/src/services/ContextualMemoryService.ts b/src/renderer/src/services/ContextualMemoryService.ts index f3a954d814..18509bdcc4 100644 --- a/src/renderer/src/services/ContextualMemoryService.ts +++ b/src/renderer/src/services/ContextualMemoryService.ts @@ -1,11 +1,12 @@ // src/renderer/src/services/ContextualMemoryService.ts -import store from '@renderer/store' -import { vectorService } from './VectorService' -import { addMemoryRetrievalLatency } from '@renderer/store/memory' -import { fetchGenerate } from '@renderer/services/ApiService' -import { Message } from '@renderer/types' 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 { @@ -107,7 +108,7 @@ class ContextualMemoryService { ]) // 合并并排序推荐结果 - let allRecommendations = [...longTermRecommendations, ...shortTermRecommendations] + const allRecommendations = [...longTermRecommendations, ...shortTermRecommendations] // 按相关性分数排序 allRecommendations.sort((a, b) => b.relevanceScore - a.relevanceScore) @@ -120,7 +121,9 @@ class ContextualMemoryService { const latency = endTime - startTime store.dispatch(addMemoryRetrievalLatency(latency)) - console.log(`[ContextualMemory] Found ${limitedRecommendations.length} recommendations in ${latency.toFixed(2)}ms`) + console.log( + `[ContextualMemory] Found ${limitedRecommendations.length} recommendations in ${latency.toFixed(2)}ms` + ) return limitedRecommendations } catch (error) { @@ -180,7 +183,7 @@ class ContextualMemoryService { ]) // 合并并排序推荐结果 - let allRecommendations = [...longTermRecommendations, ...shortTermRecommendations] + const allRecommendations = [...longTermRecommendations, ...shortTermRecommendations] // 按相关性分数排序 allRecommendations.sort((a, b) => b.relevanceScore - a.relevanceScore) @@ -223,7 +226,7 @@ class ContextualMemoryService { ]) // 合并并排序推荐结果 - let allRecommendations = [...longTermRecommendations, ...shortTermRecommendations] + const allRecommendations = [...longTermRecommendations, ...shortTermRecommendations] // 按相关性分数排序 allRecommendations.sort((a, b) => b.relevanceScore - a.relevanceScore) @@ -274,7 +277,7 @@ class ContextualMemoryService { } // 构建对话内容 - const conversation = recentMessages.map(msg => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n') + const conversation = recentMessages.map((msg) => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n') // 构建提示词 const prompt = ` @@ -307,8 +310,8 @@ ${conversation} // 提取关键信息 const keyPoints = result .split('\n') - .map(line => line.trim()) - .filter(line => line && !line.startsWith('#') && !line.startsWith('-')) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#') && !line.startsWith('-')) console.log('[ContextualMemory] Extracted key points:', keyPoints) @@ -331,7 +334,7 @@ ${conversation} */ private _buildContextQuery(messages: Message[]): string { // 提取最近消息的内容 - const messageContents = messages.map(msg => msg.content || '').filter(content => content.trim() !== '') + const messageContents = messages.map((msg) => msg.content || '').filter((content) => content.trim() !== '') // 如果没有有效内容,返回空字符串 if (messageContents.length === 0) { @@ -349,10 +352,7 @@ ${conversation} * @returns 长期记忆推荐列表 * @private */ - private async _getLongTermMemoryRecommendations( - query: string, - topicId?: string - ): Promise { + private async _getLongTermMemoryRecommendations(query: string, topicId?: string): Promise { // 获取当前状态 const state = store.getState() const memoryState = state.memory @@ -363,16 +363,14 @@ ${conversation} } // 获取所有激活的记忆列表 - const activeListIds = memoryState.memoryLists - .filter(list => list.isActive) - .map(list => list.id) + 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)) + const memories = memoryState.memories.filter((memory) => activeListIds.includes(memory.listId)) if (memories.length === 0) { return [] @@ -387,7 +385,7 @@ ${conversation} ) // 转换为推荐格式 - const recommendations: MemoryRecommendation[] = results.map(result => ({ + const recommendations: MemoryRecommendation[] = results.map((result) => ({ memory: result.memory as Memory, relevanceScore: result.similarity, source: 'long-term', @@ -405,10 +403,7 @@ ${conversation} * @returns 短期记忆推荐列表 * @private */ - private async _getShortTermMemoryRecommendations( - query: string, - topicId?: string - ): Promise { + private async _getShortTermMemoryRecommendations(query: string, topicId?: string): Promise { // 获取当前状态 const state = store.getState() const memoryState = state.memory @@ -423,7 +418,7 @@ ${conversation} // 如果指定了话题ID,只获取该话题的短期记忆 if (topicId) { - shortMemories = shortMemories.filter(memory => memory.topicId === topicId) + shortMemories = shortMemories.filter((memory) => memory.topicId === topicId) } if (shortMemories.length === 0) { @@ -439,7 +434,7 @@ ${conversation} ) // 转换为推荐格式 - const recommendations: MemoryRecommendation[] = results.map(result => ({ + const recommendations: MemoryRecommendation[] = results.map((result) => ({ memory: result.memory as ShortMemory, relevanceScore: result.similarity, source: 'short-term', @@ -472,57 +467,57 @@ ${conversation} const memoryState = state.memory // 应用多因素排序优化 - return recommendations.map(rec => { - const memory = rec.memory - let adjustedScore = rec.relevanceScore + 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) + // 1. 考虑记忆的重要性 + if (memory.importance && memoryState.priorityManagementEnabled) { + adjustedScore *= 1 + memory.importance * 0.5 // 重要性最多提升50%的分数 } - } - // 返回调整后的推荐 - return { - ...rec, - relevanceScore: adjustedScore - } - }).sort((a, b) => b.relevanceScore - a.relevanceScore) // 按调整后的分数重新排序 + // 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) // 按调整后的分数重新排序 } } diff --git a/src/renderer/src/services/HistoricalContextService.ts b/src/renderer/src/services/HistoricalContextService.ts index 30fd9d9bd5..3df3817e1b 100644 --- a/src/renderer/src/services/HistoricalContextService.ts +++ b/src/renderer/src/services/HistoricalContextService.ts @@ -1,9 +1,9 @@ // src/renderer/src/services/HistoricalContextService.ts -import store from '@renderer/store' import { TopicManager } from '@renderer/hooks/useTopic' import { fetchGenerate } from '@renderer/services/ApiService' -import { Message, Topic } from '@renderer/types' +import store from '@renderer/store' import { ShortMemory } from '@renderer/store/memory' +import { Message } from '@renderer/types' /** * 分析当前对话并决定是否需要调用历史对话 @@ -112,11 +112,11 @@ const analyzeNeedForHistoricalContext = async ( try { // 准备分析提示词 const messagesContent = recentMessages - .map(msg => `${msg.role === 'user' ? '用户' : 'AI'}: ${msg.content}`) + .map((msg) => `${msg.role === 'user' ? '用户' : 'AI'}: ${msg.content}`) .join('\n') const memoriesContent = shortMemories - .map(memory => `话题ID: ${memory.topicId}\n内容: ${memory.content}`) + .map((memory) => `话题ID: ${memory.topicId}\n内容: ${memory.content}`) .join('\n\n') const prompt = ` @@ -153,7 +153,8 @@ ${memoriesContent} // 获取分析模型 const state = store.getState() // 优先使用历史对话上下文分析模型,如果没有设置,则使用短期记忆分析模型或长期记忆分析模型 - const analyzeModel = state.memory?.historicalContextAnalyzeModel || state.memory?.shortMemoryAnalyzeModel || state.memory?.analyzeModel + const analyzeModel = + state.memory?.historicalContextAnalyzeModel || state.memory?.shortMemoryAnalyzeModel || state.memory?.analyzeModel if (!analyzeModel) { console.log('[HistoricalContext] No analyze model set') @@ -199,8 +200,7 @@ ${memoriesContent} } // 如果都失败了,尝试简单的文本分析 - const needsContext = result.toLowerCase().includes('true') && - !result.toLowerCase().includes('false') + 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]+)/) @@ -229,9 +229,7 @@ const getOriginalDialogContent = async (topicId: string): Promise } // 格式化对话内容 - const dialogContent = messages - .map(msg => `${msg.role === 'user' ? '用户' : 'AI'}: ${msg.content}`) - .join('\n\n') + const dialogContent = messages.map((msg) => `${msg.role === 'user' ? '用户' : 'AI'}: ${msg.content}`).join('\n\n') return dialogContent } catch (error) { diff --git a/src/renderer/src/services/MemoryDeduplicationService.ts b/src/renderer/src/services/MemoryDeduplicationService.ts index b94640ff6b..f72bd61359 100644 --- a/src/renderer/src/services/MemoryDeduplicationService.ts +++ b/src/renderer/src/services/MemoryDeduplicationService.ts @@ -1,7 +1,14 @@ // 记忆去重与合并服务 import { fetchGenerate } from '@renderer/services/ApiService' import store from '@renderer/store' -import { addMemory, addShortMemory, deleteMemory, deleteShortMemory, saveMemoryData, saveLongTermMemoryData } from '@renderer/store/memory' +import { + addMemory, + addShortMemory, + deleteMemory, + deleteShortMemory, + saveLongTermMemoryData, + saveMemoryData +} from '@renderer/store/memory' // 记忆去重与合并的结果接口 export interface DeduplicationResult { @@ -137,7 +144,8 @@ ${memoriesToCheck} 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 + 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) { @@ -146,7 +154,12 @@ ${memoriesToCheck} 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 + const keywords = match[6] + ? match[6] + .trim() + .split(',') + .map((k: string) => k.trim()) + : undefined similarGroups.push({ groupId, @@ -299,18 +312,26 @@ export const applyDeduplicationResult = async ( // 保存到文件 if (isShortMemory) { // 短期记忆使用saveMemoryData - await store.dispatch(saveMemoryData({ - shortMemories: currentState.shortMemories - })).unwrap() + 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() + 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) { diff --git a/src/renderer/src/services/MemoryService.ts b/src/renderer/src/services/MemoryService.ts index 97962f52b3..4821931c2f 100644 --- a/src/renderer/src/services/MemoryService.ts +++ b/src/renderer/src/services/MemoryService.ts @@ -7,26 +7,27 @@ import { useAppDispatch, useAppSelector } from '@renderer/store' import store from '@renderer/store' // Import store // AiProvider no longer needed as we're using fetchGenerate import { + accessMemory, addAnalysisLatency, addMemory, addShortMemory, - setAnalyzing, - updateAnalysisStats, - updatePerformanceMetrics, - updateUserInterest, - updateMemoryPriorities, - accessMemory, - Memory, - saveMemoryData, - saveLongTermMemoryData, - updateCurrentRecommendations, - setRecommending, clearCurrentRecommendations, - MemoryRecommendation + Memory, + MemoryRecommendation, + saveLongTermMemoryData, + saveMemoryData, + setAnalyzing, + setRecommending, + updateAnalysisStats, + updateCurrentRecommendations, + updateMemoryPriorities, + updatePerformanceMetrics, + updateUserInterest } from '@renderer/store/memory' -import { useCallback, useEffect, useRef } from 'react' // Add useRef back -import { contextualMemoryService } from './ContextualMemoryService' // Import contextual memory service import { Message } from '@renderer/types' // Import Message type +import { useCallback, useEffect, useRef } from 'react' // Add useRef back + +import { contextualMemoryService } from './ContextualMemoryService' // Import contextual memory service // 计算对话复杂度,用于调整分析深度 const calculateConversationComplexity = (conversation: string): 'low' | 'medium' | 'high' => { @@ -45,6 +46,8 @@ const calculateConversationComplexity = (conversation: string): 'low' | 'medium' } // 根据分析深度调整提示词 +// 注意:该函数当前未使用,保留供将来可能的功能扩展 +/* const adjustPromptForDepth = (basePrompt: string, depth: 'low' | 'medium' | 'high'): string => { switch (depth) { case 'low': @@ -73,6 +76,7 @@ const adjustPromptForDepth = (basePrompt: string, depth: 'low' | 'medium' | 'hig return basePrompt } } +*/ // 提取用户关注点 const extractUserInterests = (conversation: string): string[] => { @@ -99,8 +103,11 @@ const analyzeConversation = async ( customPrompt?: string ): Promise> => { try { + // 获取当前的过滤敏感信息设置 + const filterSensitiveInfo = store.getState().memory?.filterSensitiveInfo ?? true + // 使用自定义提示词或默认提示词 - const basePrompt = + let basePrompt = customPrompt || ` 请分析对话内容,提取出重要的用户偏好、习惯、需求和背景信息,这些信息在未来的对话中可能有用。 @@ -117,6 +124,23 @@ const analyzeConversation = async ( 请确保每条信息都是简洁、准确的。如果没有找到重要信息,请返回空字符串。 ` + + // 如果启用了敏感信息过滤,添加相关指令 + if (filterSensitiveInfo) { + basePrompt += ` +## 安全提示: +请注意不要提取任何敏感信息,包括但不限于: +- API密钥、访问令牌或其他凭证 +- 密码或密码提示 +- 私人联系方式(如电话号码、邮箱地址) +- 个人身份信息(如身份证号、社保号) +- 银行账户或支付信息 +- 私密的个人或商业信息 + +如果发现此类信息,请完全忽略,不要以任何形式记录或提取。 +` + } + console.log(`[Memory Analysis] Analyzing conversation using model: ${modelId}`) // 将提示词和对话内容合并到一个系统提示词中 @@ -134,7 +158,7 @@ ${conversation} console.log('[Memory Analysis] Calling fetchGenerate with combined prompt...') const result = await fetchGenerate({ prompt: combinedPrompt, - content: "", // 内容字段留空 + content: '', // 内容字段留空 modelId: modelId }) @@ -169,7 +193,7 @@ ${conversation} } catch (error) { console.error('Failed to analyze conversation with real AI:', error) // Consider logging the specific error details if possible - console.error('Error details:', JSON.stringify(error, null, 2)); + console.error('Error details:', JSON.stringify(error, null, 2)) return [] as Array<{ content: string; category: string }> // Return empty array on error } } @@ -205,14 +229,10 @@ export const getContextualMemoryRecommendations = async ( store.dispatch(setRecommending(true)) // 调用上下文感知记忆服务获取推荐 - const recommendations = await contextualMemoryService.getContextualMemoryRecommendations( - messages, - topicId, - limit - ) + const recommendations = await contextualMemoryService.getContextualMemoryRecommendations(messages, topicId, limit) // 转换为Redux状态中的推荐格式 - const memoryRecommendations: MemoryRecommendation[] = recommendations.map(rec => ({ + const memoryRecommendations: MemoryRecommendation[] = recommendations.map((rec) => ({ memoryId: rec.memory.id, relevanceScore: rec.relevanceScore, source: rec.source, @@ -239,10 +259,7 @@ export const getContextualMemoryRecommendations = async ( * @param limit - 返回的最大记忆数量 * @returns 与当前主题相关的记忆列表 */ -export const getTopicRelatedMemories = async ( - topicId: string, - limit: number = 10 -): Promise => { +export const getTopicRelatedMemories = async (topicId: string, limit: number = 10): Promise => { try { // 获取当前状态 const state = store.getState().memory @@ -257,13 +274,10 @@ export const getTopicRelatedMemories = async ( store.dispatch(setRecommending(true)) // 调用上下文感知记忆服务获取推荐 - const recommendations = await contextualMemoryService.getTopicRelatedMemories( - topicId, - limit - ) + const recommendations = await contextualMemoryService.getTopicRelatedMemories(topicId, limit) // 转换为Redux状态中的推荐格式 - const memoryRecommendations: MemoryRecommendation[] = recommendations.map(rec => ({ + const memoryRecommendations: MemoryRecommendation[] = recommendations.map((rec) => ({ memoryId: rec.memory.id, relevanceScore: rec.relevanceScore, source: rec.source, @@ -308,13 +322,10 @@ export const getAIEnhancedMemoryRecommendations = async ( store.dispatch(setRecommending(true)) // 调用上下文感知记忆服务获取推荐 - const recommendations = await contextualMemoryService.getAIEnhancedMemoryRecommendations( - messages, - limit - ) + const recommendations = await contextualMemoryService.getAIEnhancedMemoryRecommendations(messages, limit) // 转换为Redux状态中的推荐格式 - const memoryRecommendations: MemoryRecommendation[] = recommendations.map(rec => ({ + const memoryRecommendations: MemoryRecommendation[] = recommendations.map((rec) => ({ memoryId: rec.memory.id, relevanceScore: rec.relevanceScore, source: rec.source, @@ -342,7 +353,9 @@ export const useMemoryService = () => { const isActive = useAppSelector((state) => state.memory?.isActive || false) const autoAnalyze = useAppSelector((state) => state.memory?.autoAnalyze || false) const analyzeModel = useAppSelector((state) => state.memory?.analyzeModel || null) - const contextualRecommendationEnabled = useAppSelector((state) => state.memory?.contextualRecommendationEnabled || false) + const contextualRecommendationEnabled = useAppSelector( + (state) => state.memory?.contextualRecommendationEnabled || false + ) const autoRecommendMemories = useAppSelector((state) => state.memory?.autoRecommendMemories || false) // 使用 useCallback 定义分析函数,但减少依赖项 @@ -511,7 +524,7 @@ ${existingMemoriesContent} // 调用分析函数,传递自定义提示词和对话内容 // 将对话内容直接放在提示词中,不再单独传递 - const memories = await analyzeConversation("", memoryState.analyzeModel!, basePrompt) + const memories = await analyzeConversation('', memoryState.analyzeModel!, basePrompt) // 用户关注点学习 if (memoryState.interestTrackingEnabled) { @@ -623,12 +636,16 @@ ${existingMemoriesContent} if (addedNewMemories) { try { const state = store.getState().memory - await store.dispatch(saveLongTermMemoryData({ - memories: state.memories, - memoryLists: state.memoryLists, - currentListId: state.currentListId, - analyzeModel: state.analyzeModel - })).unwrap() + await store + .dispatch( + saveLongTermMemoryData({ + memories: state.memories, + memoryLists: state.memoryLists, + currentListId: state.currentListId, + analyzeModel: state.analyzeModel + }) + ) + .unwrap() console.log('[Memory Analysis] Long-term memories saved to file after analysis') } catch (error) { console.error('[Memory Analysis] Failed to save long-term memory data after analysis:', error) @@ -712,8 +729,6 @@ ${existingMemoriesContent} analyzeAndAddMemoriesRef.current = analyzeAndAddMemories }, [analyzeAndAddMemories]) - - // 记录记忆访问 const recordMemoryAccess = useCallback((memoryId: string, isShortMemory: boolean = false) => { store.dispatch(accessMemory({ id: memoryId, isShortMemory })) @@ -722,20 +737,23 @@ ${existingMemoriesContent} // Effect 来设置/清除定时器,只依赖于启动条件 useEffect(() => { // 定期更新记忆优先级 - const priorityUpdateInterval = setInterval(() => { - const memoryState = store.getState().memory - if (!memoryState?.priorityManagementEnabled) return + const priorityUpdateInterval = setInterval( + () => { + const memoryState = store.getState().memory + if (!memoryState?.priorityManagementEnabled) return - // 检查上次更新时间,避免频繁更新 - const now = Date.now() - const lastUpdate = memoryState.lastPriorityUpdate || 0 - const updateInterval = 30 * 60 * 1000 // 30分钟更新一次 + // 检查上次更新时间,避免频繁更新 + const now = Date.now() + const lastUpdate = memoryState.lastPriorityUpdate || 0 + const updateInterval = 30 * 60 * 1000 // 30分钟更新一次 - if (now - lastUpdate < updateInterval) return + if (now - lastUpdate < updateInterval) return - console.log('[Memory Priority] Updating memory priorities and freshness...') - store.dispatch(updateMemoryPriorities()) - }, 10 * 60 * 1000) // 每10分钟检查一次 + console.log('[Memory Priority] Updating memory priorities and freshness...') + store.dispatch(updateMemoryPriorities()) + }, + 10 * 60 * 1000 + ) // 每10分钟检查一次 return () => { clearInterval(priorityUpdateInterval) @@ -825,16 +843,19 @@ ${existingMemoriesContent} console.log('[ContextualMemory] Setting up auto recommendation timer...') // 每5分钟自动推荐一次记忆 - const intervalId = setInterval(() => { - const state = store.getState() - const currentTopicId = state.messages.currentTopic?.id - const messages = currentTopicId ? state.messages.messagesByTopic?.[currentTopicId] || [] : [] + const intervalId = setInterval( + () => { + const state = store.getState() + const currentTopicId = state.messages.currentTopic?.id + const messages = currentTopicId ? state.messages.messagesByTopic?.[currentTopicId] || [] : [] - if (currentTopicId && messages.length > 0) { - console.log('[ContextualMemory] Auto recommendation triggered') - getContextualRecommendations(messages, currentTopicId) - } - }, 5 * 60 * 1000) // 5分钟 + if (currentTopicId && messages.length > 0) { + console.log('[ContextualMemory] Auto recommendation triggered') + getContextualRecommendations(messages, currentTopicId) + } + }, + 5 * 60 * 1000 + ) // 5分钟 return () => { console.log('[ContextualMemory] Clearing auto recommendation timer') @@ -873,10 +894,14 @@ export const addShortMemoryItem = async ( // 保存到文件,并强制覆盖 try { const state = store.getState().memory - await store.dispatch(saveMemoryData({ - shortMemories: state.shortMemories, - forceOverwrite: true // 强制覆盖文件,确保数据正确保存 - })).unwrap() + await store + .dispatch( + saveMemoryData({ + shortMemories: state.shortMemories, + forceOverwrite: true // 强制覆盖文件,确保数据正确保存 + }) + ) + .unwrap() console.log('[Memory] Short memory saved to file after manual addition (force overwrite)') } catch (error) { console.error('[Memory] Failed to save short memory data after manual addition:', error) @@ -905,12 +930,16 @@ export const addMemoryItem = async ( // 保存到长期记忆文件 try { const state = store.getState().memory - await store.dispatch(saveLongTermMemoryData({ - memories: state.memories, - memoryLists: state.memoryLists, - currentListId: state.currentListId, - analyzeModel: state.analyzeModel - })).unwrap() + await store + .dispatch( + saveLongTermMemoryData({ + memories: state.memories, + memoryLists: state.memoryLists, + currentListId: state.currentListId, + analyzeModel: state.analyzeModel + }) + ) + .unwrap() console.log('[Memory] Long-term memory saved to file after manual addition') } catch (error) { console.error('[Memory] Failed to save long-term memory data after manual addition:', error) @@ -934,7 +963,7 @@ export const resetLongTermMemoryAnalyzedMessageIds = async (topicId: string): Pr // 找到指定话题的所有长期记忆 const memories = state.memories || [] - const topicMemories = memories.filter(memory => memory.topicId === topicId) + const topicMemories = memories.filter((memory) => memory.topicId === topicId) if (topicMemories.length === 0) { console.log(`[Memory Reset] No long-term memories found for topic ${topicId}`) @@ -947,7 +976,7 @@ export const resetLongTermMemoryAnalyzedMessageIds = async (topicId: string): Pr let hasChanges = false // 创建更新后的记忆数组 - const updatedMemories = state.memories.map(memory => { + const updatedMemories = state.memories.map((memory) => { // 只更新指定话题的记忆 if (memory.topicId === topicId && memory.analyzedMessageIds && memory.analyzedMessageIds.length > 0) { hasChanges = true @@ -972,9 +1001,13 @@ export const resetLongTermMemoryAnalyzedMessageIds = async (topicId: string): Pr }) // 保存更改到文件 - await store.dispatch(saveMemoryData({ - memories: updatedMemories - })).unwrap() + await store + .dispatch( + saveMemoryData({ + memories: updatedMemories + }) + ) + .unwrap() // 尝试获取话题的消息,以确保分析时能找到消息 try { @@ -1089,8 +1122,11 @@ export const analyzeAndAddShortMemories = async (topicId: string) => { console.log(`[Short Memory Analysis] Analyzing topic: ${topicId}`) console.log('[Short Memory Analysis] New conversation length:', newConversation.length) + // 获取当前的过滤敏感信息设置 + const filterSensitiveInfo = store.getState().memory?.filterSensitiveInfo ?? true + // 构建短期记忆分析提示词,包含已有记忆和新对话 - const prompt = ` + let prompt = ` 请对以下对话内容进行非常详细的分析和总结,提取对当前对话至关重要的上下文信息。请注意,这个分析将用于生成短期记忆,帮助AI理解当前对话的完整上下文。 分析要求: @@ -1101,7 +1137,22 @@ export const analyzeAndAddShortMemories = async (topicId: string) => { 5. 提取对理解当前对话上下文必不可少的信息 6. 记录用户提出的具体问题和关注点 7. 捕捉用户在对话中表达的偏好、困惑和反馈 -8. 记录对话中提到的文件、路径、变量名等具体技术细节 +8. 记录对话中提到的文件、路径、变量名等具体技术细节` + + // 如果启用了敏感信息过滤,添加相关指令 + if (filterSensitiveInfo) { + prompt += ` +9. 请注意不要提取任何敏感信息,包括但不限于: + - API密钥、访问令牌或其他凭证 + - 密码或密码提示 + - 私人联系方式(如电话号码、邮箱地址) + - 个人身份信息(如身份证号、社保号) + - 银行账户或支付信息 + - 私密的个人或商业信息 + 如果发现此类信息,请完全忽略,不要以任何形式记录或提取。` + } + + prompt += ` 与长期记忆不同,短期记忆应该非常详细地关注当前对话的具体细节和上下文。每条短期记忆应该是对对话片段的精准总结,确保不遗漏任何重要信息。 @@ -1173,7 +1224,7 @@ ${newConversation} let extractedLines: string[] = [] // 首先尝试匹配带有数字或短横线的列表项 - const listItemRegex = /(?:^|\n)(?:\d+\.\s*|\-\s*)(.+?)(?=\n\d+\.\s*|\n\-\s*|\n\n|$)/gs + const listItemRegex = /(?:^|\n)(?:\d+\.\s*|-\s*)(.+?)(?=\n\d+\.\s*|\n-\s*|\n\n|$)/gs let match: RegExpExecArray | null while ((match = listItemRegex.exec(result)) !== null) { if (match[1] && match[1].trim()) { @@ -1185,18 +1236,20 @@ ${newConversation} if (extractedLines.length === 0) { extractedLines = result .split('\n') - .map(line => line.trim()) - .filter(line => { + .map((line) => line.trim()) + .filter((line) => { // 过滤掉空行和非内容行(如标题、分隔符等) - return line && - !line.startsWith('#') && - !line.startsWith('---') && - !line.startsWith('===') && - !line.includes('没有找到新的重要信息') && - !line.includes('No new important information') + return ( + line && + !line.startsWith('#') && + !line.startsWith('---') && + !line.startsWith('===') && + !line.includes('没有找到新的重要信息') && + !line.includes('No new important information') + ) }) // 清理行首的数字、点和短横线 - .map(line => line.replace(/^(\d+\.\s*|\-\s*)/, '').trim()) + .map((line) => line.replace(/^(\d+\.\s*|-\s*)/, '').trim()) } console.log('[Short Memory Analysis] Extracted items:', extractedLines) @@ -1211,11 +1264,12 @@ ${newConversation} const newMemories = extractedLines.filter((content: string) => { const normalizedContent = content.toLowerCase() // 检查是否与现有记忆完全匹配或高度相似 - return !existingContents.some(existingContent => - existingContent === normalizedContent || - // 简单的相似度检查 - 如果一个字符串包含另一个的80%以上的内容 - (existingContent.includes(normalizedContent) && normalizedContent.length > existingContent.length * 0.8) || - (normalizedContent.includes(existingContent) && existingContent.length > normalizedContent.length * 0.8) + return !existingContents.some( + (existingContent) => + existingContent === normalizedContent || + // 简单的相似度检查 - 如果一个字符串包含另一个的80%以上的内容 + (existingContent.includes(normalizedContent) && normalizedContent.length > existingContent.length * 0.8) || + (normalizedContent.includes(existingContent) && existingContent.length > normalizedContent.length * 0.8) ) }) @@ -1254,12 +1308,16 @@ ${newConversation} // 显式触发保存操作,确保数据被持久化,并强制覆盖 try { const state = store.getState().memory - await store.dispatch(saveMemoryData({ - memoryLists: state.memoryLists, - memories: state.memories, - shortMemories: state.shortMemories, - forceOverwrite: true // 强制覆盖文件,确保数据正确保存 - })).unwrap() // 使用unwrap()来等待异步操作完成并处理错误 + await store + .dispatch( + saveMemoryData({ + memoryLists: state.memoryLists, + memories: state.memories, + shortMemories: state.shortMemories, + forceOverwrite: true // 强制覆盖文件,确保数据正确保存 + }) + ) + .unwrap() // 使用unwrap()来等待异步操作完成并处理错误 console.log('[Short Memory Analysis] Memory data saved successfully (force overwrite)') } catch (error) { console.error('[Short Memory Analysis] Failed to save memory data:', error) @@ -1324,16 +1382,16 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri // 处理上下文感知记忆推荐 if (contextualRecommendationEnabled && currentRecommendations && currentRecommendations.length > 0) { // 获取推荐记忆的详细信息 - const recommendedMemories: Array<{content: string, source: string, reason: string}> = [] + const recommendedMemories: Array<{ content: string; source: string; reason: string }> = [] // 处理每个推荐记忆 for (const recommendation of currentRecommendations) { // 根据来源查找记忆 let memory: any = null if (recommendation.source === 'long-term') { - memory = memories.find(m => m.id === recommendation.memoryId) + memory = memories.find((m) => m.id === recommendation.memoryId) } else if (recommendation.source === 'short-term') { - memory = shortMemories.find(m => m.id === recommendation.memoryId) + memory = shortMemories.find((m) => m.id === recommendation.memoryId) } if (memory) { @@ -1344,10 +1402,12 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri }) // 记录访问 - store.dispatch(accessMemory({ - id: memory.id, - isShortMemory: recommendation.source === 'short-term' - })) + store.dispatch( + accessMemory({ + id: memory.id, + isShortMemory: recommendation.source === 'short-term' + }) + ) } } @@ -1355,8 +1415,10 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri // 构建推荐记忆提示词 // 按重要性排序 recommendedMemories.sort((a, b) => { - const memoryA = memories.find(m => m.content === a.content) || shortMemories.find(m => m.content === a.content) - const memoryB = memories.find(m => m.content === b.content) || shortMemories.find(m => m.content === b.content) + const memoryA = + memories.find((m) => m.content === a.content) || shortMemories.find((m) => m.content === a.content) + const memoryB = + memories.find((m) => m.content === b.content) || shortMemories.find((m) => m.content === b.content) const importanceA = memoryA?.importance || 0.5 const importanceB = memoryB?.importance || 0.5 return importanceB - importanceA @@ -1383,7 +1445,7 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri // 如果启用了智能优先级管理,根据优先级排序 if (priorityManagementEnabled && topicShortMemories.length > 0) { // 计算每个记忆的综合分数(重要性 * 衰减因子 * 鲜度) - const scoredMemories = topicShortMemories.map(memory => { + const scoredMemories = topicShortMemories.map((memory) => { // 记录访问 store.dispatch(accessMemory({ id: memory.id, isShortMemory: true })) @@ -1399,7 +1461,7 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri scoredMemories.sort((a, b) => b.score - a.score) // 提取排序后的记忆 - topicShortMemories = scoredMemories.map(item => item.memory) + topicShortMemories = scoredMemories.map((item) => item.memory) // 限制数量,避免提示词过长 if (topicShortMemories.length > 10) { @@ -1437,7 +1499,7 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri // 如果启用了智能优先级管理,根据优先级排序 if (priorityManagementEnabled && activeMemories.length > 0) { // 计算每个记忆的综合分数 - const scoredMemories = activeMemories.map(memory => { + const scoredMemories = activeMemories.map((memory) => { // 记录访问 store.dispatch(accessMemory({ id: memory.id })) @@ -1457,9 +1519,9 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri const memoriesByList: Record = {} // 提取排序后的记忆 - const sortedMemories = scoredMemories.map(item => item.memory) + const sortedMemories = scoredMemories.map((item) => item.memory) - sortedMemories.forEach(memory => { + sortedMemories.forEach((memory) => { if (!memoriesByList[memory.listId]) { memoriesByList[memory.listId] = [] } diff --git a/src/renderer/src/services/MessagesService.ts b/src/renderer/src/services/MessagesService.ts index 14c024d997..c2dff59bd3 100644 --- a/src/renderer/src/services/MessagesService.ts +++ b/src/renderer/src/services/MessagesService.ts @@ -1,5 +1,6 @@ import SearchPopup from '@renderer/components/Popups/SearchPopup' import { DEFAULT_CONTEXTCOUNT } from '@renderer/config/constant' +import db from '@renderer/databases' import { getTopicById } from '@renderer/hooks/useTopic' import i18n from '@renderer/i18n' import { fetchMessagesSummary } from '@renderer/services/ApiService' @@ -200,6 +201,40 @@ export function getMessageModelId(message: Message) { return message?.model?.id || message.modelId } +/** + * 根据消息ID查找消息 + * @param messageId 消息ID + * @returns 找到的消息,如果未找到则返回null + */ +export async function findMessageById(messageId: string): Promise { + console.log(`[findMessageById] 正在查找消息ID: ${messageId}`) + + try { + // 获取所有话题 + const topics = await db.topics.toArray() + console.log(`[findMessageById] 找到 ${topics.length} 个话题`) + + // 遍历所有话题,查找消息 + for (const topic of topics) { + if (!topic.messages || topic.messages.length === 0) { + continue + } + + const message = topic.messages.find((msg) => msg.id === messageId) + if (message) { + console.log(`[findMessageById] 在话题 ${topic.id} 中找到消息`) + return message + } + } + + console.log(`[findMessageById] 未找到消息ID: ${messageId}`) + return null + } catch (error) { + console.error(`[findMessageById] 查找消息时出错:`, error) + return null + } +} + export function resetAssistantMessage(message: Message, model?: Model): Message { return { ...message, diff --git a/src/renderer/src/store/memory.ts b/src/renderer/src/store/memory.ts index 0cc365b34f..6abd9e5c6f 100644 --- a/src/renderer/src/store/memory.ts +++ b/src/renderer/src/store/memory.ts @@ -1,7 +1,6 @@ -import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' -import { nanoid } from 'nanoid' -import log from 'electron-log' +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit' import store, { RootState } from '@renderer/store' +import { nanoid } from 'nanoid' // 记忆列表接口 export interface MemoryList { @@ -93,6 +92,7 @@ export interface MemoryState { isActive: boolean // 记忆功能是否激活 shortMemoryActive: boolean // 短记忆功能是否激活 autoAnalyze: boolean // 是否自动分析 + filterSensitiveInfo: boolean // 是否过滤敏感信息 analyzeModel: string | null // 用于长期记忆分析的模型ID shortMemoryAnalyzeModel: string | null // 用于短期记忆分析的模型ID historicalContextAnalyzeModel: string | null // 用于历史对话上下文分析的模型ID @@ -148,6 +148,7 @@ const initialState: MemoryState = { isActive: true, shortMemoryActive: true, // 默认启用短记忆功能 autoAnalyze: true, + filterSensitiveInfo: true, // 默认启用敏感信息过滤 analyzeModel: 'gpt-3.5-turbo', // 设置默认长期记忆分析模型 shortMemoryAnalyzeModel: 'gpt-3.5-turbo', // 设置默认短期记忆分析模型 historicalContextAnalyzeModel: 'gpt-3.5-turbo', // 设置默认历史对话上下文分析模型 @@ -288,6 +289,11 @@ const memorySlice = createSlice({ state.autoAnalyze = action.payload }, + // 设置是否过滤敏感信息 + setFilterSensitiveInfo: (state, action: PayloadAction) => { + state.filterSensitiveInfo = action.payload + }, + // 设置长期记忆分析模型 setAnalyzeModel: (state, action: PayloadAction) => { state.analyzeModel = action.payload @@ -497,10 +503,12 @@ const memorySlice = createSlice({ const messageIdReferences = new Map() // 统计所有记忆中每个消息ID的引用次数 - state.shortMemories.forEach(memory => { - if (memory.id !== action.payload && memory.analyzedMessageIds) { // 排除要删除的记忆 - memory.analyzedMessageIds.forEach(msgId => { - if (messageIdsToCheck.has(msgId)) { // 只关注要删除的记忆中的消息ID + state.shortMemories.forEach((memory) => { + if (memory.id !== action.payload && memory.analyzedMessageIds) { + // 排除要删除的记忆 + memory.analyzedMessageIds.forEach((msgId) => { + if (messageIdsToCheck.has(msgId)) { + // 只关注要删除的记忆中的消息ID messageIdReferences.set(msgId, (messageIdReferences.get(msgId) || 0) + 1) } }) @@ -508,10 +516,12 @@ const memorySlice = createSlice({ }) // 找出没有被其他记忆引用的消息ID - const unusedMessageIds = Array.from(messageIdsToCheck).filter(msgId => !messageIdReferences.has(msgId)) + const unusedMessageIds = Array.from(messageIdsToCheck).filter((msgId) => !messageIdReferences.has(msgId)) if (unusedMessageIds.length > 0) { - console.log(`[Memory] Found ${unusedMessageIds.length} message IDs that are no longer referenced by any memory`) + console.log( + `[Memory] Found ${unusedMessageIds.length} message IDs that are no longer referenced by any memory` + ) // 将这些消息ID标记为未分析,以便下次分析时重新分析这些消息 // 注意:我们不需要显式地清除标记,因为分析逻辑会检查消息ID是否在任何记忆的analyzedMessageIds中 @@ -655,11 +665,11 @@ const memorySlice = createSlice({ // 更新长期记忆优先级 if (state.memories && state.memories.length > 0) { - state.memories.forEach(memory => { + state.memories.forEach((memory) => { // 计算时间衰减因子 if (state.decayEnabled && memory.lastAccessedAt) { const daysSinceLastAccess = (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60 * 24) - const decayFactor = Math.max(0, 1 - (daysSinceLastAccess * state.decayRate)) + const decayFactor = Math.max(0, 1 - daysSinceLastAccess * state.decayRate) memory.decayFactor = decayFactor } else { memory.decayFactor = 1 // 无衰减 @@ -673,20 +683,20 @@ const memorySlice = createSlice({ : daysSinceCreation // 鲜度评分结合创建时间和最后访问时间 - const creationFreshness = Math.max(0, 1 - (daysSinceCreation / 30)) // 30天内创建的记忆较新 - const accessFreshness = Math.max(0, 1 - (lastAccessDays / 7)) // 7天内访问的记忆较新 - memory.freshness = (creationFreshness * 0.3) + (accessFreshness * 0.7) // 加权平均 + const creationFreshness = Math.max(0, 1 - daysSinceCreation / 30) // 30天内创建的记忆较新 + const accessFreshness = Math.max(0, 1 - lastAccessDays / 7) // 7天内访问的记忆较新 + memory.freshness = creationFreshness * 0.3 + accessFreshness * 0.7 // 加权平均 } }) } // 更新短期记忆优先级 if (state.shortMemories && state.shortMemories.length > 0) { - state.shortMemories.forEach(memory => { + state.shortMemories.forEach((memory) => { // 计算时间衰减因子 if (state.decayEnabled && memory.lastAccessedAt) { const hoursSinceLastAccess = (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60) - const decayFactor = Math.max(0, 1 - (hoursSinceLastAccess * state.decayRate * 4)) // 短期记忆衰减更快 + const decayFactor = Math.max(0, 1 - hoursSinceLastAccess * state.decayRate * 4) // 短期记忆衰减更快 memory.decayFactor = decayFactor } else { memory.decayFactor = 1 // 无衰减 @@ -700,9 +710,9 @@ const memorySlice = createSlice({ : hoursSinceCreation // 短期记忆的鲜度评分更注重最近性 - const creationFreshness = Math.max(0, 1 - (hoursSinceCreation / 24)) // 24小时内创建的记忆较新 - const accessFreshness = Math.max(0, 1 - (lastAccessHours / 6)) // 6小时内访问的记忆较新 - memory.freshness = (creationFreshness * 0.2) + (accessFreshness * 0.8) // 加权平均,更注重访问时间 + const creationFreshness = Math.max(0, 1 - hoursSinceCreation / 24) // 24小时内创建的记忆较新 + const accessFreshness = Math.max(0, 1 - lastAccessHours / 6) // 6小时内访问的记忆较新 + memory.freshness = creationFreshness * 0.2 + accessFreshness * 0.8 // 加权平均,更注重访问时间 } }) } @@ -718,29 +728,29 @@ const memorySlice = createSlice({ // 更新长期记忆鲜度 if (state.memories && state.memories.length > 0) { - state.memories.forEach(memory => { + state.memories.forEach((memory) => { const daysSinceCreation = (now - new Date(memory.createdAt).getTime()) / (1000 * 60 * 60 * 24) const lastAccessDays = memory.lastAccessedAt ? (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60 * 24) : daysSinceCreation - const creationFreshness = Math.max(0, 1 - (daysSinceCreation / 30)) - const accessFreshness = Math.max(0, 1 - (lastAccessDays / 7)) - memory.freshness = (creationFreshness * 0.3) + (accessFreshness * 0.7) + const creationFreshness = Math.max(0, 1 - daysSinceCreation / 30) + const accessFreshness = Math.max(0, 1 - lastAccessDays / 7) + memory.freshness = creationFreshness * 0.3 + accessFreshness * 0.7 }) } // 更新短期记忆鲜度 if (state.shortMemories && state.shortMemories.length > 0) { - state.shortMemories.forEach(memory => { + state.shortMemories.forEach((memory) => { const hoursSinceCreation = (now - new Date(memory.createdAt).getTime()) / (1000 * 60 * 60) const lastAccessHours = memory.lastAccessedAt ? (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60) : hoursSinceCreation - const creationFreshness = Math.max(0, 1 - (hoursSinceCreation / 24)) - const accessFreshness = Math.max(0, 1 - (lastAccessHours / 6)) - memory.freshness = (creationFreshness * 0.2) + (accessFreshness * 0.8) + const creationFreshness = Math.max(0, 1 - hoursSinceCreation / 24) + const accessFreshness = Math.max(0, 1 - lastAccessHours / 6) + memory.freshness = creationFreshness * 0.2 + accessFreshness * 0.8 }) } }, @@ -752,14 +762,14 @@ const memorySlice = createSlice({ if (isShortMemory) { // 更新短期记忆访问信息 - const memory = state.shortMemories?.find(m => m.id === id) + const memory = state.shortMemories?.find((m) => m.id === id) if (memory) { memory.accessCount = (memory.accessCount || 0) + 1 memory.lastAccessedAt = now } } else { // 更新长期记忆访问信息 - const memory = state.memories?.find(m => m.id === id) + const memory = state.memories?.find((m) => m.id === id) if (memory) { memory.accessCount = (memory.accessCount || 0) + 1 memory.lastAccessedAt = now @@ -822,7 +832,7 @@ const memorySlice = createSlice({ console.log('[Memory Reducer] Loaded short memory analyze model:', action.payload.shortMemoryAnalyzeModel) } - log.info('Short-term memory data loaded into state') + console.log('Short-term memory data loaded into state') } }) .addCase(loadLongTermMemoryData.fulfilled, (state, action) => { @@ -840,7 +850,7 @@ const memorySlice = createSlice({ // 自动选择默认的记忆列表 if (!state.currentListId && state.memoryLists && state.memoryLists.length > 0) { // 先尝试找到一个isActive为true的列表 - const activeList = state.memoryLists.find(list => list.isActive) + const activeList = state.memoryLists.find((list) => list.isActive) if (activeList) { state.currentListId = activeList.id console.log('[Memory Reducer] Auto-selected active memory list:', activeList.name) @@ -851,15 +861,21 @@ const memorySlice = createSlice({ } } - log.info('Long-term memory data loaded into state') + console.log('Long-term memory data loaded into state') if (action.payload.historicalContextAnalyzeModel) { state.historicalContextAnalyzeModel = action.payload.historicalContextAnalyzeModel - console.log('[Memory Reducer] Loaded historical context analyze model:', action.payload.historicalContextAnalyzeModel) + console.log( + '[Memory Reducer] Loaded historical context analyze model:', + action.payload.historicalContextAnalyzeModel + ) } else { // 如果文件中没有historicalContextAnalyzeModel,使用shortMemoryAnalyzeModel或analyzeModel作为默认值 state.historicalContextAnalyzeModel = state.shortMemoryAnalyzeModel || state.analyzeModel - console.log('[Memory Reducer] Using default model for historical context:', state.historicalContextAnalyzeModel) + console.log( + '[Memory Reducer] Using default model for historical context:', + state.historicalContextAnalyzeModel + ) } if (action.payload.vectorizeModel) { @@ -867,7 +883,7 @@ const memorySlice = createSlice({ console.log('[Memory Reducer] Loaded vectorize model:', action.payload.vectorizeModel) } - log.info('Memory data loaded into state') + console.log('Memory data loaded into state') } }) } @@ -879,6 +895,7 @@ export const { editMemory, setMemoryActive, setAutoAnalyze, + setFilterSensitiveInfo, setAnalyzeModel, setShortMemoryAnalyzeModel, setHistoricalContextAnalyzeModel, @@ -972,8 +989,11 @@ export const saveMemoryData = createAsyncThunk( const completeData = { // 基本设置 isActive: memoryData.isActive !== undefined ? memoryData.isActive : state.isActive, - shortMemoryActive: memoryData.shortMemoryActive !== undefined ? memoryData.shortMemoryActive : state.shortMemoryActive, + shortMemoryActive: + memoryData.shortMemoryActive !== undefined ? memoryData.shortMemoryActive : state.shortMemoryActive, autoAnalyze: memoryData.autoAnalyze !== undefined ? memoryData.autoAnalyze : state.autoAnalyze, + filterSensitiveInfo: + memoryData.filterSensitiveInfo !== undefined ? memoryData.filterSensitiveInfo : state.filterSensitiveInfo, // 模型选择 analyzeModel: memoryData.analyzeModel || state.analyzeModel, @@ -987,26 +1007,47 @@ export const saveMemoryData = createAsyncThunk( currentListId: memoryData.currentListId || state.currentListId, // 自适应分析相关 - adaptiveAnalysisEnabled: memoryData.adaptiveAnalysisEnabled !== undefined ? memoryData.adaptiveAnalysisEnabled : state.adaptiveAnalysisEnabled, - analysisFrequency: memoryData.analysisFrequency !== undefined ? memoryData.analysisFrequency : state.analysisFrequency, + adaptiveAnalysisEnabled: + memoryData.adaptiveAnalysisEnabled !== undefined + ? memoryData.adaptiveAnalysisEnabled + : state.adaptiveAnalysisEnabled, + analysisFrequency: + memoryData.analysisFrequency !== undefined ? memoryData.analysisFrequency : state.analysisFrequency, analysisDepth: memoryData.analysisDepth || state.analysisDepth, // 用户关注点相关 - interestTrackingEnabled: memoryData.interestTrackingEnabled !== undefined ? memoryData.interestTrackingEnabled : state.interestTrackingEnabled, + interestTrackingEnabled: + memoryData.interestTrackingEnabled !== undefined + ? memoryData.interestTrackingEnabled + : state.interestTrackingEnabled, // 性能监控相关 - monitoringEnabled: memoryData.monitoringEnabled !== undefined ? memoryData.monitoringEnabled : state.monitoringEnabled, + monitoringEnabled: + memoryData.monitoringEnabled !== undefined ? memoryData.monitoringEnabled : state.monitoringEnabled, // 智能优先级与时效性管理相关 - priorityManagementEnabled: memoryData.priorityManagementEnabled !== undefined ? memoryData.priorityManagementEnabled : state.priorityManagementEnabled, + priorityManagementEnabled: + memoryData.priorityManagementEnabled !== undefined + ? memoryData.priorityManagementEnabled + : state.priorityManagementEnabled, decayEnabled: memoryData.decayEnabled !== undefined ? memoryData.decayEnabled : state.decayEnabled, - freshnessEnabled: memoryData.freshnessEnabled !== undefined ? memoryData.freshnessEnabled : state.freshnessEnabled, + freshnessEnabled: + memoryData.freshnessEnabled !== undefined ? memoryData.freshnessEnabled : state.freshnessEnabled, decayRate: memoryData.decayRate !== undefined ? memoryData.decayRate : state.decayRate, // 上下文感知记忆推荐相关 - contextualRecommendationEnabled: memoryData.contextualRecommendationEnabled !== undefined ? memoryData.contextualRecommendationEnabled : state.contextualRecommendationEnabled, - autoRecommendMemories: memoryData.autoRecommendMemories !== undefined ? memoryData.autoRecommendMemories : state.autoRecommendMemories, - recommendationThreshold: memoryData.recommendationThreshold !== undefined ? memoryData.recommendationThreshold : state.recommendationThreshold, + contextualRecommendationEnabled: + memoryData.contextualRecommendationEnabled !== undefined + ? memoryData.contextualRecommendationEnabled + : state.contextualRecommendationEnabled, + autoRecommendMemories: + memoryData.autoRecommendMemories !== undefined + ? memoryData.autoRecommendMemories + : state.autoRecommendMemories, + recommendationThreshold: + memoryData.recommendationThreshold !== undefined + ? memoryData.recommendationThreshold + : state.recommendationThreshold } const result = await window.api.memory.saveData(completeData, forceOverwrite) @@ -1020,20 +1061,17 @@ export const saveMemoryData = createAsyncThunk( ) // 加载长期记忆数据的异步 thunk -export const loadLongTermMemoryData = createAsyncThunk( - 'memory/loadLongTermData', - async () => { - try { - console.log('[Long-term Memory] Loading long-term memory data from file...') - const data = await window.api.memory.loadLongTermData() - console.log('[Long-term Memory] Long-term memory data loaded successfully') - return data - } catch (error) { - console.error('[Long-term Memory] Failed to load long-term memory data:', error) - return null - } +export const loadLongTermMemoryData = createAsyncThunk('memory/loadLongTermData', async () => { + try { + console.log('[Long-term Memory] Loading long-term memory data from file...') + const data = await window.api.memory.loadLongTermData() + console.log('[Long-term Memory] Long-term memory data loaded successfully') + return data + } catch (error) { + console.error('[Long-term Memory] Failed to load long-term memory data:', error) + return null } -) +}) // 保存长期记忆数据的异步 thunk export const saveLongTermMemoryData = createAsyncThunk( @@ -1060,6 +1098,8 @@ export const saveLongTermMemoryData = createAsyncThunk( // 基本设置 isActive: memoryData.isActive !== undefined ? memoryData.isActive : state.isActive, autoAnalyze: memoryData.autoAnalyze !== undefined ? memoryData.autoAnalyze : state.autoAnalyze, + filterSensitiveInfo: + memoryData.filterSensitiveInfo !== undefined ? memoryData.filterSensitiveInfo : state.filterSensitiveInfo, // 模型选择 analyzeModel: memoryData.analyzeModel || state.analyzeModel, @@ -1070,26 +1110,47 @@ export const saveLongTermMemoryData = createAsyncThunk( currentListId: memoryData.currentListId || state.currentListId, // 自适应分析相关 - adaptiveAnalysisEnabled: memoryData.adaptiveAnalysisEnabled !== undefined ? memoryData.adaptiveAnalysisEnabled : state.adaptiveAnalysisEnabled, - analysisFrequency: memoryData.analysisFrequency !== undefined ? memoryData.analysisFrequency : state.analysisFrequency, + adaptiveAnalysisEnabled: + memoryData.adaptiveAnalysisEnabled !== undefined + ? memoryData.adaptiveAnalysisEnabled + : state.adaptiveAnalysisEnabled, + analysisFrequency: + memoryData.analysisFrequency !== undefined ? memoryData.analysisFrequency : state.analysisFrequency, analysisDepth: memoryData.analysisDepth || state.analysisDepth, // 用户关注点相关 - interestTrackingEnabled: memoryData.interestTrackingEnabled !== undefined ? memoryData.interestTrackingEnabled : state.interestTrackingEnabled, + interestTrackingEnabled: + memoryData.interestTrackingEnabled !== undefined + ? memoryData.interestTrackingEnabled + : state.interestTrackingEnabled, // 性能监控相关 - monitoringEnabled: memoryData.monitoringEnabled !== undefined ? memoryData.monitoringEnabled : state.monitoringEnabled, + monitoringEnabled: + memoryData.monitoringEnabled !== undefined ? memoryData.monitoringEnabled : state.monitoringEnabled, // 智能优先级与时效性管理相关 - priorityManagementEnabled: memoryData.priorityManagementEnabled !== undefined ? memoryData.priorityManagementEnabled : state.priorityManagementEnabled, + priorityManagementEnabled: + memoryData.priorityManagementEnabled !== undefined + ? memoryData.priorityManagementEnabled + : state.priorityManagementEnabled, decayEnabled: memoryData.decayEnabled !== undefined ? memoryData.decayEnabled : state.decayEnabled, - freshnessEnabled: memoryData.freshnessEnabled !== undefined ? memoryData.freshnessEnabled : state.freshnessEnabled, + freshnessEnabled: + memoryData.freshnessEnabled !== undefined ? memoryData.freshnessEnabled : state.freshnessEnabled, decayRate: memoryData.decayRate !== undefined ? memoryData.decayRate : state.decayRate, // 上下文感知记忆推荐相关 - contextualRecommendationEnabled: memoryData.contextualRecommendationEnabled !== undefined ? memoryData.contextualRecommendationEnabled : state.contextualRecommendationEnabled, - autoRecommendMemories: memoryData.autoRecommendMemories !== undefined ? memoryData.autoRecommendMemories : state.autoRecommendMemories, - recommendationThreshold: memoryData.recommendationThreshold !== undefined ? memoryData.recommendationThreshold : state.recommendationThreshold, + contextualRecommendationEnabled: + memoryData.contextualRecommendationEnabled !== undefined + ? memoryData.contextualRecommendationEnabled + : state.contextualRecommendationEnabled, + autoRecommendMemories: + memoryData.autoRecommendMemories !== undefined + ? memoryData.autoRecommendMemories + : state.autoRecommendMemories, + recommendationThreshold: + memoryData.recommendationThreshold !== undefined + ? memoryData.recommendationThreshold + : state.recommendationThreshold } const result = await window.api.memory.saveLongTermData(completeData, forceOverwrite) @@ -1103,57 +1164,54 @@ export const saveLongTermMemoryData = createAsyncThunk( ) // 保存所有记忆设置的函数 -export const saveAllMemorySettings = createAsyncThunk( - 'memory/saveAllSettings', - async (_, { dispatch, getState }) => { - try { - const state = (getState() as RootState).memory +export const saveAllMemorySettings = createAsyncThunk('memory/saveAllSettings', async (_, { dispatch, getState }) => { + try { + const state = (getState() as RootState).memory - // 创建一个包含所有设置的对象,但不包含记忆内容和记忆列表 - const settings = { - // 基本设置 - isActive: state.isActive, - shortMemoryActive: state.shortMemoryActive, - autoAnalyze: state.autoAnalyze, + // 创建一个包含所有设置的对象,但不包含记忆内容和记忆列表 + const settings = { + // 基本设置 + isActive: state.isActive, + shortMemoryActive: state.shortMemoryActive, + autoAnalyze: state.autoAnalyze, - // 模型选择 - analyzeModel: state.analyzeModel, - shortMemoryAnalyzeModel: state.shortMemoryAnalyzeModel, - historicalContextAnalyzeModel: state.historicalContextAnalyzeModel, - vectorizeModel: state.vectorizeModel, + // 模型选择 + analyzeModel: state.analyzeModel, + shortMemoryAnalyzeModel: state.shortMemoryAnalyzeModel, + historicalContextAnalyzeModel: state.historicalContextAnalyzeModel, + vectorizeModel: state.vectorizeModel, - // 自适应分析相关 - adaptiveAnalysisEnabled: state.adaptiveAnalysisEnabled, - analysisFrequency: state.analysisFrequency, - analysisDepth: state.analysisDepth, + // 自适应分析相关 + adaptiveAnalysisEnabled: state.adaptiveAnalysisEnabled, + analysisFrequency: state.analysisFrequency, + analysisDepth: state.analysisDepth, - // 用户关注点相关 - interestTrackingEnabled: state.interestTrackingEnabled, + // 用户关注点相关 + interestTrackingEnabled: state.interestTrackingEnabled, - // 性能监控相关 - monitoringEnabled: state.monitoringEnabled, + // 性能监控相关 + monitoringEnabled: state.monitoringEnabled, - // 智能优先级与时效性管理相关 - priorityManagementEnabled: state.priorityManagementEnabled, - decayEnabled: state.decayEnabled, - freshnessEnabled: state.freshnessEnabled, - decayRate: state.decayRate, + // 智能优先级与时效性管理相关 + priorityManagementEnabled: state.priorityManagementEnabled, + decayEnabled: state.decayEnabled, + freshnessEnabled: state.freshnessEnabled, + decayRate: state.decayRate, - // 上下文感知记忆推荐相关 - contextualRecommendationEnabled: state.contextualRecommendationEnabled, - autoRecommendMemories: state.autoRecommendMemories, - recommendationThreshold: state.recommendationThreshold, - } - - const result = await dispatch(saveMemoryData(settings)).unwrap() - console.log('[Memory] All memory settings saved successfully') - return result - } catch (error) { - console.error('[Memory] Failed to save all memory settings:', error) - throw error + // 上下文感知记忆推荐相关 + contextualRecommendationEnabled: state.contextualRecommendationEnabled, + autoRecommendMemories: state.autoRecommendMemories, + recommendationThreshold: state.recommendationThreshold } + + const result = await dispatch(saveMemoryData(settings)).unwrap() + console.log('[Memory] All memory settings saved successfully') + return result + } catch (error) { + console.error('[Memory] Failed to save all memory settings:', error) + throw error } -) +}) // Middleware removed to prevent duplicate saves triggered by batch additions. // Explicit saves should be handled where needed, e.g., at the end of analysis functions. diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 49e06b4de3..ccd86625d0 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -71,6 +71,13 @@ export type Message = { useful?: boolean error?: Record enabledMCPs?: MCPServer[] + // 引用消息 + referencedMessages?: { + id: string + content: string + role: 'user' | 'assistant' + createdAt: string + }[] metadata?: { // Gemini groundingMetadata?: GroundingMetadata diff --git a/敏感信息过滤功能实现方案.txt b/敏感信息过滤功能实现方案.txt new file mode 100644 index 0000000000..277c4cb802 --- /dev/null +++ b/敏感信息过滤功能实现方案.txt @@ -0,0 +1,280 @@ +# 敏感信息过滤功能实现方案(修改版) + +## 需求分析 + +用户希望增加一个按钮,控制记忆功能是否过滤密钥等安全敏感信息。当开启过滤功能时,分析模型会过滤掉密钥等敏感信息;关闭则不过滤。此功能对于保护用户隐私和敏感数据至关重要。 + +## 实现思路 + +1. 在Redux状态中添加一个新的状态属性`filterSensitiveInfo` +2. 在设置界面中添加一个开关按钮,默认为开启状态 +3. 修改分析函数,根据`filterSensitiveInfo`状态添加过滤指令 +4. 添加日志记录,跟踪过滤状态的变化 + +## 修改文件 + +### 1. 修改 src/renderer/src/store/memory.ts + +```typescript +// 在 MemoryState 接口中添加 +export interface MemoryState { + // 其他属性... + filterSensitiveInfo: boolean // 是否过滤敏感信息 +} + +// 在 initialState 中添加 +const initialState: MemoryState = { + // 其他属性... + filterSensitiveInfo: true, // 默认启用敏感信息过滤 +} + +// 添加新的 action creator +setFilterSensitiveInfo: (state, action: PayloadAction) => { + state.filterSensitiveInfo = action.payload +}, + +// 导出 action +export const { + // 其他 actions... + setFilterSensitiveInfo, +} = memorySlice.actions + +// 修改 saveMemoryData 函数,确保 filterSensitiveInfo 设置也被保存 +const completeData = { + // 基本设置 + isActive: memoryData.isActive !== undefined ? memoryData.isActive : state.isActive, + shortMemoryActive: memoryData.shortMemoryActive !== undefined ? memoryData.shortMemoryActive : state.shortMemoryActive, + autoAnalyze: memoryData.autoAnalyze !== undefined ? memoryData.autoAnalyze : state.autoAnalyze, + filterSensitiveInfo: memoryData.filterSensitiveInfo !== undefined ? memoryData.filterSensitiveInfo : state.filterSensitiveInfo, + + // 其他属性... +} + +// 同样修改 saveLongTermMemoryData 函数 +const completeData = { + // 基本设置 + isActive: memoryData.isActive !== undefined ? memoryData.isActive : state.isActive, + autoAnalyze: memoryData.autoAnalyze !== undefined ? memoryData.autoAnalyze : state.autoAnalyze, + filterSensitiveInfo: memoryData.filterSensitiveInfo !== undefined ? memoryData.filterSensitiveInfo : state.filterSensitiveInfo, + + // 其他属性... +} +``` + +### 2. 修改 src/renderer/src/pages/settings/MemorySettings/index.tsx + +```typescript +// 导入 InfoCircleOutlined 图标 +import { + AppstoreOutlined, + DeleteOutlined, + EditOutlined, + InfoCircleOutlined, + PlusOutlined, + SearchOutlined, + UnorderedListOutlined +} from '@ant-design/icons' + +// 导入 setFilterSensitiveInfo action +import { + addMemory, + clearMemories, + deleteMemory, + editMemory, + setAnalyzeModel, + setAnalyzing, + setAutoAnalyze, + setFilterSensitiveInfo, + setMemoryActive, + setShortMemoryAnalyzeModel, + saveMemoryData, + saveLongTermMemoryData, + saveAllMemorySettings +} from '@renderer/store/memory' + +// 从 Redux 获取 filterSensitiveInfo 状态 +const filterSensitiveInfo = useAppSelector((state) => state.memory?.filterSensitiveInfo ?? true) // 默认启用敏感信息过滤 + +// 添加处理切换敏感信息过滤的函数 +const handleToggleFilterSensitiveInfo = async (checked: boolean) => { + dispatch(setFilterSensitiveInfo(checked)) + console.log('[Memory Settings] Filter sensitive info set:', checked) + + // 使用Redux Thunk保存到JSON文件 + try { + await dispatch(saveMemoryData({ filterSensitiveInfo: checked })).unwrap() + console.log('[Memory Settings] Filter sensitive info saved to file successfully:', checked) + } catch (error) { + console.error('[Memory Settings] Failed to save filter sensitive info to file:', error) + } +} + +// 在短期记忆设置中添加开关按钮 + + + {t('settings.memory.filterSensitiveInfo') || '过滤敏感信息'} + + + + + + + +// 在长期记忆设置中也添加相同的开关按钮 + + + {t('settings.memory.filterSensitiveInfo') || '过滤敏感信息'} + + + + + + +``` + +### 3. 修改 src/renderer/src/services/MemoryService.ts + +```typescript +// 修改 analyzeConversation 函数 +const analyzeConversation = async ( + conversation: string, + modelId: string, + customPrompt?: string +): Promise> => { + try { + // 获取当前的过滤敏感信息设置 + const filterSensitiveInfo = store.getState().memory?.filterSensitiveInfo ?? true + + // 使用自定义提示词或默认提示词 + let basePrompt = + customPrompt || + ` +请分析对话内容,提取出重要的用户偏好、习惯、需求和背景信息,这些信息在未来的对话中可能有用。 + +将每条信息分类并按以下格式返回: +类别: 信息内容 + +类别应该是以下几种之一: +- 用户偏好:用户喜好、喜欢的事物、风格等 +- 技术需求:用户的技术相关需求、开发偏好等 +- 个人信息:用户的背景、经历等个人信息 +- 交互偏好:用户喜欢的交流方式、沟通风格等 +- 其他:不属于以上类别的重要信息 + +请确保每条信息都是简洁、准确的。如果没有找到重要信息,请返回空字符串。 +` + + // 如果启用了敏感信息过滤,添加相关指令 + if (filterSensitiveInfo) { + basePrompt += ` +## 安全提示: +请注意不要提取任何敏感信息,包括但不限于: +- API密钥、访问令牌或其他凭证 +- 密码或密码提示 +- 私人联系方式(如电话号码、邮箱地址) +- 个人身份信息(如身份证号、社保号) +- 银行账户或支付信息 +- 私密的个人或商业信息 + +如果发现此类信息,请完全忽略,不要以任何形式记录或提取。 +` + } + + // 其余代码保持不变... + } +} + +// 修改 analyzeAndAddShortMemories 函数 +export const analyzeAndAddShortMemories = async (topicId: string) => { + // 其他代码... + + try { + // 获取当前的过滤敏感信息设置 + const filterSensitiveInfo = store.getState().memory?.filterSensitiveInfo ?? true + + // 构建短期记忆分析提示词 + let prompt = ` +请对以下对话内容进行非常详细的分析和总结,提取对当前对话至关重要的上下文信息。请注意,这个分析将用于生成短期记忆,帮助AI理解当前对话的完整上下文。 + +分析要求: +1. 非常详细地总结用户的每一句话中表达的关键信息、需求和意图 +2. 全面分析AI回复中的重要内容和对用户问题的解决方案 +3. 详细记录对话中的重要事实、数据、代码示例和具体细节 +4. 清晰捕捉对话的逻辑发展、转折点和关键决策 +5. 提取对理解当前对话上下文必不可少的信息 +6. 记录用户提出的具体问题和关注点 +7. 捕捉用户在对话中表达的偏好、困惑和反馈 +8. 记录对话中提到的文件、路径、变量名等具体技术细节 +` + + // 如果启用了敏感信息过滤,添加相关指令 + if (filterSensitiveInfo) { + prompt += ` +9. 请注意不要提取任何敏感信息,包括但不限于: + - API密钥、访问令牌或其他凭证 + - 密码或密码提示 + - 私人联系方式(如电话号码、邮箱地址) + - 个人身份信息(如身份证号、社保号) + - 银行账户或支付信息 + - 私密的个人或商业信息 + 如果发现此类信息,请完全忽略,不要以任何形式记录或提取。 +` + } + + // 其余代码保持不变... + } +} +``` + +### 4. 修改 src/renderer/src/i18n/locales/zh-cn.json 和 en-us.json + +```json +{ + "settings": { + "memory": { + "filterSensitiveInfo": "过滤敏感信息", + "filterSensitiveInfoTip": "启用后,记忆功能将不会提取API密钥、密码等敏感信息" + } + } +} +``` + +```json +{ + "settings": { + "memory": { + "filterSensitiveInfo": "Filter Sensitive Information", + "filterSensitiveInfoTip": "When enabled, memory function will not extract API keys, passwords, or other sensitive information" + } + } +} +``` + +## 实现效果 + +这些修改后,用户将能够通过开关按钮控制记忆功能是否过滤敏感信息: + +1. 当开启过滤功能时(默认状态),分析模型会被明确指示不要提取API密钥、密码等敏感信息 +2. 当关闭过滤功能时,分析模型会正常提取所有信息,包括可能的敏感信息 + +开关按钮会出现在短期记忆和长期记忆设置中,用户可以根据需要随时切换。设置会被保存到配置文件中,确保应用重启后设置仍然生效。 + +## 思考过程 + +1. **状态管理**:首先考虑如何在Redux中添加新的状态属性,并确保它能够被正确保存和加载。 + +2. **UI设计**:在设置界面中添加开关按钮,并提供提示信息,帮助用户理解这个功能的作用。 + +3. **提示词修改**:根据开关状态修改分析提示词,添加不要提取敏感信息的指令。这是实现过滤功能的核心部分。 + +4. **国际化支持**:添加相关的翻译键值对,确保功能在不同语言环境下都能正常使用。 + +5. **持久化**:确保设置能够被正确保存到配置文件中,并在应用重启后加载。 + +## 注意事项 + +1. 这个功能只能在一定程度上防止敏感信息被提取,但不能完全保证。如果用户在对话中明确提到了敏感信息,AI模型可能仍然会提取部分内容。 + +2. 建议用户在讨论敏感信息时,最好暂时关闭记忆功能,或者在对话中避免提及敏感信息。 + +3. 这个功能只影响新分析的对话内容,已经存储的记忆不会受到影响。如果用户想要清除可能包含敏感信息的记忆,需要手动删除这些记忆。