From a8f18caf0ea299b1fd313525a2a77af8dbdb2772 Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Fri, 11 Apr 2025 04:00:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=83=A8=E5=88=86=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/index.ts | 11 -- src/main/services/MCPService.ts | 24 --- .../src/components/VoiceCallButton.tsx | 54 +++--- .../src/components/VoiceCallModal.tsx | 167 +++++++++--------- .../src/components/VoiceVisualizer.tsx | 90 +++++----- 5 files changed, 160 insertions(+), 186 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index d9f92fb348..ded41250cc 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,12 +3,10 @@ import { replaceDevtoolsFont } from '@main/utils/windowUtil' import { IpcChannel } from '@shared/IpcChannel' import { app, ipcMain } from 'electron' import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer' -import Logger from 'electron-log' import { registerIpc } from './ipc' import { configManager } from './services/ConfigManager' import { registerMsTTSIpcHandlers } from './services/MsTTSIpcHandler' -import mcpService from './services/MCPService' import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient' import { registerShortcuts } from './services/ShortcutService' import { TrayService } from './services/TrayService' @@ -98,15 +96,6 @@ if (!app.requestSingleInstanceLock()) { app.isQuitting = true }) - app.on('will-quit', async () => { - // event.preventDefault() - try { - await mcpService.cleanup() - } catch (error) { - Logger.error('Error cleaning up MCP service:', error) - } - }) - // In this file you can include the rest of your app"s specific main process // code. You can also put them in separate files and require them here. } diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index 63a4ffb240..ecfa14a83c 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -322,30 +322,6 @@ class McpService { // 转换回字符串 return Array.from(existingPaths).join(pathSeparator) } - - /** - * 清理所有MCP客户端连接 - */ - public async cleanup(): Promise { - Logger.info('[MCP] Cleaning up all MCP clients...') - const closePromises: Promise[] = [] - - // 关闭所有客户端连接 - for (const [serverKey, client] of this.clients.entries()) { - try { - Logger.info(`[MCP] Closing client for server: ${serverKey}`) - closePromises.push(client.close()) - } catch (error) { - Logger.error(`[MCP] Error closing client for server: ${serverKey}`, error) - } finally { - this.clients.delete(serverKey) - } - } - - // 等待所有关闭操作完成 - await Promise.allSettled(closePromises) - Logger.info('[MCP] All MCP clients cleaned up') - } } export default new McpService() diff --git a/src/renderer/src/components/VoiceCallButton.tsx b/src/renderer/src/components/VoiceCallButton.tsx index 9f95c569ae..00d784b4ed 100644 --- a/src/renderer/src/components/VoiceCallButton.tsx +++ b/src/renderer/src/components/VoiceCallButton.tsx @@ -1,36 +1,35 @@ -import { LoadingOutlined, PhoneOutlined } from '@ant-design/icons' -import { Button, Tooltip } from 'antd' -import React, { useState } from 'react' -import { useTranslation } from 'react-i18next' - -import { VoiceCallService } from '../services/VoiceCallService' -import VoiceCallModal from './VoiceCallModal' +import React, { useState } from 'react'; +import { Button, Tooltip } from 'antd'; +import { PhoneOutlined, LoadingOutlined } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; +import VoiceCallModal from './VoiceCallModal'; +import { VoiceCallService } from '../services/VoiceCallService'; interface Props { - disabled?: boolean - style?: React.CSSProperties + disabled?: boolean; + style?: React.CSSProperties; } const VoiceCallButton: React.FC = ({ disabled = false, style }) => { - const { t } = useTranslation() - const [isModalVisible, setIsModalVisible] = useState(false) - const [isLoading, setIsLoading] = useState(false) + const { t } = useTranslation(); + const [isModalVisible, setIsModalVisible] = useState(false); + const [isLoading, setIsLoading] = useState(false); const handleClick = async () => { - if (disabled || isLoading) return - - setIsLoading(true) + if (disabled || isLoading) return; + + setIsLoading(true); try { // 初始化语音服务 - await VoiceCallService.initialize() - setIsModalVisible(true) + await VoiceCallService.initialize(); + setIsModalVisible(true); } catch (error) { - console.error('Failed to initialize voice call:', error) - window.message.error(t('voice_call.initialization_failed')) + console.error('Failed to initialize voice call:', error); + window.message.error(t('voice_call.initialization_failed')); } finally { - setIsLoading(false) + setIsLoading(false); } - } + }; return ( <> @@ -43,9 +42,14 @@ const VoiceCallButton: React.FC = ({ disabled = false, style }) => { style={style} /> - {isModalVisible && setIsModalVisible(false)} />} + {isModalVisible && ( + setIsModalVisible(false)} + /> + )} - ) -} + ); +}; -export default VoiceCallButton +export default VoiceCallButton; diff --git a/src/renderer/src/components/VoiceCallModal.tsx b/src/renderer/src/components/VoiceCallModal.tsx index 55368cc0b9..dbceac6062 100644 --- a/src/renderer/src/components/VoiceCallModal.tsx +++ b/src/renderer/src/components/VoiceCallModal.tsx @@ -1,3 +1,5 @@ +import React, { useEffect, useState } from 'react'; +import { Modal, Button, Space, Tooltip } from 'antd'; import { AudioMutedOutlined, AudioOutlined, @@ -5,128 +7,125 @@ import { PauseCircleOutlined, PlayCircleOutlined, SoundOutlined -} from '@ant-design/icons' -import { Button, Modal, Space, Tooltip } from 'antd' -import React, { useCallback, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' - -import { VoiceCallService } from '../services/VoiceCallService' -import VoiceVisualizer from './VoiceVisualizer' +} from '@ant-design/icons'; +import styled from 'styled-components'; +import { useTranslation } from 'react-i18next'; +import VoiceVisualizer from './VoiceVisualizer'; +import { VoiceCallService } from '../services/VoiceCallService'; interface Props { - visible: boolean - onClose: () => void + visible: boolean; + onClose: () => void; } const VoiceCallModal: React.FC = ({ visible, onClose }) => { - const { t } = useTranslation() - const [isMuted, setIsMuted] = useState(false) - const [isPaused, setIsPaused] = useState(false) - const [transcript, setTranscript] = useState('') - const [response, setResponse] = useState('') - const [isListening, setIsListening] = useState(false) - const [isSpeaking, setIsSpeaking] = useState(false) - const [isRecording, setIsRecording] = useState(false) - const [isProcessing, setIsProcessing] = useState(false) - - const handleClose = useCallback(() => { - VoiceCallService.endCall() - onClose() - }, [onClose]) + const { t } = useTranslation(); + const [isMuted, setIsMuted] = useState(false); + const [isPaused, setIsPaused] = useState(false); + const [transcript, setTranscript] = useState(''); + const [response, setResponse] = useState(''); + const [isListening, setIsListening] = useState(false); + const [isSpeaking, setIsSpeaking] = useState(false); + const [isRecording, setIsRecording] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); useEffect(() => { const startVoiceCall = async () => { try { await VoiceCallService.startCall({ - onTranscript: (text: string) => setTranscript(text), - onResponse: (text: string) => setResponse(text), + onTranscript: (text) => setTranscript(text), + onResponse: (text) => setResponse(text), onListeningStateChange: setIsListening, - onSpeakingStateChange: setIsSpeaking - }) + onSpeakingStateChange: setIsSpeaking, + }); } catch (error) { - console.error('Voice call error:', error) - window.message.error(t('voice_call.error')) - handleClose() + console.error('Voice call error:', error); + window.message.error(t('voice_call.error')); + handleClose(); } - } + }; if (visible) { - startVoiceCall() + startVoiceCall(); } return () => { - VoiceCallService.endCall() - } - }, [visible, t, handleClose]) + VoiceCallService.endCall(); + }; + }, [visible, t]); + + const handleClose = () => { + VoiceCallService.endCall(); + onClose(); + }; const toggleMute = () => { - const newMuteState = !isMuted - setIsMuted(newMuteState) - VoiceCallService.setMuted(newMuteState) - } + const newMuteState = !isMuted; + setIsMuted(newMuteState); + VoiceCallService.setMuted(newMuteState); + }; const togglePause = () => { - const newPauseState = !isPaused - setIsPaused(newPauseState) - VoiceCallService.setPaused(newPauseState) - } + const newPauseState = !isPaused; + setIsPaused(newPauseState); + VoiceCallService.setPaused(newPauseState); + }; // 长按说话相关处理 const handleRecordStart = async (e: React.MouseEvent | React.TouchEvent) => { - e.preventDefault() // 防止触摸事件的默认行为 + e.preventDefault(); // 防止触摸事件的默认行为 - if (isProcessing || isPaused) return + if (isProcessing || isPaused) return; - setIsRecording(true) - await VoiceCallService.startRecording() - } + setIsRecording(true); + await VoiceCallService.startRecording(); + }; const handleRecordEnd = async (e: React.MouseEvent | React.TouchEvent) => { - e.preventDefault() // 防止触摸事件的默认行为 + e.preventDefault(); // 防止触摸事件的默认行为 - if (!isRecording) return + if (!isRecording) return; // 立即更新UI状态 - setIsRecording(false) - setIsProcessing(true) + setIsRecording(false); + setIsProcessing(true); // 确保录音完全停止 try { - await VoiceCallService.stopRecording() - console.log('录音已停止') + await VoiceCallService.stopRecording(); + console.log('录音已停止'); } catch (error) { - console.error('停止录音出错:', error) + console.error('停止录音出错:', error); } // 处理结果会通过回调函数返回,不需要在这里处理 setTimeout(() => { - setIsProcessing(false) - }, 500) // 添加短暂延迟,防止用户立即再次点击 - } + setIsProcessing(false); + }, 500); // 添加短暂延迟,防止用户立即再次点击 + }; // 处理鼠标/触摸离开按钮的情况 const handleRecordCancel = async (e: React.MouseEvent | React.TouchEvent) => { - e.preventDefault() + e.preventDefault(); if (isRecording) { // 立即更新UI状态 - setIsRecording(false) - setIsProcessing(true) + setIsRecording(false); + setIsProcessing(true); // 取消录音,不发送给AI try { - await VoiceCallService.cancelRecording() - console.log('录音已取消') + await VoiceCallService.cancelRecording(); + console.log('录音已取消'); } catch (error) { - console.error('取消录音出错:', error) + console.error('取消录音出错:', error); } setTimeout(() => { - setIsProcessing(false) - }, 500) + setIsProcessing(false); + }, 500); } - } + }; return ( = ({ visible, onClose }) => { footer={null} width={500} centered - maskClosable={false}> + maskClosable={false} + > @@ -174,7 +174,7 @@ const VoiceCallModal: React.FC = ({ visible, onClose }) => { /> } onMouseDown={handleRecordStart} onMouseUp={handleRecordEnd} @@ -183,7 +183,8 @@ const VoiceCallModal: React.FC = ({ visible, onClose }) => { onTouchEnd={handleRecordEnd} onTouchCancel={handleRecordCancel} size="large" - disabled={isProcessing || isPaused}> + disabled={isProcessing || isPaused} + > {isRecording ? t('voice_call.release_to_send') : t('voice_call.press_to_talk')} @@ -199,21 +200,21 @@ const VoiceCallModal: React.FC = ({ visible, onClose }) => { - ) -} + ); +}; const Container = styled.div` display: flex; flex-direction: column; gap: 20px; height: 400px; -` +`; const VisualizerContainer = styled.div` display: flex; justify-content: space-between; height: 100px; -` +`; const TranscriptContainer = styled.div` flex: 1; @@ -222,33 +223,33 @@ const TranscriptContainer = styled.div` border-radius: 8px; padding: 16px; background-color: var(--color-background-2); -` +`; const TranscriptText = styled.p` margin-bottom: 8px; color: var(--color-text-1); -` +`; const ResponseText = styled.p` margin-bottom: 8px; color: var(--color-primary); -` +`; const UserLabel = styled.span` font-weight: bold; color: var(--color-text-1); -` +`; const AILabel = styled.span` font-weight: bold; color: var(--color-primary); -` +`; const ControlsContainer = styled.div` display: flex; justify-content: center; padding: 10px 0; -` +`; const RecordButton = styled(Button)` min-width: 150px; @@ -257,6 +258,6 @@ const RecordButton = styled(Button)` &:active { transform: scale(0.95); } -` +`; -export default VoiceCallModal +export default VoiceCallModal; diff --git a/src/renderer/src/components/VoiceVisualizer.tsx b/src/renderer/src/components/VoiceVisualizer.tsx index 8b6a018e9c..31d5662c92 100644 --- a/src/renderer/src/components/VoiceVisualizer.tsx +++ b/src/renderer/src/components/VoiceVisualizer.tsx @@ -1,74 +1,74 @@ -import React, { useEffect, useRef } from 'react' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' +import React, { useEffect, useRef } from 'react'; +import styled from 'styled-components'; +import { useTranslation } from 'react-i18next'; interface Props { - isActive: boolean - type: 'input' | 'output' + isActive: boolean; + type: 'input' | 'output'; } const VoiceVisualizer: React.FC = ({ isActive, type }) => { - const { t } = useTranslation() - const canvasRef = useRef(null) - const animationRef = useRef(undefined) + const { t } = useTranslation(); + const canvasRef = useRef(null); + const animationRef = useRef(undefined); useEffect(() => { - const canvas = canvasRef.current - if (!canvas) return + const canvas = canvasRef.current; + if (!canvas) return; - const ctx = canvas.getContext('2d') - if (!ctx) return + const ctx = canvas.getContext('2d'); + if (!ctx) return; - const width = canvas.width - const height = canvas.height + const width = canvas.width; + const height = canvas.height; const drawVisualizer = () => { - ctx.clearRect(0, 0, width, height) + ctx.clearRect(0, 0, width, height); if (!isActive) { // 绘制静态波形 - ctx.beginPath() - ctx.moveTo(0, height / 2) - ctx.lineTo(width, height / 2) - ctx.strokeStyle = type === 'input' ? 'var(--color-text-2)' : 'var(--color-primary)' - ctx.lineWidth = 2 - ctx.stroke() - return + ctx.beginPath(); + ctx.moveTo(0, height / 2); + ctx.lineTo(width, height / 2); + ctx.strokeStyle = type === 'input' ? 'var(--color-text-2)' : 'var(--color-primary)'; + ctx.lineWidth = 2; + ctx.stroke(); + return; } // 绘制动态波形 - const barCount = 30 - const barWidth = width / barCount - const color = type === 'input' ? 'var(--color-text-1)' : 'var(--color-primary)' + const barCount = 30; + const barWidth = width / barCount; + const color = type === 'input' ? 'var(--color-text-1)' : 'var(--color-primary)'; for (let i = 0; i < barCount; i++) { - const barHeight = Math.random() * (height / 2) + 10 - const x = i * barWidth - const y = height / 2 - barHeight / 2 + const barHeight = Math.random() * (height / 2) + 10; + const x = i * barWidth; + const y = height / 2 - barHeight / 2; - ctx.fillStyle = color - ctx.fillRect(x, y, barWidth - 2, barHeight) + ctx.fillStyle = color; + ctx.fillRect(x, y, barWidth - 2, barHeight); } - animationRef.current = requestAnimationFrame(drawVisualizer) - } + animationRef.current = requestAnimationFrame(drawVisualizer); + }; - drawVisualizer() + drawVisualizer(); return () => { if (animationRef.current) { - cancelAnimationFrame(animationRef.current) + cancelAnimationFrame(animationRef.current); } - } - }, [isActive, type]) + }; + }, [isActive, type]); return ( - ) -} + ); +}; const Container = styled.div<{ $type: 'input' | 'output' }>` display: flex; @@ -77,17 +77,21 @@ const Container = styled.div<{ $type: 'input' | 'output' }>` width: 45%; border-radius: 8px; padding: 10px; - background-color: ${(props) => (props.$type === 'input' ? 'var(--color-background-3)' : 'var(--color-primary-bg)')}; -` + background-color: ${props => + props.$type === 'input' + ? 'var(--color-background-3)' + : 'var(--color-primary-bg)' + }; +`; const Label = styled.div` margin-bottom: 8px; font-weight: bold; -` +`; const Canvas = styled.canvas` width: 100%; height: 50px; -` +`; -export default VoiceVisualizer +export default VoiceVisualizer;