修复部分问题

This commit is contained in:
1600822305 2025-04-11 04:00:42 +08:00
parent 644995dd76
commit a4eeea6732
5 changed files with 160 additions and 186 deletions

View File

@ -3,12 +3,10 @@ import { replaceDevtoolsFont } from '@main/utils/windowUtil'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { app, ipcMain } from 'electron' import { app, ipcMain } from 'electron'
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer' import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
import Logger from 'electron-log'
import { registerIpc } from './ipc' import { registerIpc } from './ipc'
import { configManager } from './services/ConfigManager' import { configManager } from './services/ConfigManager'
import { registerMsTTSIpcHandlers } from './services/MsTTSIpcHandler' import { registerMsTTSIpcHandlers } from './services/MsTTSIpcHandler'
import mcpService from './services/MCPService'
import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient' import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient'
import { registerShortcuts } from './services/ShortcutService' import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService' import { TrayService } from './services/TrayService'
@ -98,15 +96,6 @@ if (!app.requestSingleInstanceLock()) {
app.isQuitting = true 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 // 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. // code. You can also put them in separate files and require them here.
} }

View File

@ -322,30 +322,6 @@ class McpService {
// 转换回字符串 // 转换回字符串
return Array.from(existingPaths).join(pathSeparator) return Array.from(existingPaths).join(pathSeparator)
} }
/**
* MCP客户端连接
*/
public async cleanup(): Promise<void> {
Logger.info('[MCP] Cleaning up all MCP clients...')
const closePromises: Promise<void>[] = []
// 关闭所有客户端连接
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() export default new McpService()

View File

@ -1,36 +1,35 @@
import { LoadingOutlined, PhoneOutlined } from '@ant-design/icons' import React, { useState } from 'react';
import { Button, Tooltip } from 'antd' import { Button, Tooltip } from 'antd';
import React, { useState } from 'react' import { PhoneOutlined, LoadingOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next';
import VoiceCallModal from './VoiceCallModal';
import { VoiceCallService } from '../services/VoiceCallService' import { VoiceCallService } from '../services/VoiceCallService';
import VoiceCallModal from './VoiceCallModal'
interface Props { interface Props {
disabled?: boolean disabled?: boolean;
style?: React.CSSProperties style?: React.CSSProperties;
} }
const VoiceCallButton: React.FC<Props> = ({ disabled = false, style }) => { const VoiceCallButton: React.FC<Props> = ({ disabled = false, style }) => {
const { t } = useTranslation() const { t } = useTranslation();
const [isModalVisible, setIsModalVisible] = useState(false) const [isModalVisible, setIsModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false);
const handleClick = async () => { const handleClick = async () => {
if (disabled || isLoading) return if (disabled || isLoading) return;
setIsLoading(true) setIsLoading(true);
try { try {
// 初始化语音服务 // 初始化语音服务
await VoiceCallService.initialize() await VoiceCallService.initialize();
setIsModalVisible(true) setIsModalVisible(true);
} catch (error) { } catch (error) {
console.error('Failed to initialize voice call:', error) console.error('Failed to initialize voice call:', error);
window.message.error(t('voice_call.initialization_failed')) window.message.error(t('voice_call.initialization_failed'));
} finally { } finally {
setIsLoading(false) setIsLoading(false);
} }
} };
return ( return (
<> <>
@ -43,9 +42,14 @@ const VoiceCallButton: React.FC<Props> = ({ disabled = false, style }) => {
style={style} style={style}
/> />
</Tooltip> </Tooltip>
{isModalVisible && <VoiceCallModal visible={isModalVisible} onClose={() => setIsModalVisible(false)} />} {isModalVisible && (
<VoiceCallModal
visible={isModalVisible}
onClose={() => setIsModalVisible(false)}
/>
)}
</> </>
) );
} };
export default VoiceCallButton export default VoiceCallButton;

View File

@ -1,3 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Modal, Button, Space, Tooltip } from 'antd';
import { import {
AudioMutedOutlined, AudioMutedOutlined,
AudioOutlined, AudioOutlined,
@ -5,128 +7,125 @@ import {
PauseCircleOutlined, PauseCircleOutlined,
PlayCircleOutlined, PlayCircleOutlined,
SoundOutlined SoundOutlined
} from '@ant-design/icons' } from '@ant-design/icons';
import { Button, Modal, Space, Tooltip } from 'antd' import styled from 'styled-components';
import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next';
import { useTranslation } from 'react-i18next' import VoiceVisualizer from './VoiceVisualizer';
import styled from 'styled-components' import { VoiceCallService } from '../services/VoiceCallService';
import { VoiceCallService } from '../services/VoiceCallService'
import VoiceVisualizer from './VoiceVisualizer'
interface Props { interface Props {
visible: boolean visible: boolean;
onClose: () => void onClose: () => void;
} }
const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => { const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
const { t } = useTranslation() const { t } = useTranslation();
const [isMuted, setIsMuted] = useState(false) const [isMuted, setIsMuted] = useState(false);
const [isPaused, setIsPaused] = useState(false) const [isPaused, setIsPaused] = useState(false);
const [transcript, setTranscript] = useState('') const [transcript, setTranscript] = useState('');
const [response, setResponse] = useState('') const [response, setResponse] = useState('');
const [isListening, setIsListening] = useState(false) const [isListening, setIsListening] = useState(false);
const [isSpeaking, setIsSpeaking] = useState(false) const [isSpeaking, setIsSpeaking] = useState(false);
const [isRecording, setIsRecording] = useState(false) const [isRecording, setIsRecording] = useState(false);
const [isProcessing, setIsProcessing] = useState(false) const [isProcessing, setIsProcessing] = useState(false);
const handleClose = useCallback(() => {
VoiceCallService.endCall()
onClose()
}, [onClose])
useEffect(() => { useEffect(() => {
const startVoiceCall = async () => { const startVoiceCall = async () => {
try { try {
await VoiceCallService.startCall({ await VoiceCallService.startCall({
onTranscript: (text: string) => setTranscript(text), onTranscript: (text) => setTranscript(text),
onResponse: (text: string) => setResponse(text), onResponse: (text) => setResponse(text),
onListeningStateChange: setIsListening, onListeningStateChange: setIsListening,
onSpeakingStateChange: setIsSpeaking onSpeakingStateChange: setIsSpeaking,
}) });
} catch (error) { } catch (error) {
console.error('Voice call error:', error) console.error('Voice call error:', error);
window.message.error(t('voice_call.error')) window.message.error(t('voice_call.error'));
handleClose() handleClose();
} }
} };
if (visible) { if (visible) {
startVoiceCall() startVoiceCall();
} }
return () => { return () => {
VoiceCallService.endCall() VoiceCallService.endCall();
} };
}, [visible, t, handleClose]) }, [visible, t]);
const handleClose = () => {
VoiceCallService.endCall();
onClose();
};
const toggleMute = () => { const toggleMute = () => {
const newMuteState = !isMuted const newMuteState = !isMuted;
setIsMuted(newMuteState) setIsMuted(newMuteState);
VoiceCallService.setMuted(newMuteState) VoiceCallService.setMuted(newMuteState);
} };
const togglePause = () => { const togglePause = () => {
const newPauseState = !isPaused const newPauseState = !isPaused;
setIsPaused(newPauseState) setIsPaused(newPauseState);
VoiceCallService.setPaused(newPauseState) VoiceCallService.setPaused(newPauseState);
} };
// 长按说话相关处理 // 长按说话相关处理
const handleRecordStart = async (e: React.MouseEvent | React.TouchEvent) => { const handleRecordStart = async (e: React.MouseEvent | React.TouchEvent) => {
e.preventDefault() // 防止触摸事件的默认行为 e.preventDefault(); // 防止触摸事件的默认行为
if (isProcessing || isPaused) return if (isProcessing || isPaused) return;
setIsRecording(true) setIsRecording(true);
await VoiceCallService.startRecording() await VoiceCallService.startRecording();
} };
const handleRecordEnd = async (e: React.MouseEvent | React.TouchEvent) => { const handleRecordEnd = async (e: React.MouseEvent | React.TouchEvent) => {
e.preventDefault() // 防止触摸事件的默认行为 e.preventDefault(); // 防止触摸事件的默认行为
if (!isRecording) return if (!isRecording) return;
// 立即更新UI状态 // 立即更新UI状态
setIsRecording(false) setIsRecording(false);
setIsProcessing(true) setIsProcessing(true);
// 确保录音完全停止 // 确保录音完全停止
try { try {
await VoiceCallService.stopRecording() await VoiceCallService.stopRecording();
console.log('录音已停止') console.log('录音已停止');
} catch (error) { } catch (error) {
console.error('停止录音出错:', error) console.error('停止录音出错:', error);
} }
// 处理结果会通过回调函数返回,不需要在这里处理 // 处理结果会通过回调函数返回,不需要在这里处理
setTimeout(() => { setTimeout(() => {
setIsProcessing(false) setIsProcessing(false);
}, 500) // 添加短暂延迟,防止用户立即再次点击 }, 500); // 添加短暂延迟,防止用户立即再次点击
} };
// 处理鼠标/触摸离开按钮的情况 // 处理鼠标/触摸离开按钮的情况
const handleRecordCancel = async (e: React.MouseEvent | React.TouchEvent) => { const handleRecordCancel = async (e: React.MouseEvent | React.TouchEvent) => {
e.preventDefault() e.preventDefault();
if (isRecording) { if (isRecording) {
// 立即更新UI状态 // 立即更新UI状态
setIsRecording(false) setIsRecording(false);
setIsProcessing(true) setIsProcessing(true);
// 取消录音不发送给AI // 取消录音不发送给AI
try { try {
await VoiceCallService.cancelRecording() await VoiceCallService.cancelRecording();
console.log('录音已取消') console.log('录音已取消');
} catch (error) { } catch (error) {
console.error('取消录音出错:', error) console.error('取消录音出错:', error);
} }
setTimeout(() => { setTimeout(() => {
setIsProcessing(false) setIsProcessing(false);
}, 500) }, 500);
} }
} };
return ( return (
<Modal <Modal
@ -136,7 +135,8 @@ const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
footer={null} footer={null}
width={500} width={500}
centered centered
maskClosable={false}> maskClosable={false}
>
<Container> <Container>
<VisualizerContainer> <VisualizerContainer>
<VoiceVisualizer isActive={isListening || isRecording} type="input" /> <VoiceVisualizer isActive={isListening || isRecording} type="input" />
@ -174,7 +174,7 @@ const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
/> />
<Tooltip title={t('voice_call.press_to_talk')}> <Tooltip title={t('voice_call.press_to_talk')}>
<RecordButton <RecordButton
type={isRecording ? 'primary' : 'default'} type={isRecording ? "primary" : "default"}
icon={<SoundOutlined />} icon={<SoundOutlined />}
onMouseDown={handleRecordStart} onMouseDown={handleRecordStart}
onMouseUp={handleRecordEnd} onMouseUp={handleRecordEnd}
@ -183,7 +183,8 @@ const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
onTouchEnd={handleRecordEnd} onTouchEnd={handleRecordEnd}
onTouchCancel={handleRecordCancel} onTouchCancel={handleRecordCancel}
size="large" size="large"
disabled={isProcessing || isPaused}> disabled={isProcessing || isPaused}
>
{isRecording ? t('voice_call.release_to_send') : t('voice_call.press_to_talk')} {isRecording ? t('voice_call.release_to_send') : t('voice_call.press_to_talk')}
</RecordButton> </RecordButton>
</Tooltip> </Tooltip>
@ -199,21 +200,21 @@ const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
</ControlsContainer> </ControlsContainer>
</Container> </Container>
</Modal> </Modal>
) );
} };
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
height: 400px; height: 400px;
` `;
const VisualizerContainer = styled.div` const VisualizerContainer = styled.div`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
height: 100px; height: 100px;
` `;
const TranscriptContainer = styled.div` const TranscriptContainer = styled.div`
flex: 1; flex: 1;
@ -222,33 +223,33 @@ const TranscriptContainer = styled.div`
border-radius: 8px; border-radius: 8px;
padding: 16px; padding: 16px;
background-color: var(--color-background-2); background-color: var(--color-background-2);
` `;
const TranscriptText = styled.p` const TranscriptText = styled.p`
margin-bottom: 8px; margin-bottom: 8px;
color: var(--color-text-1); color: var(--color-text-1);
` `;
const ResponseText = styled.p` const ResponseText = styled.p`
margin-bottom: 8px; margin-bottom: 8px;
color: var(--color-primary); color: var(--color-primary);
` `;
const UserLabel = styled.span` const UserLabel = styled.span`
font-weight: bold; font-weight: bold;
color: var(--color-text-1); color: var(--color-text-1);
` `;
const AILabel = styled.span` const AILabel = styled.span`
font-weight: bold; font-weight: bold;
color: var(--color-primary); color: var(--color-primary);
` `;
const ControlsContainer = styled.div` const ControlsContainer = styled.div`
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 10px 0; padding: 10px 0;
` `;
const RecordButton = styled(Button)` const RecordButton = styled(Button)`
min-width: 150px; min-width: 150px;
@ -257,6 +258,6 @@ const RecordButton = styled(Button)`
&:active { &:active {
transform: scale(0.95); transform: scale(0.95);
} }
` `;
export default VoiceCallModal export default VoiceCallModal;

View File

@ -1,74 +1,74 @@
import React, { useEffect, useRef } from 'react' import React, { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next' import styled from 'styled-components';
import styled from 'styled-components' import { useTranslation } from 'react-i18next';
interface Props { interface Props {
isActive: boolean isActive: boolean;
type: 'input' | 'output' type: 'input' | 'output';
} }
const VoiceVisualizer: React.FC<Props> = ({ isActive, type }) => { const VoiceVisualizer: React.FC<Props> = ({ isActive, type }) => {
const { t } = useTranslation() const { t } = useTranslation();
const canvasRef = useRef<HTMLCanvasElement>(null) const canvasRef = useRef<HTMLCanvasElement>(null);
const animationRef = useRef<number | undefined>(undefined) const animationRef = useRef<number | undefined>(undefined);
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current const canvas = canvasRef.current;
if (!canvas) return if (!canvas) return;
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d');
if (!ctx) return if (!ctx) return;
const width = canvas.width const width = canvas.width;
const height = canvas.height const height = canvas.height;
const drawVisualizer = () => { const drawVisualizer = () => {
ctx.clearRect(0, 0, width, height) ctx.clearRect(0, 0, width, height);
if (!isActive) { if (!isActive) {
// 绘制静态波形 // 绘制静态波形
ctx.beginPath() ctx.beginPath();
ctx.moveTo(0, height / 2) ctx.moveTo(0, height / 2);
ctx.lineTo(width, height / 2) ctx.lineTo(width, height / 2);
ctx.strokeStyle = type === 'input' ? 'var(--color-text-2)' : 'var(--color-primary)' ctx.strokeStyle = type === 'input' ? 'var(--color-text-2)' : 'var(--color-primary)';
ctx.lineWidth = 2 ctx.lineWidth = 2;
ctx.stroke() ctx.stroke();
return return;
} }
// 绘制动态波形 // 绘制动态波形
const barCount = 30 const barCount = 30;
const barWidth = width / barCount const barWidth = width / barCount;
const color = type === 'input' ? 'var(--color-text-1)' : 'var(--color-primary)' const color = type === 'input' ? 'var(--color-text-1)' : 'var(--color-primary)';
for (let i = 0; i < barCount; i++) { for (let i = 0; i < barCount; i++) {
const barHeight = Math.random() * (height / 2) + 10 const barHeight = Math.random() * (height / 2) + 10;
const x = i * barWidth const x = i * barWidth;
const y = height / 2 - barHeight / 2 const y = height / 2 - barHeight / 2;
ctx.fillStyle = color ctx.fillStyle = color;
ctx.fillRect(x, y, barWidth - 2, barHeight) ctx.fillRect(x, y, barWidth - 2, barHeight);
} }
animationRef.current = requestAnimationFrame(drawVisualizer) animationRef.current = requestAnimationFrame(drawVisualizer);
} };
drawVisualizer() drawVisualizer();
return () => { return () => {
if (animationRef.current) { if (animationRef.current) {
cancelAnimationFrame(animationRef.current) cancelAnimationFrame(animationRef.current);
} }
} };
}, [isActive, type]) }, [isActive, type]);
return ( return (
<Container $type={type}> <Container $type={type}>
<Label>{type === 'input' ? t('voice_call.you') : t('voice_call.ai')}</Label> <Label>{type === 'input' ? t('voice_call.you') : t('voice_call.ai')}</Label>
<Canvas ref={canvasRef} width={200} height={50} /> <Canvas ref={canvasRef} width={200} height={50} />
</Container> </Container>
) );
} };
const Container = styled.div<{ $type: 'input' | 'output' }>` const Container = styled.div<{ $type: 'input' | 'output' }>`
display: flex; display: flex;
@ -77,17 +77,21 @@ const Container = styled.div<{ $type: 'input' | 'output' }>`
width: 45%; width: 45%;
border-radius: 8px; border-radius: 8px;
padding: 10px; 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` const Label = styled.div`
margin-bottom: 8px; margin-bottom: 8px;
font-weight: bold; font-weight: bold;
` `;
const Canvas = styled.canvas` const Canvas = styled.canvas`
width: 100%; width: 100%;
height: 50px; height: 50px;
` `;
export default VoiceVisualizer export default VoiceVisualizer;