mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-25 03:10:08 +08:00
修复部分问题
This commit is contained in:
parent
4c8751e56f
commit
a8f18caf0e
@ -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.
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user