修复部分问题

This commit is contained in:
1600822305 2025-04-11 04:00:42 +08:00
parent 4c8751e56f
commit a8f18caf0e
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 { 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.
}

View File

@ -322,30 +322,6 @@ class McpService {
// 转换回字符串
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()

View File

@ -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<Props> = ({ 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<Props> = ({ disabled = false, style }) => {
style={style}
/>
</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 {
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<Props> = ({ 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 (
<Modal
@ -136,7 +135,8 @@ const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
footer={null}
width={500}
centered
maskClosable={false}>
maskClosable={false}
>
<Container>
<VisualizerContainer>
<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')}>
<RecordButton
type={isRecording ? 'primary' : 'default'}
type={isRecording ? "primary" : "default"}
icon={<SoundOutlined />}
onMouseDown={handleRecordStart}
onMouseUp={handleRecordEnd}
@ -183,7 +183,8 @@ const VoiceCallModal: React.FC<Props> = ({ 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')}
</RecordButton>
</Tooltip>
@ -199,21 +200,21 @@ const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
</ControlsContainer>
</Container>
</Modal>
)
}
);
};
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;

View File

@ -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<Props> = ({ isActive, type }) => {
const { t } = useTranslation()
const canvasRef = useRef<HTMLCanvasElement>(null)
const animationRef = useRef<number | undefined>(undefined)
const { t } = useTranslation();
const canvasRef = useRef<HTMLCanvasElement>(null);
const animationRef = useRef<number | undefined>(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 (
<Container $type={type}>
<Label>{type === 'input' ? t('voice_call.you') : t('voice_call.ai')}</Label>
<Canvas ref={canvasRef} width={200} height={50} />
</Container>
)
}
);
};
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;