From 877beeab43a2877c6fbd2e553a29adb46eaba87b Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Tue, 15 Apr 2025 01:31:22 +0800 Subject: [PATCH] 111 --- check-duplicate-messages.js | 77 +++++++++ package.json | 2 +- src/main/ipc.ts | 11 -- src/preload/index.ts | 4 +- .../components/Popups/ShortMemoryPopup.tsx | 39 +++-- src/renderer/src/i18n/locales/ja-jp.json | 4 +- src/renderer/src/i18n/locales/ru-ru.json | 4 +- src/renderer/src/i18n/locales/zh-tw.json | 4 +- .../src/pages/home/Messages/Message.tsx | 66 +++----- .../pages/home/Messages/MessageContent.tsx | 6 - .../src/pages/home/Messages/Messages.tsx | 16 +- src/renderer/src/pages/home/Navbar.tsx | 36 +---- .../src/pages/home/Tabs/TopicsTab.tsx | 20 +++ .../src/pages/settings/SettingsPage.tsx | 18 +-- .../services/MemoryDeduplicationService.ts | 151 +++++++++++++++--- src/renderer/src/services/MemoryService.ts | 137 +++++++++++----- src/renderer/src/services/MessagesService.ts | 8 + src/renderer/src/store/messages.ts | 52 ++++-- yarn.lock | 8 +- 19 files changed, 450 insertions(+), 213 deletions(-) create mode 100644 check-duplicate-messages.js diff --git a/check-duplicate-messages.js b/check-duplicate-messages.js new file mode 100644 index 0000000000..243cf4caed --- /dev/null +++ b/check-duplicate-messages.js @@ -0,0 +1,77 @@ +// 检查重复消息的脚本 +const { app } = require('electron'); +const path = require('path'); +const fs = require('fs'); + +// 获取数据库文件路径 +const userDataPath = app.getPath('userData'); +const dbFilePath = path.join(userDataPath, 'CherryStudio.db'); + +console.log('数据库文件路径:', dbFilePath); + +// 检查文件是否存在 +if (fs.existsSync(dbFilePath)) { + console.log('数据库文件存在'); + + // 读取数据库内容 + const dbContent = fs.readFileSync(dbFilePath, 'utf8'); + + // 解析数据库内容 + try { + const data = JSON.parse(dbContent); + + // 检查topics表中的消息 + if (data.topics) { + console.log('找到topics表,共有', data.topics.length, '个主题'); + + // 遍历每个主题 + data.topics.forEach(topic => { + console.log(`检查主题: ${topic.id}`); + + if (topic.messages && Array.isArray(topic.messages)) { + console.log(` 主题消息数量: ${topic.messages.length}`); + + // 检查重复消息 + const messageIds = new Set(); + const duplicates = []; + + topic.messages.forEach(message => { + if (messageIds.has(message.id)) { + duplicates.push(message.id); + } else { + messageIds.add(message.id); + } + }); + + if (duplicates.length > 0) { + console.log(` 发现${duplicates.length}条重复消息ID:`, duplicates); + } else { + console.log(' 未发现重复消息ID'); + } + + // 检查重复的askId (对于助手消息) + const askIds = {}; + topic.messages.forEach(message => { + if (message.role === 'assistant' && message.askId) { + if (!askIds[message.askId]) { + askIds[message.askId] = []; + } + askIds[message.askId].push(message.id); + } + }); + + // 输出每个askId对应的助手消息数量 + Object.entries(askIds).forEach(([askId, messageIds]) => { + if (messageIds.length > 1) { + console.log(` askId ${askId} 有 ${messageIds.length} 条助手消息`); + } + }); + } + }); + } + } catch (error) { + console.error('解析数据库内容失败:', error); + } +} else { + console.log('数据库文件不存在'); +} diff --git a/package.json b/package.json index a44ee7dcf1..b71e95ca38 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "@types/react-dom": "^19.0.4", "@types/react-infinite-scroll-component": "^5.0.0", "@types/tinycolor2": "^1", - "@vitejs/plugin-react": "^4.2.1", + "@vitejs/plugin-react": "^4.3.4", "analytics": "^0.8.16", "antd": "^5.22.5", "applescript": "^1.0.0", diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 12494016f1..7ddf4b773d 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -22,11 +22,8 @@ import FileStorage from './services/FileStorage' import { GeminiService } from './services/GeminiService' import KnowledgeService from './services/KnowledgeService' import mcpService from './services/MCPService' -<<<<<<< HEAD import { memoryFileService } from './services/MemoryFileService' -======= import * as MsTTSService from './services/MsTTSService' ->>>>>>> origin/1600822305-patch-2 import * as NutstoreService from './services/NutstoreService' import ObsidianVaultService from './services/ObsidianVaultService' import { ProxyConfig, proxyManager } from './services/ProxyManager' @@ -310,7 +307,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ) // search window -<<<<<<< HEAD ipcMain.handle(IpcChannel.SearchWindow_Open, async (_, uid: string) => { await searchService.openSearchWindow(uid) }) @@ -337,12 +333,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.LongTermMemory_SaveData, async (_, data, forceOverwrite = false) => { return await memoryFileService.saveLongTermData(data, forceOverwrite) }) -======= - ipcMain.handle(IpcChannel.SearchWindow_Open, (_, uid: string) => searchService.openSearchWindow(uid)) - ipcMain.handle(IpcChannel.SearchWindow_Close, (_, uid: string) => searchService.closeSearchWindow(uid)) - ipcMain.handle(IpcChannel.SearchWindow_OpenUrl, (_, uid: string, url: string) => - searchService.openUrlInSearchWindow(uid, url) - ) // 注册ASR服务器IPC处理程序 asrServerService.registerIpcHandlers() @@ -352,5 +342,4 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.MsTTS_Synthesize, (_, text: string, voice: string, outputFormat: string) => MsTTSService.synthesize(text, voice, outputFormat) ) ->>>>>>> origin/1600822305-patch-2 } diff --git a/src/preload/index.ts b/src/preload/index.ts index f771f6de14..20bd1cf375 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -188,7 +188,6 @@ const api = { closeSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Close, uid), openUrlInSearchWindow: (uid: string, url: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_OpenUrl, uid, url) }, -<<<<<<< HEAD memory: { loadData: () => ipcRenderer.invoke(IpcChannel.Memory_LoadData), saveData: (data: any) => ipcRenderer.invoke(IpcChannel.Memory_SaveData, data), @@ -196,11 +195,10 @@ const api = { loadLongTermData: () => ipcRenderer.invoke(IpcChannel.LongTermMemory_LoadData), saveLongTermData: (data: any, forceOverwrite: boolean = false) => ipcRenderer.invoke(IpcChannel.LongTermMemory_SaveData, data, forceOverwrite) -======= + }, asrServer: { startServer: () => ipcRenderer.invoke(IpcChannel.Asr_StartServer), stopServer: (pid: number) => ipcRenderer.invoke(IpcChannel.Asr_StopServer, pid) ->>>>>>> origin/1600822305-patch-2 } } diff --git a/src/renderer/src/components/Popups/ShortMemoryPopup.tsx b/src/renderer/src/components/Popups/ShortMemoryPopup.tsx index 61e1be7dbb..31e94c230c 100644 --- a/src/renderer/src/components/Popups/ShortMemoryPopup.tsx +++ b/src/renderer/src/components/Popups/ShortMemoryPopup.tsx @@ -7,8 +7,9 @@ import store from '@renderer/store' import { deleteShortMemory } from '@renderer/store/memory' import { Button, Card, Col, Empty, Input, List, message, Modal, Row, Statistic, Tooltip } from 'antd' import _ from 'lodash' -import { useCallback, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { createSelector } from 'reselect' import styled from 'styled-components' // 不再需要确认对话框 @@ -36,13 +37,27 @@ const PopupContainer: React.FC = ({ topicId, resolve }) => { const dispatch = useAppDispatch() const [open, setOpen] = useState(true) + // 创建记忆选择器 - 使用createSelector进行记忆化 + const selectShortMemoriesByTopicId = useMemo( + () => + createSelector( + [(state) => state.memory?.shortMemories || [], (_state, topicId) => topicId], + (shortMemories, topicId) => { + return topicId ? shortMemories.filter((memory) => memory.topicId === topicId) : [] + } + ), + [] + ) + // 获取短记忆状态 const shortMemoryActive = useAppSelector((state) => state.memory?.shortMemoryActive || false) - const shortMemories = useAppSelector((state) => { - const allShortMemories = state.memory?.shortMemories || [] - // 只显示当前话题的短记忆 - return topicId ? allShortMemories.filter((memory) => memory.topicId === topicId) : [] - }) + const shortMemories = useAppSelector((state) => selectShortMemoriesByTopicId(state, topicId)) + + // 获取分析统计数据 + const totalAnalyses = useAppSelector((state) => state.memory?.analysisStats?.totalAnalyses || 0) + const successfulAnalyses = useAppSelector((state) => state.memory?.analysisStats?.successfulAnalyses || 0) + const successRate = totalAnalyses ? (successfulAnalyses / totalAnalyses) * 100 : 0 + const avgAnalysisTime = useAppSelector((state) => state.memory?.analysisStats?.averageAnalysisTime || 0) // 添加短记忆的状态 const [newMemoryContent, setNewMemoryContent] = useState('') @@ -185,20 +200,14 @@ const PopupContainer: React.FC = ({ topicId, resolve }) => { @@ -206,7 +215,7 @@ const PopupContainer: React.FC = ({ topicId, resolve }) => { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index eb4d9b1336..fa5d78ad20 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1395,7 +1395,6 @@ "title": "プライバシー設定", "enable_privacy_mode": "匿名エラーレポートとデータ統計の送信" }, -<<<<<<< HEAD "memory": { "title": "メモリー機能", "description": "AIアシスタントの長期メモリーを管理し、会話を自動分析して重要な情報を抽出します", @@ -1453,7 +1452,7 @@ "totalAnalyses": "分析回数合計", "successRate": "成功率", "avgAnalysisTime": "平均分析時間" -======= + }, "tts": { "title": "音声合成設定", "enable": "音声合成を有効にする", @@ -1618,7 +1617,6 @@ "asr_tts_info": "音声通話は上記の音声認識(ASR)と音声合成(TTS)の設定を使用します", "test": "音声通話テスト", "test_info": "入力ボックスの右側にある音声通話ボタンを使用してテストしてください" ->>>>>>> origin/1600822305-patch-2 } }, "translate": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index d62c806edd..f57372678b 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1398,7 +1398,6 @@ "title": "Настройки приватности", "enable_privacy_mode": "Анонимная отправка отчетов об ошибках и статистики" }, -<<<<<<< HEAD "memory": { "title": "[to be translated]:记忆功能", "description": "[to be translated]:管理AI助手的长期记忆,自动分析对话并提取重要信息", @@ -1452,7 +1451,7 @@ "confirmDelete": "[to be translated]:确认删除", "confirmDeleteContent": "[to be translated]:确定要删除这条短期记忆吗?", "delete": "[to be translated]:删除" -======= + }, "tts": { "title": "Настройки преобразования текста в речь", "enable": "Включить преобразование текста в речь", @@ -1617,7 +1616,6 @@ "test": "Тестировать голосовой вызов", "test_info": "Используйте кнопку голосового вызова справа от поля ввода для тестирования", "welcome_message": "Здравствуйте, я ваш ИИ-ассистент. Пожалуйста, нажмите и удерживайте кнопку разговора для начала диалога." ->>>>>>> origin/1600822305-patch-2 } }, "translate": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 4e4033fd5a..29e13aca0f 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1395,7 +1395,6 @@ "title": "隱私設定", "enable_privacy_mode": "匿名發送錯誤報告和資料統計" }, -<<<<<<< HEAD "memory": { "title": "記憶功能", "description": "管理AI助手的長期記憶,自動分析對話並提取重要信息", @@ -1453,7 +1452,7 @@ "totalAnalyses": "總分析次數", "successRate": "成功率", "avgAnalysisTime": "平均分析時間" -======= + }, "tts": { "title": "語音設定", "enable": "啟用語音合成", @@ -1618,7 +1617,6 @@ "test": "測試通話", "test_info": "請使用輸入框右側的語音通話按鈕進行測試", "welcome_message": "您好,我是您的AI助理,請長按說話按鈕進行對話。" ->>>>>>> origin/1600822305-patch-2 } }, "translate": { diff --git a/src/renderer/src/pages/home/Messages/Message.tsx b/src/renderer/src/pages/home/Messages/Message.tsx index 636fd50ab0..2726562bf6 100644 --- a/src/renderer/src/pages/home/Messages/Message.tsx +++ b/src/renderer/src/pages/home/Messages/Message.tsx @@ -327,8 +327,8 @@ const getContextMenuItems = ( t: (key: string) => string, selectedQuoteText: string, selectedText: string, -<<<<<<< HEAD - message: Message + message: Message, + currentMessage?: Message ): ItemType[] => { const items: ItemType[] = [] @@ -350,47 +350,31 @@ const getContextMenuItems = ( EventEmitter.emit(EVENT_NAMES.QUOTE_TEXT, selectedQuoteText) } }) -======= - currentMessage?: Message -) => [ - { - 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) - } - }, - { - key: 'speak', - label: '朗读', - onClick: () => { - // 从选中的文本开始朗读后面的内容 - if (selectedText && currentMessage?.content) { - // 找到选中文本在消息中的位置 - const startIndex = currentMessage.content.indexOf(selectedText) - if (startIndex !== -1) { - // 获取选中文本及其后面的所有内容 - const textToSpeak = currentMessage.content.substring(startIndex) - import('@renderer/services/TTSService').then(({ default: TTSService }) => { - TTSService.speak(textToSpeak) - }) - } else { - // 如果找不到精确位置,则只朗读选中的文本 - import('@renderer/services/TTSService').then(({ default: TTSService }) => { - TTSService.speak(selectedText) - }) + + // 添加朗读选项 + items.push({ + key: 'speak', + label: '朗读', + onClick: () => { + // 从选中的文本开始朗读后面的内容 + if (selectedText && currentMessage?.content) { + // 找到选中文本在消息中的位置 + const startIndex = currentMessage.content.indexOf(selectedText) + if (startIndex !== -1) { + // 获取选中文本及其后面的所有内容 + const textToSpeak = currentMessage.content.substring(startIndex) + import('@renderer/services/TTSService').then(({ default: TTSService }) => { + TTSService.speak(textToSpeak) + }) + } else { + // 如果找不到精确位置,则只朗读选中的文本 + import('@renderer/services/TTSService').then(({ default: TTSService }) => { + TTSService.speak(selectedText) + }) + } } } - } ->>>>>>> origin/1600822305-patch-2 + }) } // 添加复制消息ID选项,但不显示ID diff --git a/src/renderer/src/pages/home/Messages/MessageContent.tsx b/src/renderer/src/pages/home/Messages/MessageContent.tsx index 50f8c494f5..336023b5f8 100644 --- a/src/renderer/src/pages/home/Messages/MessageContent.tsx +++ b/src/renderer/src/pages/home/Messages/MessageContent.tsx @@ -222,7 +222,6 @@ const MessageContent: React.FC = ({ message: _message, model }) => { {message.mentions?.map((model) => {'@' + model.name})} -<<<<<<< HEAD {message.referencedMessages && message.referencedMessages.length > 0 && (
{message.referencedMessages.map((refMsg, index) => ( @@ -317,16 +316,11 @@ const MessageContent: React.FC = ({ message: _message, model }) => {
- -======= - - {isSegmentedPlayback ? ( ) : ( )} ->>>>>>> origin/1600822305-patch-2 {message.metadata?.generateImage && } {message.translatedContent && ( diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index b32eef90b5..f150fabdea 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -265,12 +265,20 @@ const computeDisplayMessages = (messages: Message[], startIndex: number, display const userIdSet = new Set() // 用户消息 id 集合 const assistantIdSet = new Set() // 助手消息 askId 集合 + const processedIds = new Set() // 用于跟踪已处理的消息ID const displayMessages: Message[] = [] // 处理单条消息的函数 const processMessage = (message: Message) => { if (!message) return + // 跳过已处理的消息ID + if (processedIds.has(message.id)) { + return + } + + processedIds.add(message.id) // 标记此消息ID为已处理 + const idSet = message.role === 'user' ? userIdSet : assistantIdSet const messageId = message.role === 'user' ? message.id : message.askId @@ -279,8 +287,12 @@ const computeDisplayMessages = (messages: Message[], startIndex: number, display displayMessages.push(message) return } - // 如果是相同 askId 的助手消息,也要显示 - displayMessages.push(message) + + // 如果是相同 askId 的助手消息,检查是否已经有相同ID的消息 + // 只有在没有相同ID的情况下才添加 + if (message.role === 'assistant' && !displayMessages.some(m => m.id === message.id)) { + displayMessages.push(message) + } } // 遍历消息直到满足显示数量要求 diff --git a/src/renderer/src/pages/home/Navbar.tsx b/src/renderer/src/pages/home/Navbar.tsx index b690ab17d5..a6a2b87912 100644 --- a/src/renderer/src/pages/home/Navbar.tsx +++ b/src/renderer/src/pages/home/Navbar.tsx @@ -1,4 +1,4 @@ -import { BookOutlined, FormOutlined, SearchOutlined } from '@ant-design/icons' +import { BookOutlined } from '@ant-design/icons' import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar' import { HStack } from '@renderer/components/Layout' import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover' @@ -11,11 +11,11 @@ import { useSettings } from '@renderer/hooks/useSettings' import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' -import { analyzeAndAddShortMemories } from '@renderer/services/MemoryService' + import { useAppDispatch } from '@renderer/store' import { setNarrowMode } from '@renderer/store/settings' import { Assistant, Topic } from '@renderer/types' -import { Button, Tooltip } from 'antd' +import { Tooltip } from 'antd' import { t } from 'i18next' import { LayoutGrid, MessageSquareDiff, PanelLeftClose, PanelRightClose, Search } from 'lucide-react' import { FC } from 'react' @@ -64,21 +64,7 @@ const HeaderNavbar: FC = ({ activeAssistant, activeTopic }) => { } } - const handleAnalyzeShortMemory = async () => { - if (activeTopic && activeTopic.id) { - try { - const result = await analyzeAndAddShortMemories(activeTopic.id) - if (result) { - window.message.success(t('settings.memory.shortMemoryAnalysisSuccess') || '分析成功') - } else { - window.message.info(t('settings.memory.shortMemoryAnalysisNoNew') || '无新信息') - } - } catch (error) { - console.error('Failed to analyze conversation for short memory:', error) - window.message.error(t('settings.memory.shortMemoryAnalysisError') || '分析失败') - } - } - } + return ( @@ -116,9 +102,6 @@ const HeaderNavbar: FC = ({ activeAssistant, activeTopic }) => { - - {t('settings.memory.analyzeConversation') || '分析对话'} - SearchPopup.show()}> @@ -189,17 +172,6 @@ const NarrowIcon = styled(NavbarIcon)` } ` -const AnalyzeButton = styled(Button)` - font-size: 12px; - height: 28px; - padding: 0 10px; - border-radius: 4px; - margin-right: 8px; - -webkit-app-region: none; - @media (max-width: 1000px) { - display: none; - } -` export default HeaderNavbar diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 967e7f4105..3e932a5df9 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -6,6 +6,7 @@ import { FolderOutlined, PushpinOutlined, QuestionCircleOutlined, + SearchOutlined, UploadOutlined } from '@ant-design/icons' import DragableList from '@renderer/components/DragableList' @@ -20,6 +21,7 @@ import { useSettings } from '@renderer/hooks/useSettings' import { TopicManager } from '@renderer/hooks/useTopic' import { fetchMessagesSummary } from '@renderer/services/ApiService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' +import { analyzeAndAddShortMemories } from '@renderer/services/MemoryService' import store from '@renderer/store' import { RootState } from '@renderer/store' import { setGenerating } from '@renderer/store/runtime' @@ -238,6 +240,24 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic }) } }, + { + label: t('settings.memory.analyzeConversation') || '分析对话', + key: 'analyze-conversation', + icon: , + async onClick() { + try { + const result = await analyzeAndAddShortMemories(topic.id) + if (result) { + window.message.success(t('settings.memory.shortMemoryAnalysisSuccess') || '分析成功') + } else { + window.message.info(t('settings.memory.shortMemoryAnalysisNoNew') || '无新信息') + } + } catch (error) { + console.error('Failed to analyze conversation for short memory:', error) + window.message.error(t('settings.memory.shortMemoryAnalysisError') || '分析失败') + } + } + }, { label: t('chat.topics.copy.title'), key: 'copy', diff --git a/src/renderer/src/pages/settings/SettingsPage.tsx b/src/renderer/src/pages/settings/SettingsPage.tsx index 8a6128cf6d..cacf8a4311 100644 --- a/src/renderer/src/pages/settings/SettingsPage.tsx +++ b/src/renderer/src/pages/settings/SettingsPage.tsx @@ -1,17 +1,4 @@ -import { - AppstoreOutlined, - CloudOutlined, - CodeOutlined, - ExperimentOutlined, - GlobalOutlined, - InfoCircleOutlined, - LayoutOutlined, - MacCommandOutlined, - RocketOutlined, - SaveOutlined, - SettingOutlined, - ThunderboltOutlined -} from '@ant-design/icons' +import { ExperimentOutlined } from '@ant-design/icons' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon' import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings' @@ -160,12 +147,9 @@ const SettingsPage: FC = () => { } /> } /> } /> -<<<<<<< HEAD } /> } /> -======= } /> ->>>>>>> origin/1600822305-patch-2 } /> {showMiniAppSettings && } />} } /> diff --git a/src/renderer/src/services/MemoryDeduplicationService.ts b/src/renderer/src/services/MemoryDeduplicationService.ts index f72bd61359..7f9e33afb9 100644 --- a/src/renderer/src/services/MemoryDeduplicationService.ts +++ b/src/renderer/src/services/MemoryDeduplicationService.ts @@ -125,6 +125,14 @@ ${memoriesToCheck} console.log('[Memory Deduplication] Analysis result:', result) + // 输出更详细的日志信息以便调试 + console.log('[Memory Deduplication] Attempting to parse result with format:', { + hasGroupSection: result.includes('识别出的相似组'), + hasIndependentSection: result.includes('独立记忆项'), + containsGroupKeyword: result.includes('组'), + resultLength: result.length + }) + // 解析结果 const similarGroups: DeduplicationResult['similarGroups'] = [] const independentMemories: string[] = [] @@ -144,38 +152,139 @@ ${memoriesToCheck} if (similarGroupsMatch && similarGroupsMatch[1]) { const groupsText = similarGroupsMatch[1].trim() // 更新正则表达式以匹配新的格式,包括重要性和关键词 - const groupRegex = + // 输出原始文本以便调试 + console.log('[Memory Deduplication] Group text to parse:', groupsText) + + // 原始正则表达式 + const originalGroupRegex = /-\s*组(\d+)?:\s*\[([\d,\s]+)\]\s*-\s*合并建议:\s*"([^"]+)"\s*-\s*分类:\s*"([^"]+)"\s*(?:-\s*重要性:\s*"([^"]+)")?\s*(?:-\s*关键词:\s*"([^"]+)")?/g - let match: RegExpExecArray | null - while ((match = groupRegex.exec(groupsText)) !== null) { - const groupId = match[1] || String(similarGroups.length + 1) - const memoryIndices = match[2].split(',').map((s: string) => s.trim()) - const mergedContent = match[3].trim() - const category = match[4]?.trim() - const importance = match[5] ? parseFloat(match[5].trim()) : undefined - const keywords = match[6] - ? match[6] - .trim() - .split(',') - .map((k: string) => k.trim()) - : undefined + // 新增正则表达式,匹配AI返回的不同格式 + const alternativeGroupRegex = /-\s*组(\d+)?:\s*(?:\*\*)?["\[]?([\d,\s]+)["\]]?(?:\*\*)?\s*-\s*合并建议:\s*(?:\*\*)?["']?([^"'\n-]+)["']?(?:\*\*)?\s*-\s*分类:\s*(?:\*\*)?["']?([^"'\n-]+)["']?(?:\*\*)?/g + + // 简化的正则表达式,直接匹配组号和方括号内的数字 + const simpleGroupRegex = /-\s*组(\d+)?:\s*\[([\d,\s]+)\]\s*-\s*合并建议:\s*(.+?)\s*-\s*分类:\s*(.+?)(?=\s*$|\s*-\s*组|\s*\n)/gm + + // 尝试所有正则表达式 + const regexesToTry = [simpleGroupRegex, alternativeGroupRegex, originalGroupRegex] + + // 逐个尝试正则表达式 + for (const regex of regexesToTry) { + let match: RegExpExecArray | null + let found = false + + // 重置正则表达式的lastIndex + regex.lastIndex = 0 + + while ((match = regex.exec(groupsText)) !== null) { + found = true + const groupId = match[1] || String(similarGroups.length + 1) + // 清理引号和方括号 + const memoryIndicesStr = match[2].replace(/["'\[\]]/g, '') + const memoryIndices = memoryIndicesStr.split(',').map((s: string) => s.trim()) + const mergedContent = match[3].trim().replace(/^["']|["']$/g, '') // 移除首尾的引号 + const category = match[4]?.trim().replace(/^["']|["']$/g, '') // 移除首尾的引号 + + console.log(`[Memory Deduplication] Found group with regex ${regex.toString().substring(0, 30)}...`, { + groupId, + memoryIndices, + mergedContent, + category + }) + + similarGroups.push({ + groupId, + memoryIds: memoryIndices, + mergedContent, + category: category || '其他' + }) + } + + // 如果找到了匹配项,就不再尝试其他正则表达式 + if (found) break; + } + + // 旧的解析代码已被上面的新代码替代 + } + + // 解析独立记忆项 + console.log('[Memory Deduplication] Attempting to parse independent memories') + + // 尝试多种正则表达式匹配独立记忆项 + const independentRegexes = [ + /2\.\s*独立记忆项:\s*(?:\*\*)?\s*\[?([\d,\s"]+)\]?(?:\*\*)?/i, + /2\.\s*独立记忆项\s*:\s*([\d,\s]+)/i, + /独立记忆项\s*:\s*([\d,\s]+)/i + ] + + let independentFound = false + + for (const regex of independentRegexes) { + const independentMatch = result.match(regex) + if (independentMatch && independentMatch[1]) { + // 处理可能包含引号的情况 + const cleanedIndependentStr = independentMatch[1].replace(/["'\[\]]/g, '') + const items = cleanedIndependentStr.split(',').map((s: string) => s.trim()) + + console.log(`[Memory Deduplication] Found independent memories with regex ${regex.toString().substring(0, 30)}...`, items) + + independentMemories.push(...items) + independentFound = true + break + } + } + + // 如果没有找到独立记忆项,尝试直接从结果中提取数字 + if (!independentFound) { + // 尝试直接提取数字 + const numberMatches = result.match(/\b(\d+)\b/g) + if (numberMatches) { + // 过滤出不在相似组中的数字 + const usedIndices = new Set() + similarGroups.forEach(group => { + group.memoryIds.forEach(id => usedIndices.add(id)) + }) + + const unusedIndices = numberMatches.filter(num => !usedIndices.has(num)) + if (unusedIndices.length > 0) { + console.log('[Memory Deduplication] Extracted independent memories from numbers in result:', unusedIndices) + independentMemories.push(...unusedIndices) + } + } + } + + // 如果没有解析到相似组和独立记忆项,但结果中包含“组”字样,尝试使用更宽松的正则表达式 + if (similarGroups.length === 0 && independentMemories.length === 0 && result.includes('组')) { + // 尝试使用更宽松的正则表达式提取组信息 + const looseGroupRegex = /-\s*组\s*(\d+)?\s*:\s*["\[]?\s*([\d,\s"]+)\s*["\]]?\s*-\s*合并建议\s*:\s*["']?([^"'\n-]+)["']?/g + + let looseMatch: RegExpExecArray | null + while ((looseMatch = looseGroupRegex.exec(result)) !== null) { + const groupId = looseMatch[1] || String(similarGroups.length + 1) + // 清理引号和方括号 + const memoryIndicesStr = looseMatch[2].replace(/["'\[\]]/g, '') + const memoryIndices = memoryIndicesStr.split(',').map((s: string) => s.trim()) + const mergedContent = looseMatch[3].trim() similarGroups.push({ groupId, memoryIds: memoryIndices, mergedContent, - category, - importance, - keywords + category: '其他' // 默认类别 }) } } - // 解析独立记忆项 - const independentMatch = result.match(/2\.\s*独立记忆项:\s*\[([\d,\s]+)\]/i) - if (independentMatch && independentMatch[1]) { - independentMemories.push(...independentMatch[1].split(',').map((s: string) => s.trim())) + // 如果仍然没有解析到任何内容,尝试直接从原始结果中提取信息 + if (similarGroups.length === 0 && independentMemories.length === 0) { + console.log('[Memory Deduplication] No groups or independent memories found, attempting direct extraction') + + // 尝试提取所有数字作为独立记忆项 + const allNumbers = result.match(/\b(\d+)\b/g) + if (allNumbers && allNumbers.length > 0) { + console.log('[Memory Deduplication] Extracted all numbers as independent memories:', allNumbers) + independentMemories.push(...allNumbers) + } } console.log('[Memory Deduplication] Parsed result:', { similarGroups, independentMemories }) diff --git a/src/renderer/src/services/MemoryService.ts b/src/renderer/src/services/MemoryService.ts index 4821931c2f..75d738fb3d 100644 --- a/src/renderer/src/services/MemoryService.ts +++ b/src/renderer/src/services/MemoryService.ts @@ -110,19 +110,39 @@ const analyzeConversation = async ( let basePrompt = customPrompt || ` -请分析对话内容,提取出重要的用户偏好、习惯、需求和背景信息,这些信息在未来的对话中可能有用。 +你是一个专业的对话分析专家,负责从对话中提取关键信息,形成精准的长期记忆。 -将每条信息分类并按以下格式返回: +## 输出格式要求(非常重要): +你必须严格按照以下格式输出每条提取的信息: 类别: 信息内容 -类别应该是以下几种之一: -- 用户偏好:用户喜好、喜欢的事物、风格等 -- 技术需求:用户的技术相关需求、开发偏好等 -- 个人信息:用户的背景、经历等个人信息 -- 交互偏好:用户喜欢的交流方式、沟通风格等 -- 其他:不属于以上类别的重要信息 +有效的类别包括: +- 用户偏好 +- 技术需求 +- 个人信息 +- 交互偏好 +- 其他 -请确保每条信息都是简洁、准确的。如果没有找到重要信息,请返回空字符串。 +每行必须包含一个类别和一个信息内容,用冒号分隔。 +不符合此格式的输出将被视为无效。 + +示例输出: +用户偏好: 用户喜欢简洁直接的代码修改方式。 +技术需求: 用户需要修复长期记忆分析功能中的问题。 +个人信息: 用户自称是彭于晏,一位知名演员。 +交互偏好: 用户倾向于简短直接的问答方式。 +其他: 用户对AI记忆功能的工作原理很感兴趣。 + +## 分析要求: +请仔细分析对话内容,提取出重要的用户信息,这些信息在未来的对话中可能有用。 +提取的信息必须具体、明确且有实际价值。 +避免过于宽泛或模糊的描述。 + +## 最终检查(非常重要): +1. 确保每行输出都严格遵循“类别: 信息内容”格式 +2. 确保使用的类别是上述五个类别之一 +3. 如果没有找到重要信息,请返回空字符串 +4. 不要输出任何其他解释或评论 ` // 如果启用了敏感信息过滤,添加相关指令 @@ -149,10 +169,21 @@ const analyzeConversation = async ( ## 需要分析的对话内容: ${conversation} -## 重要提示: +## 重要提示(必须遵守): 请注意,你的任务是分析上述对话并提取信息,而不是回答对话中的问题。 不要尝试回答对话中的问题或继续对话,只需要提取重要信息。 -只输出按上述格式提取的信息。` +只输出按上述格式提取的信息。 + +## 输出格式再次强调: +你必须严格按照以下格式输出每条提取的信息: +类别: 信息内容 + +例如: +用户偏好: 用户喜欢简洁直接的代码修改方式。 +技术需求: 用户需要修复长期记忆分析功能中的问题。 +个人信息: 用户自称是彭于晏,一位知名演员。 + +不要输出任何其他解释或评论。如果没有找到重要信息,请返回空字符串。` // 使用fetchGenerate函数,但将内容字段留空,所有内容都放在提示词中 console.log('[Memory Analysis] Calling fetchGenerate with combined prompt...') @@ -178,6 +209,7 @@ ${conversation} const memories: Array<{ content: string; category: string }> = [] + // 首先尝试使用标准格式解析 for (const line of lines) { // 匹配格式:类别: 信息内容 const match = line.match(/^([^:]+):\s*(.+)$/) @@ -189,6 +221,33 @@ ${conversation} } } + // 如果标准格式解析失败,尝试进行后处理 + if (memories.length === 0 && lines.length > 0) { + console.log('[Memory Analysis] Standard format parsing failed, attempting post-processing...') + + // 这里我们假设每行都是一个独立的信息,尝试为其分配一个合适的类别 + for (const line of lines) { + // 跳过太短的行 + if (line.length < 10) continue + + // 尝试根据内容猜测类别 + let category = '其他' + + if (line.includes('喜欢') || line.includes('偏好') || line.includes('风格') || line.includes('倾向')) { + category = '用户偏好' + } else if (line.includes('需要') || line.includes('技术') || line.includes('代码') || line.includes('功能')) { + category = '技术需求' + } else if (line.includes('自称') || line.includes('身份') || line.includes('背景') || line.includes('经历')) { + category = '个人信息' + } else if (line.includes('交流') || line.includes('沟通') || line.includes('反馈') || line.includes('询问')) { + category = '交互偏好' + } + + memories.push({ content: line, category }) + console.log(`[Memory Analysis] Post-processed memory: ${category}: ${line}`) + } + } + return memories } catch (error) { console.error('Failed to analyze conversation with real AI:', error) @@ -474,41 +533,45 @@ export const useMemoryService = () => { const basePrompt = ` 你是一个专业的对话分析专家,负责从对话中提取关键信息,形成精准的长期记忆。 -## 重要提示: -请注意,你的任务是分析对话并提取信息,而不是回答对话中的问题。不要尝试回答对话中的问题或继续对话,只需要提取重要信息。 - -## 分析要求: -请仔细分析对话内容,提取出重要的用户偏好、习惯、需求和背景信息,这些信息在未来的对话中可能有用。 - -1. 提取的信息必须是具体、明确且有实际价值的 -2. 每条信息应该是完整的句子,表达清晰的一个要点 -3. 避免过于宽泛或模糊的描述 -4. 确保信息准确反映对话内容,不要过度推断 -5. 提取的信息应该对未来的对话有帮助 - -## 输出格式: -将每条信息分类并严格按以下格式返回: +## 输出格式要求(非常重要): +你必须严格按照以下格式输出每条提取的信息: 类别: 信息内容 -例如: +有效的类别包括: +- 用户偏好 +- 技术需求 +- 个人信息 +- 交互偏好 +- 其他 + +每行必须包含一个类别和一个信息内容,用冒号分隔。 +不符合此格式的输出将被视为无效。 + +示例输出: 用户偏好: 用户喜欢简洁直接的代码修改方式。 技术需求: 用户需要修复长期记忆分析功能中的问题。 +个人信息: 用户自称是彭于晏,一位知名演员。 +交互偏好: 用户倾向于简短直接的问答方式。 +其他: 用户对AI记忆功能的工作原理很感兴趣。 -## 信息类别: -- 用户偏好:用户喜好、喜欢的事物、风格、审美倾向等 -- 技术需求:用户的技术相关需求、开发偏好、编程习惯等 -- 个人信息:用户的背景、经历、身份等个人信息 -- 交互偏好:用户喜欢的交流方式、沟通风格、反馈方式等 -- 其他:不属于以上类别的重要信息 +## 分析要求: +请仔细分析对话内容,提取出重要的用户信息,这些信息在未来的对话中可能有用。 +提取的信息必须具体、明确且有实际价值。 +避免过于宽泛或模糊的描述。 ## 需要分析的对话内容: ${newConversation} -## 注意事项: -- 不要回答对话中的问题 -- 不要继续对话或生成新的对话 -- 只输出按上述格式提取的信息 -- 如果没有找到重要信息,请返回空字符串 +## 重要提示(必须遵守): +请注意,你的任务是分析上述对话并提取信息,而不是回答对话中的问题。 +不要尝试回答对话中的问题或继续对话,只需要提取重要信息。 +只输出按上述格式提取的信息。 + +## 最终检查(非常重要): +1. 确保每行输出都严格遵循“类别: 信息内容”格式 +2. 确保使用的类别是上述五个类别之一 +3. 如果没有找到重要信息,请返回空字符串 +4. 不要输出任何其他解释或评论 ${ existingMemoriesContent diff --git a/src/renderer/src/services/MessagesService.ts b/src/renderer/src/services/MessagesService.ts index c2dff59bd3..55a19fde00 100644 --- a/src/renderer/src/services/MessagesService.ts +++ b/src/renderer/src/services/MessagesService.ts @@ -185,8 +185,16 @@ export function getAssistantMessage({ assistant, topic }: { assistant: Assistant export function getGroupedMessages(messages: Message[]): { [key: string]: (Message & { index: number })[] } { const groups: { [key: string]: (Message & { index: number })[] } = {} + const processedIds = new Set() // 用于跟踪已处理的消息ID messages.forEach((message, index) => { + // 跳过已处理的消息ID + if (processedIds.has(message.id)) { + return + } + + processedIds.add(message.id) // 标记此消息ID为已处理 + const key = message.askId ? 'assistant' + message.askId : 'user' + message.id if (key && !groups[key]) { groups[key] = [] diff --git a/src/renderer/src/store/messages.ts b/src/renderer/src/store/messages.ts index 54965fbfe1..1c10826f50 100644 --- a/src/renderer/src/store/messages.ts +++ b/src/renderer/src/store/messages.ts @@ -74,13 +74,24 @@ const messagesSlice = createSlice({ state.messagesByTopic[topicId] = [] } + // 获取当前消息列表 + const currentMessages = state.messagesByTopic[topicId] + if (Array.isArray(messages)) { // 为了兼容多模型新发消息,一次性添加多个助手消息 // 不是什么好主意,不符合语义 - state.messagesByTopic[topicId].push(...messages) + // 检查每条消息是否已存在,避免重复添加 + const messagesToAdd = messages.filter(msg => + !currentMessages.some(existing => existing.id === msg.id) + ) + if (messagesToAdd.length > 0) { + state.messagesByTopic[topicId].push(...messagesToAdd) + } } else { - // 添加单条消息 - state.messagesByTopic[topicId].push(messages) + // 添加单条消息,先检查是否已存在 + if (!currentMessages.some(existing => existing.id === messages.id)) { + state.messagesByTopic[topicId].push(messages) + } } }, appendMessage: ( @@ -96,15 +107,23 @@ const messagesSlice = createSlice({ // 确保消息数组存在并且拿到引用 const messagesList = state.messagesByTopic[topicId] - // 要插入的消息 + // 要插入的消息,先过滤掉已存在的消息 const messagesToInsert = Array.isArray(messages) ? messages : [messages] + const uniqueMessagesToInsert = messagesToInsert.filter(msg => + !messagesList.some(existing => existing.id === msg.id) + ) + + // 如果没有新消息需要插入,直接返回 + if (uniqueMessagesToInsert.length === 0) { + return + } if (position !== undefined && position >= 0 && position <= messagesList.length) { // 如果指定了位置,在特定位置插入消息 - messagesList.splice(position, 0, ...messagesToInsert) + messagesList.splice(position, 0, ...uniqueMessagesToInsert) } else { // 否则默认添加到末尾 - messagesList.push(...messagesToInsert) + messagesList.push(...uniqueMessagesToInsert) } }, updateMessage: ( @@ -158,16 +177,25 @@ const messagesSlice = createSlice({ } // 尝试找到现有消息 - const existingMessage = state.messagesByTopic[topicId].find( + const existingMessageIndex = state.messagesByTopic[topicId].findIndex( (m) => m.role === 'assistant' && m.id === streamMessage.id ) - if (existingMessage) { - // 更新 - Object.assign(existingMessage, streamMessage) + if (existingMessageIndex !== -1) { + // 更新现有消息 + Object.assign(state.messagesByTopic[topicId][existingMessageIndex], streamMessage) } else { - // 添加新消息 - state.messagesByTopic[topicId].push(streamMessage) + // 检查是否有重复的消息(相同的askId和内容) + const duplicateMessage = state.messagesByTopic[topicId].find( + (m) => m.role === 'assistant' && + m.askId === streamMessage.askId && + m.content === streamMessage.content + ) + + // 只有在没有重复消息的情况下才添加新消息 + if (!duplicateMessage) { + state.messagesByTopic[topicId].push(streamMessage) + } } // 删除流状态 diff --git a/yarn.lock b/yarn.lock index f4791ec8b6..635a63a0b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4126,7 +4126,7 @@ __metadata: languageName: node linkType: hard -"@vitejs/plugin-react@npm:^4.2.1": +"@vitejs/plugin-react@npm:^4.3.4": version: 4.3.4 resolution: "@vitejs/plugin-react@npm:4.3.4" dependencies: @@ -4233,7 +4233,7 @@ __metadata: "@types/react-dom": "npm:^19.0.4" "@types/react-infinite-scroll-component": "npm:^5.0.0" "@types/tinycolor2": "npm:^1" - "@vitejs/plugin-react": "npm:^4.2.1" + "@vitejs/plugin-react": "npm:^4.3.4" "@xyflow/react": "npm:^12.4.4" adm-zip: "npm:^0.5.16" analytics: "npm:^0.8.16" @@ -15620,7 +15620,6 @@ __metadata: languageName: node linkType: hard -<<<<<<< HEAD "rw@npm:1": version: 1.3.3 resolution: "rw@npm:1.3.3" @@ -15628,10 +15627,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": -======= "safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": ->>>>>>> origin/1600822305-patch-2 version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3