This commit is contained in:
1600822305 2025-04-15 01:31:22 +08:00
parent 227dc01c85
commit 877beeab43
19 changed files with 450 additions and 213 deletions

View File

@ -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('数据库文件不存在');
}

View File

@ -137,7 +137,7 @@
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@types/react-infinite-scroll-component": "^5.0.0", "@types/react-infinite-scroll-component": "^5.0.0",
"@types/tinycolor2": "^1", "@types/tinycolor2": "^1",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.3.4",
"analytics": "^0.8.16", "analytics": "^0.8.16",
"antd": "^5.22.5", "antd": "^5.22.5",
"applescript": "^1.0.0", "applescript": "^1.0.0",

View File

@ -22,11 +22,8 @@ import FileStorage from './services/FileStorage'
import { GeminiService } from './services/GeminiService' import { GeminiService } from './services/GeminiService'
import KnowledgeService from './services/KnowledgeService' import KnowledgeService from './services/KnowledgeService'
import mcpService from './services/MCPService' import mcpService from './services/MCPService'
<<<<<<< HEAD
import { memoryFileService } from './services/MemoryFileService' import { memoryFileService } from './services/MemoryFileService'
=======
import * as MsTTSService from './services/MsTTSService' import * as MsTTSService from './services/MsTTSService'
>>>>>>> origin/1600822305-patch-2
import * as NutstoreService from './services/NutstoreService' import * as NutstoreService from './services/NutstoreService'
import ObsidianVaultService from './services/ObsidianVaultService' import ObsidianVaultService from './services/ObsidianVaultService'
import { ProxyConfig, proxyManager } from './services/ProxyManager' import { ProxyConfig, proxyManager } from './services/ProxyManager'
@ -310,7 +307,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
) )
// search window // search window
<<<<<<< HEAD
ipcMain.handle(IpcChannel.SearchWindow_Open, async (_, uid: string) => { ipcMain.handle(IpcChannel.SearchWindow_Open, async (_, uid: string) => {
await searchService.openSearchWindow(uid) 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) => { ipcMain.handle(IpcChannel.LongTermMemory_SaveData, async (_, data, forceOverwrite = false) => {
return await memoryFileService.saveLongTermData(data, forceOverwrite) 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处理程序 // 注册ASR服务器IPC处理程序
asrServerService.registerIpcHandlers() 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) => ipcMain.handle(IpcChannel.MsTTS_Synthesize, (_, text: string, voice: string, outputFormat: string) =>
MsTTSService.synthesize(text, voice, outputFormat) MsTTSService.synthesize(text, voice, outputFormat)
) )
>>>>>>> origin/1600822305-patch-2
} }

View File

@ -188,7 +188,6 @@ const api = {
closeSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Close, uid), closeSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Close, uid),
openUrlInSearchWindow: (uid: string, url: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_OpenUrl, uid, url) openUrlInSearchWindow: (uid: string, url: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_OpenUrl, uid, url)
}, },
<<<<<<< HEAD
memory: { memory: {
loadData: () => ipcRenderer.invoke(IpcChannel.Memory_LoadData), loadData: () => ipcRenderer.invoke(IpcChannel.Memory_LoadData),
saveData: (data: any) => ipcRenderer.invoke(IpcChannel.Memory_SaveData, data), saveData: (data: any) => ipcRenderer.invoke(IpcChannel.Memory_SaveData, data),
@ -196,11 +195,10 @@ const api = {
loadLongTermData: () => ipcRenderer.invoke(IpcChannel.LongTermMemory_LoadData), loadLongTermData: () => ipcRenderer.invoke(IpcChannel.LongTermMemory_LoadData),
saveLongTermData: (data: any, forceOverwrite: boolean = false) => saveLongTermData: (data: any, forceOverwrite: boolean = false) =>
ipcRenderer.invoke(IpcChannel.LongTermMemory_SaveData, data, forceOverwrite) ipcRenderer.invoke(IpcChannel.LongTermMemory_SaveData, data, forceOverwrite)
======= },
asrServer: { asrServer: {
startServer: () => ipcRenderer.invoke(IpcChannel.Asr_StartServer), startServer: () => ipcRenderer.invoke(IpcChannel.Asr_StartServer),
stopServer: (pid: number) => ipcRenderer.invoke(IpcChannel.Asr_StopServer, pid) stopServer: (pid: number) => ipcRenderer.invoke(IpcChannel.Asr_StopServer, pid)
>>>>>>> origin/1600822305-patch-2
} }
} }

View File

@ -7,8 +7,9 @@ import store from '@renderer/store'
import { deleteShortMemory } from '@renderer/store/memory' import { deleteShortMemory } from '@renderer/store/memory'
import { Button, Card, Col, Empty, Input, List, message, Modal, Row, Statistic, Tooltip } from 'antd' import { Button, Card, Col, Empty, Input, List, message, Modal, Row, Statistic, Tooltip } from 'antd'
import _ from 'lodash' import _ from 'lodash'
import { useCallback, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { createSelector } from 'reselect'
import styled from 'styled-components' import styled from 'styled-components'
// 不再需要确认对话框 // 不再需要确认对话框
@ -36,13 +37,27 @@ const PopupContainer: React.FC<Props> = ({ topicId, resolve }) => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const [open, setOpen] = useState(true) 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 shortMemoryActive = useAppSelector((state) => state.memory?.shortMemoryActive || false)
const shortMemories = useAppSelector((state) => { const shortMemories = useAppSelector((state) => selectShortMemoriesByTopicId(state, topicId))
const allShortMemories = state.memory?.shortMemories || []
// 只显示当前话题的短记忆 // 获取分析统计数据
return topicId ? allShortMemories.filter((memory) => memory.topicId === 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('') const [newMemoryContent, setNewMemoryContent] = useState('')
@ -185,20 +200,14 @@ const PopupContainer: React.FC<Props> = ({ topicId, resolve }) => {
<Col span={8}> <Col span={8}>
<Statistic <Statistic
title={t('settings.memory.totalAnalyses') || '总分析次数'} title={t('settings.memory.totalAnalyses') || '总分析次数'}
value={store.getState().memory?.analysisStats?.totalAnalyses || 0} value={totalAnalyses}
precision={0} precision={0}
/> />
</Col> </Col>
<Col span={8}> <Col span={8}>
<Statistic <Statistic
title={t('settings.memory.successRate') || '成功率'} title={t('settings.memory.successRate') || '成功率'}
value={ value={successRate}
store.getState().memory?.analysisStats?.totalAnalyses
? ((store.getState().memory?.analysisStats?.successfulAnalyses || 0) /
(store.getState().memory?.analysisStats?.totalAnalyses || 1)) *
100
: 0
}
precision={1} precision={1}
suffix="%" suffix="%"
/> />
@ -206,7 +215,7 @@ const PopupContainer: React.FC<Props> = ({ topicId, resolve }) => {
<Col span={8}> <Col span={8}>
<Statistic <Statistic
title={t('settings.memory.avgAnalysisTime') || '平均分析时间'} title={t('settings.memory.avgAnalysisTime') || '平均分析时间'}
value={store.getState().memory?.analysisStats?.averageAnalysisTime || 0} value={avgAnalysisTime}
precision={0} precision={0}
suffix="ms" suffix="ms"
/> />

View File

@ -1395,7 +1395,6 @@
"title": "プライバシー設定", "title": "プライバシー設定",
"enable_privacy_mode": "匿名エラーレポートとデータ統計の送信" "enable_privacy_mode": "匿名エラーレポートとデータ統計の送信"
}, },
<<<<<<< HEAD
"memory": { "memory": {
"title": "メモリー機能", "title": "メモリー機能",
"description": "AIアシスタントの長期メモリーを管理し、会話を自動分析して重要な情報を抽出します", "description": "AIアシスタントの長期メモリーを管理し、会話を自動分析して重要な情報を抽出します",
@ -1453,7 +1452,7 @@
"totalAnalyses": "分析回数合計", "totalAnalyses": "分析回数合計",
"successRate": "成功率", "successRate": "成功率",
"avgAnalysisTime": "平均分析時間" "avgAnalysisTime": "平均分析時間"
======= },
"tts": { "tts": {
"title": "音声合成設定", "title": "音声合成設定",
"enable": "音声合成を有効にする", "enable": "音声合成を有効にする",
@ -1618,7 +1617,6 @@
"asr_tts_info": "音声通話は上記の音声認識(ASR)と音声合成(TTS)の設定を使用します", "asr_tts_info": "音声通話は上記の音声認識(ASR)と音声合成(TTS)の設定を使用します",
"test": "音声通話テスト", "test": "音声通話テスト",
"test_info": "入力ボックスの右側にある音声通話ボタンを使用してテストしてください" "test_info": "入力ボックスの右側にある音声通話ボタンを使用してテストしてください"
>>>>>>> origin/1600822305-patch-2
} }
}, },
"translate": { "translate": {

View File

@ -1398,7 +1398,6 @@
"title": "Настройки приватности", "title": "Настройки приватности",
"enable_privacy_mode": "Анонимная отправка отчетов об ошибках и статистики" "enable_privacy_mode": "Анонимная отправка отчетов об ошибках и статистики"
}, },
<<<<<<< HEAD
"memory": { "memory": {
"title": "[to be translated]:记忆功能", "title": "[to be translated]:记忆功能",
"description": "[to be translated]:管理AI助手的长期记忆自动分析对话并提取重要信息", "description": "[to be translated]:管理AI助手的长期记忆自动分析对话并提取重要信息",
@ -1452,7 +1451,7 @@
"confirmDelete": "[to be translated]:确认删除", "confirmDelete": "[to be translated]:确认删除",
"confirmDeleteContent": "[to be translated]:确定要删除这条短期记忆吗?", "confirmDeleteContent": "[to be translated]:确定要删除这条短期记忆吗?",
"delete": "[to be translated]:删除" "delete": "[to be translated]:删除"
======= },
"tts": { "tts": {
"title": "Настройки преобразования текста в речь", "title": "Настройки преобразования текста в речь",
"enable": "Включить преобразование текста в речь", "enable": "Включить преобразование текста в речь",
@ -1617,7 +1616,6 @@
"test": "Тестировать голосовой вызов", "test": "Тестировать голосовой вызов",
"test_info": "Используйте кнопку голосового вызова справа от поля ввода для тестирования", "test_info": "Используйте кнопку голосового вызова справа от поля ввода для тестирования",
"welcome_message": "Здравствуйте, я ваш ИИ-ассистент. Пожалуйста, нажмите и удерживайте кнопку разговора для начала диалога." "welcome_message": "Здравствуйте, я ваш ИИ-ассистент. Пожалуйста, нажмите и удерживайте кнопку разговора для начала диалога."
>>>>>>> origin/1600822305-patch-2
} }
}, },
"translate": { "translate": {

View File

@ -1395,7 +1395,6 @@
"title": "隱私設定", "title": "隱私設定",
"enable_privacy_mode": "匿名發送錯誤報告和資料統計" "enable_privacy_mode": "匿名發送錯誤報告和資料統計"
}, },
<<<<<<< HEAD
"memory": { "memory": {
"title": "記憶功能", "title": "記憶功能",
"description": "管理AI助手的長期記憶自動分析對話並提取重要信息", "description": "管理AI助手的長期記憶自動分析對話並提取重要信息",
@ -1453,7 +1452,7 @@
"totalAnalyses": "總分析次數", "totalAnalyses": "總分析次數",
"successRate": "成功率", "successRate": "成功率",
"avgAnalysisTime": "平均分析時間" "avgAnalysisTime": "平均分析時間"
======= },
"tts": { "tts": {
"title": "語音設定", "title": "語音設定",
"enable": "啟用語音合成", "enable": "啟用語音合成",
@ -1618,7 +1617,6 @@
"test": "測試通話", "test": "測試通話",
"test_info": "請使用輸入框右側的語音通話按鈕進行測試", "test_info": "請使用輸入框右側的語音通話按鈕進行測試",
"welcome_message": "您好我是您的AI助理請長按說話按鈕進行對話。" "welcome_message": "您好我是您的AI助理請長按說話按鈕進行對話。"
>>>>>>> origin/1600822305-patch-2
} }
}, },
"translate": { "translate": {

View File

@ -327,8 +327,8 @@ const getContextMenuItems = (
t: (key: string) => string, t: (key: string) => string,
selectedQuoteText: string, selectedQuoteText: string,
selectedText: string, selectedText: string,
<<<<<<< HEAD message: Message,
message: Message currentMessage?: Message
): ItemType[] => { ): ItemType[] => {
const items: ItemType[] = [] const items: ItemType[] = []
@ -350,47 +350,31 @@ const getContextMenuItems = (
EventEmitter.emit(EVENT_NAMES.QUOTE_TEXT, selectedQuoteText) EventEmitter.emit(EVENT_NAMES.QUOTE_TEXT, selectedQuoteText)
} }
}) })
=======
currentMessage?: Message // 添加朗读选项
) => [ items.push({
{ key: 'speak',
key: 'copy', label: '朗读',
label: t('common.copy'), onClick: () => {
onClick: () => { // 从选中的文本开始朗读后面的内容
navigator.clipboard.writeText(selectedText) if (selectedText && currentMessage?.content) {
window.message.success({ content: t('message.copied'), key: 'copy-message' }) // 找到选中文本在消息中的位置
} const startIndex = currentMessage.content.indexOf(selectedText)
}, if (startIndex !== -1) {
{ // 获取选中文本及其后面的所有内容
key: 'quote', const textToSpeak = currentMessage.content.substring(startIndex)
label: t('chat.message.quote'), import('@renderer/services/TTSService').then(({ default: TTSService }) => {
onClick: () => { TTSService.speak(textToSpeak)
EventEmitter.emit(EVENT_NAMES.QUOTE_TEXT, selectedQuoteText) })
} } else {
}, // 如果找不到精确位置,则只朗读选中的文本
{ import('@renderer/services/TTSService').then(({ default: TTSService }) => {
key: 'speak', TTSService.speak(selectedText)
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 // 添加复制消息ID选项但不显示ID

View File

@ -222,7 +222,6 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
<Flex gap="8px" wrap style={{ marginBottom: 10 }}> <Flex gap="8px" wrap style={{ marginBottom: 10 }}>
{message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)} {message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)}
</Flex> </Flex>
<<<<<<< HEAD
{message.referencedMessages && message.referencedMessages.length > 0 && ( {message.referencedMessages && message.referencedMessages.length > 0 && (
<div> <div>
{message.referencedMessages.map((refMsg, index) => ( {message.referencedMessages.map((refMsg, index) => (
@ -317,16 +316,11 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
<MessageThought message={message} /> <MessageThought message={message} />
<MessageTools message={message} /> <MessageTools message={message} />
</div> </div>
<Markdown message={{ ...message, content: processedContent.replace(toolUseRegex, '') }} />
=======
<MessageThought message={message} />
<MessageTools message={message} />
{isSegmentedPlayback ? ( {isSegmentedPlayback ? (
<TTSHighlightedText text={processedContent.replace(toolUseRegex, '')} /> <TTSHighlightedText text={processedContent.replace(toolUseRegex, '')} />
) : ( ) : (
<Markdown message={{ ...message, content: processedContent.replace(toolUseRegex, '') }} /> <Markdown message={{ ...message, content: processedContent.replace(toolUseRegex, '') }} />
)} )}
>>>>>>> origin/1600822305-patch-2
{message.metadata?.generateImage && <MessageImage message={message} />} {message.metadata?.generateImage && <MessageImage message={message} />}
{message.translatedContent && ( {message.translatedContent && (
<Fragment> <Fragment>

View File

@ -265,12 +265,20 @@ const computeDisplayMessages = (messages: Message[], startIndex: number, display
const userIdSet = new Set() // 用户消息 id 集合 const userIdSet = new Set() // 用户消息 id 集合
const assistantIdSet = new Set() // 助手消息 askId 集合 const assistantIdSet = new Set() // 助手消息 askId 集合
const processedIds = new Set<string>() // 用于跟踪已处理的消息ID
const displayMessages: Message[] = [] const displayMessages: Message[] = []
// 处理单条消息的函数 // 处理单条消息的函数
const processMessage = (message: Message) => { const processMessage = (message: Message) => {
if (!message) return if (!message) return
// 跳过已处理的消息ID
if (processedIds.has(message.id)) {
return
}
processedIds.add(message.id) // 标记此消息ID为已处理
const idSet = message.role === 'user' ? userIdSet : assistantIdSet const idSet = message.role === 'user' ? userIdSet : assistantIdSet
const messageId = message.role === 'user' ? message.id : message.askId const messageId = message.role === 'user' ? message.id : message.askId
@ -279,8 +287,12 @@ const computeDisplayMessages = (messages: Message[], startIndex: number, display
displayMessages.push(message) displayMessages.push(message)
return return
} }
// 如果是相同 askId 的助手消息,也要显示
displayMessages.push(message) // 如果是相同 askId 的助手消息检查是否已经有相同ID的消息
// 只有在没有相同ID的情况下才添加
if (message.role === 'assistant' && !displayMessages.some(m => m.id === message.id)) {
displayMessages.push(message)
}
} }
// 遍历消息直到满足显示数量要求 // 遍历消息直到满足显示数量要求

View File

@ -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 { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover' import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
@ -11,11 +11,11 @@ import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { analyzeAndAddShortMemories } from '@renderer/services/MemoryService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setNarrowMode } from '@renderer/store/settings' import { setNarrowMode } from '@renderer/store/settings'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { Button, Tooltip } from 'antd' import { Tooltip } from 'antd'
import { t } from 'i18next' import { t } from 'i18next'
import { LayoutGrid, MessageSquareDiff, PanelLeftClose, PanelRightClose, Search } from 'lucide-react' import { LayoutGrid, MessageSquareDiff, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
import { FC } from 'react' import { FC } from 'react'
@ -64,21 +64,7 @@ const HeaderNavbar: FC<Props> = ({ 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 ( return (
<Navbar className="home-navbar"> <Navbar className="home-navbar">
@ -116,9 +102,6 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, activeTopic }) => {
<BookOutlined /> <BookOutlined />
</NarrowIcon> </NarrowIcon>
</Tooltip> </Tooltip>
<AnalyzeButton onClick={handleAnalyzeShortMemory}>
{t('settings.memory.analyzeConversation') || '分析对话'}
</AnalyzeButton>
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}> <Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}> <NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} /> <Search size={18} />
@ -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 export default HeaderNavbar

View File

@ -6,6 +6,7 @@ import {
FolderOutlined, FolderOutlined,
PushpinOutlined, PushpinOutlined,
QuestionCircleOutlined, QuestionCircleOutlined,
SearchOutlined,
UploadOutlined UploadOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import DragableList from '@renderer/components/DragableList' import DragableList from '@renderer/components/DragableList'
@ -20,6 +21,7 @@ import { useSettings } from '@renderer/hooks/useSettings'
import { TopicManager } from '@renderer/hooks/useTopic' import { TopicManager } from '@renderer/hooks/useTopic'
import { fetchMessagesSummary } from '@renderer/services/ApiService' import { fetchMessagesSummary } from '@renderer/services/ApiService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { analyzeAndAddShortMemories } from '@renderer/services/MemoryService'
import store from '@renderer/store' import store from '@renderer/store'
import { RootState } from '@renderer/store' import { RootState } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime' import { setGenerating } from '@renderer/store/runtime'
@ -238,6 +240,24 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
}) })
} }
}, },
{
label: t('settings.memory.analyzeConversation') || '分析对话',
key: 'analyze-conversation',
icon: <SearchOutlined />,
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'), label: t('chat.topics.copy.title'),
key: 'copy', key: 'copy',

View File

@ -1,17 +1,4 @@
import { import { ExperimentOutlined } from '@ant-design/icons'
AppstoreOutlined,
CloudOutlined,
CodeOutlined,
ExperimentOutlined,
GlobalOutlined,
InfoCircleOutlined,
LayoutOutlined,
MacCommandOutlined,
RocketOutlined,
SaveOutlined,
SettingOutlined,
ThunderboltOutlined
} from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon' import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings' import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings'
@ -160,12 +147,9 @@ const SettingsPage: FC = () => {
<Route path="model" element={<ModelSettings />} /> <Route path="model" element={<ModelSettings />} />
<Route path="web-search" element={<WebSearchSettings />} /> <Route path="web-search" element={<WebSearchSettings />} />
<Route path="mcp/*" element={<MCPSettings />} /> <Route path="mcp/*" element={<MCPSettings />} />
<<<<<<< HEAD
<Route path="memory" element={<MemorySettings />} /> <Route path="memory" element={<MemorySettings />} />
<Route path="general" element={<GeneralSettings />} /> <Route path="general" element={<GeneralSettings />} />
=======
<Route path="general/*" element={<GeneralSettings />} /> <Route path="general/*" element={<GeneralSettings />} />
>>>>>>> origin/1600822305-patch-2
<Route path="display" element={<DisplaySettings />} /> <Route path="display" element={<DisplaySettings />} />
{showMiniAppSettings && <Route path="miniapps" element={<MiniAppSettings />} />} {showMiniAppSettings && <Route path="miniapps" element={<MiniAppSettings />} />}
<Route path="shortcut" element={<ShortcutSettings />} /> <Route path="shortcut" element={<ShortcutSettings />} />

View File

@ -125,6 +125,14 @@ ${memoriesToCheck}
console.log('[Memory Deduplication] Analysis result:', result) 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 similarGroups: DeduplicationResult['similarGroups'] = []
const independentMemories: string[] = [] const independentMemories: string[] = []
@ -144,38 +152,139 @@ ${memoriesToCheck}
if (similarGroupsMatch && similarGroupsMatch[1]) { if (similarGroupsMatch && similarGroupsMatch[1]) {
const groupsText = similarGroupsMatch[1].trim() 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 /-\s*组(\d+)?:\s*\[([\d,\s]+)\]\s*-\s*合并建议:\s*"([^"]+)"\s*-\s*分类:\s*"([^"]+)"\s*(?:-\s*重要性:\s*"([^"]+)")?\s*(?:-\s*关键词:\s*"([^"]+)")?/g
let match: RegExpExecArray | null // 新增正则表达式匹配AI返回的不同格式
while ((match = groupRegex.exec(groupsText)) !== null) { const alternativeGroupRegex = /-\s*组(\d+)?:\s*(?:\*\*)?["\[]?([\d,\s]+)["\]]?(?:\*\*)?\s*-\s*合并建议:\s*(?:\*\*)?["']?([^"'\n-]+)["']?(?:\*\*)?\s*-\s*分类:\s*(?:\*\*)?["']?([^"'\n-]+)["']?(?:\*\*)?/g
const groupId = match[1] || String(similarGroups.length + 1)
const memoryIndices = match[2].split(',').map((s: string) => s.trim()) // 简化的正则表达式,直接匹配组号和方括号内的数字
const mergedContent = match[3].trim() const simpleGroupRegex = /-\s*组(\d+)?:\s*\[([\d,\s]+)\]\s*-\s*合并建议:\s*(.+?)\s*-\s*分类:\s*(.+?)(?=\s*$|\s*-\s*组|\s*\n)/gm
const category = match[4]?.trim()
const importance = match[5] ? parseFloat(match[5].trim()) : undefined // 尝试所有正则表达式
const keywords = match[6] const regexesToTry = [simpleGroupRegex, alternativeGroupRegex, originalGroupRegex]
? match[6]
.trim() // 逐个尝试正则表达式
.split(',') for (const regex of regexesToTry) {
.map((k: string) => k.trim()) let match: RegExpExecArray | null
: undefined 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({ similarGroups.push({
groupId, groupId,
memoryIds: memoryIndices, memoryIds: memoryIndices,
mergedContent, mergedContent,
category, category: '其他' // 默认类别
importance,
keywords
}) })
} }
} }
// 解析独立记忆项 // 如果仍然没有解析到任何内容,尝试直接从原始结果中提取信息
const independentMatch = result.match(/2\.\s*独立记忆项:\s*\[([\d,\s]+)\]/i) if (similarGroups.length === 0 && independentMemories.length === 0) {
if (independentMatch && independentMatch[1]) { console.log('[Memory Deduplication] No groups or independent memories found, attempting direct extraction')
independentMemories.push(...independentMatch[1].split(',').map((s: string) => s.trim()))
// 尝试提取所有数字作为独立记忆项
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 }) console.log('[Memory Deduplication] Parsed result:', { similarGroups, independentMemories })

View File

@ -110,19 +110,39 @@ const analyzeConversation = async (
let basePrompt = let basePrompt =
customPrompt || customPrompt ||
` `
##
类别: 信息内容 类别: 信息内容
- -
- -
- -
- -
- -
用户偏好: 用户喜欢简洁直接的代码修改方式
技术需求: 用户需要修复长期记忆分析功能中的问题
个人信息: 用户自称是彭于晏
交互偏好: 用户倾向于简短直接的问答方式
其他: 用户对AI记忆功能的工作原理很感兴趣
##
##
1. 类别: 信息内容
2. 使
3.
4.
` `
// 如果启用了敏感信息过滤,添加相关指令 // 如果启用了敏感信息过滤,添加相关指令
@ -149,10 +169,21 @@ const analyzeConversation = async (
## ##
${conversation} ${conversation}
## ##
`
##
类别: 信息内容
用户偏好: 用户喜欢简洁直接的代码修改方式
技术需求: 用户需要修复长期记忆分析功能中的问题
个人信息: 用户自称是彭于晏
`
// 使用fetchGenerate函数但将内容字段留空所有内容都放在提示词中 // 使用fetchGenerate函数但将内容字段留空所有内容都放在提示词中
console.log('[Memory Analysis] Calling fetchGenerate with combined prompt...') console.log('[Memory Analysis] Calling fetchGenerate with combined prompt...')
@ -178,6 +209,7 @@ ${conversation}
const memories: Array<{ content: string; category: string }> = [] const memories: Array<{ content: string; category: string }> = []
// 首先尝试使用标准格式解析
for (const line of lines) { for (const line of lines) {
// 匹配格式:类别: 信息内容 // 匹配格式:类别: 信息内容
const match = line.match(/^([^:]+):\s*(.+)$/) 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 return memories
} catch (error) { } catch (error) {
console.error('Failed to analyze conversation with real AI:', error) console.error('Failed to analyze conversation with real AI:', error)
@ -474,41 +533,45 @@ export const useMemoryService = () => {
const basePrompt = ` const basePrompt = `
## ##
##
1.
2.
3.
4.
5.
##
类别: 信息内容 类别: 信息内容
-
-
-
-
-
用户偏好: 用户喜欢简洁直接的代码修改方式 用户偏好: 用户喜欢简洁直接的代码修改方式
技术需求: 用户需要修复长期记忆分析功能中的问题 技术需求: 用户需要修复长期记忆分析功能中的问题
个人信息: 用户自称是彭于晏
交互偏好: 用户倾向于简短直接的问答方式
其他: 用户对AI记忆功能的工作原理很感兴趣
## ##
-
-
-
-
-
## ##
${newConversation} ${newConversation}
## ##
-
-
-
-
##
1. 类别: 信息内容
2. 使
3.
4.
${ ${
existingMemoriesContent existingMemoriesContent

View File

@ -185,8 +185,16 @@ export function getAssistantMessage({ assistant, topic }: { assistant: Assistant
export function getGroupedMessages(messages: Message[]): { [key: string]: (Message & { index: number })[] } { export function getGroupedMessages(messages: Message[]): { [key: string]: (Message & { index: number })[] } {
const groups: { [key: string]: (Message & { index: number })[] } = {} const groups: { [key: string]: (Message & { index: number })[] } = {}
const processedIds = new Set<string>() // 用于跟踪已处理的消息ID
messages.forEach((message, index) => { 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 const key = message.askId ? 'assistant' + message.askId : 'user' + message.id
if (key && !groups[key]) { if (key && !groups[key]) {
groups[key] = [] groups[key] = []

View File

@ -74,13 +74,24 @@ const messagesSlice = createSlice({
state.messagesByTopic[topicId] = [] state.messagesByTopic[topicId] = []
} }
// 获取当前消息列表
const currentMessages = state.messagesByTopic[topicId]
if (Array.isArray(messages)) { 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 { } else {
// 添加单条消息 // 添加单条消息,先检查是否已存在
state.messagesByTopic[topicId].push(messages) if (!currentMessages.some(existing => existing.id === messages.id)) {
state.messagesByTopic[topicId].push(messages)
}
} }
}, },
appendMessage: ( appendMessage: (
@ -96,15 +107,23 @@ const messagesSlice = createSlice({
// 确保消息数组存在并且拿到引用 // 确保消息数组存在并且拿到引用
const messagesList = state.messagesByTopic[topicId] const messagesList = state.messagesByTopic[topicId]
// 要插入的消息 // 要插入的消息,先过滤掉已存在的消息
const messagesToInsert = Array.isArray(messages) ? messages : [messages] 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) { if (position !== undefined && position >= 0 && position <= messagesList.length) {
// 如果指定了位置,在特定位置插入消息 // 如果指定了位置,在特定位置插入消息
messagesList.splice(position, 0, ...messagesToInsert) messagesList.splice(position, 0, ...uniqueMessagesToInsert)
} else { } else {
// 否则默认添加到末尾 // 否则默认添加到末尾
messagesList.push(...messagesToInsert) messagesList.push(...uniqueMessagesToInsert)
} }
}, },
updateMessage: ( 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 (m) => m.role === 'assistant' && m.id === streamMessage.id
) )
if (existingMessage) { if (existingMessageIndex !== -1) {
// 更新 // 更新现有消息
Object.assign(existingMessage, streamMessage) Object.assign(state.messagesByTopic[topicId][existingMessageIndex], streamMessage)
} else { } else {
// 添加新消息 // 检查是否有重复的消息相同的askId和内容
state.messagesByTopic[topicId].push(streamMessage) 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)
}
} }
// 删除流状态 // 删除流状态

View File

@ -4126,7 +4126,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@vitejs/plugin-react@npm:^4.2.1": "@vitejs/plugin-react@npm:^4.3.4":
version: 4.3.4 version: 4.3.4
resolution: "@vitejs/plugin-react@npm:4.3.4" resolution: "@vitejs/plugin-react@npm:4.3.4"
dependencies: dependencies:
@ -4233,7 +4233,7 @@ __metadata:
"@types/react-dom": "npm:^19.0.4" "@types/react-dom": "npm:^19.0.4"
"@types/react-infinite-scroll-component": "npm:^5.0.0" "@types/react-infinite-scroll-component": "npm:^5.0.0"
"@types/tinycolor2": "npm:^1" "@types/tinycolor2": "npm:^1"
"@vitejs/plugin-react": "npm:^4.2.1" "@vitejs/plugin-react": "npm:^4.3.4"
"@xyflow/react": "npm:^12.4.4" "@xyflow/react": "npm:^12.4.4"
adm-zip: "npm:^0.5.16" adm-zip: "npm:^0.5.16"
analytics: "npm:^0.8.16" analytics: "npm:^0.8.16"
@ -15620,7 +15620,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
<<<<<<< HEAD
"rw@npm:1": "rw@npm:1":
version: 1.3.3 version: 1.3.3
resolution: "rw@npm:1.3.3" resolution: "rw@npm:1.3.3"
@ -15628,10 +15627,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "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 version: 5.2.1
resolution: "safe-buffer@npm:5.2.1" resolution: "safe-buffer@npm:5.2.1"
checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3