coze-middleschool/src/components/Provider/RealtimeClientProvider.tsx

357 lines
9.7 KiB
TypeScript

import { ChatEventType, RoleType } from "@coze/api";
import {
EventNames,
RealtimeAPIError,
RealtimeClient,
RealtimeError,
RealtimeUtils,
} from "@coze/realtime-api";
import {
createContext,
ReactNode,
useCallback,
useContext,
useRef,
useState,
} from "react";
import { useToast } from "@/hooks/use-toast";
type RoomInfo = {
appId: string;
roomId: string;
token: string;
uid: string;
};
export const RealtimeClientContext = createContext<{
client: RealtimeClient | null;
isConnecting: boolean;
isConnected: boolean;
audioEnabled: boolean;
isSupportVideo: boolean;
messageList: { content: string; role: RoleType }[];
isAiTalking: boolean;
roomInfo: RoomInfo | null;
initClient: (initMessage?:string) => void;
handleConnect: (initMessage?:string) => Promise<void>;
handleInterrupt: () => void;
handleDisconnect: () => void;
toggleMicrophone: () => void;
}>({
client: null,
isConnecting: false,
isConnected: false,
audioEnabled: true,
isSupportVideo: false,
messageList: [],
isAiTalking: false,
roomInfo: null,
initClient: () => {},
handleConnect: () => Promise.resolve(),
handleInterrupt: () => {},
handleDisconnect: () => {},
toggleMicrophone: () => {},
});
// 添加自定义hook
export const useRealtimeClient = () => {
const context = useContext(RealtimeClientContext);
if (context === undefined) {
throw new Error("useRealtimeClient必须在RealtimeClientProvider内部使用");
}
return { ...context };
};
export const RealtimeClientProvider = ({
children,
}: {
children: ReactNode;
}) => {
const token = import.meta.env.VITE_COZE_TOKEN;
const botId = import.meta.env.VITE_COZE_BOT_ID;
const voiceId = import.meta.env.VITE_COZE_VOICE_ID;
const connectorId = "1024";
const clientRef = useRef<RealtimeClient | null>(null);
// 实时语音回复消息列表
const [messageList, setMessageList] = useState<
{ content: string; role: RoleType }[]
>([]);
// 是否正在连接
const [isConnecting, setIsConnecting] = useState(false);
// 是否已连接
const [isConnected, setIsConnected] = useState(false);
// 是否开启麦克风
const [audioEnabled, setAudioEnabled] = useState(true);
// 是否支持视频
const [isSupportVideo] = useState(false);
// 是否正在说话
const [isAiTalking, setIsAiTalking] = useState(false);
const [roomInfo, setRoomInfo] = useState<RoomInfo | null>(null);
const { toast } = useToast();
const initClient = async (_initMessage?:string) => {
const permission = await RealtimeUtils.checkDevicePermission(false);
const device = await RealtimeUtils.getAudioDevices();
if (!permission.audio) {
toast({
title: "连接错误",
description: "需要麦克风访问权限",
});
throw new Error("需要麦克风访问权限");
}
if (device.audioInputs.length === 0) {
toast({
title: "连接错误",
description: "没有麦克风设备",
});
throw new Error("没有麦克风设备");
}
const client = new RealtimeClient({
accessToken: token,
botId: botId,
voiceId: voiceId,
connectorId: connectorId,
allowPersonalAccessTokenInBrowser: true, // 可选:允许在浏览器中使用个人访问令牌
});
clientRef.current = client;
setupEventListeners(client);
setupMessageEventListeners(client,_initMessage ?? '');
setupInitMessageEventListener(client,_initMessage)
};
const handleConnect = async (initMessage?:string) => {
try {
if (!clientRef.current) {
await initClient(initMessage);
}
await clientRef.current?.connect();
await toggleMicrophone();
} catch (error) {
console.error(error);
if (error instanceof RealtimeAPIError) {
switch (error.code) {
case RealtimeError.CREATE_ROOM_ERROR:
console.error(`创建房间失败: ${error.message}`);
break;
case RealtimeError.CONNECTION_ERROR:
console.error(`加入房间失败: ${error.message}`);
break;
case RealtimeError.DEVICE_ACCESS_ERROR:
console.error(`获取设备失败: ${error.message}`);
break;
default:
console.error(`连接错误: ${error.message}`);
}
} else {
console.error("连接错误:" + error);
}
}
};
const handleInterrupt = () => {
try {
clientRef.current?.interrupt();
} catch (error) {
console.error("打断失败:" + error);
}
};
const handleDisconnect = async() => {
try {
// 关闭客户的时候清除一些信息
setIsAiTalking(false);
setMessageList([]);
await clientRef.current?.setAudioEnable(false);
setAudioEnabled(false);
clientRef.current?.disconnect();
clientRef.current?.clearEventHandlers();
clientRef.current = null;
setIsConnected(false);
} catch (error) {
console.error("断开失败:" + error);
}
};
const toggleMicrophone = async () => {
try {
await clientRef.current?.setAudioEnable(!audioEnabled);
setAudioEnabled(!audioEnabled);
} catch (error) {
console.error("切换麦克风状态失败:" + error);
}
};
const setupInitMessageEventListener = useCallback((client: RealtimeClient,_initMessage?:string) => {
client.on(EventNames.ALL_SERVER, async(eventName, _event: any) => {
if (eventName === "server.session.created") {
await client.sendMessage({
id:'',
"event_type":"session.update",
data:{
chat_config:{
allow_voice_interrupt:false
}
}
})
}
if(eventName === "server.bot.join" && _initMessage){
// 这里需要加个 server. 前缀
await clientRef.current?.sendMessage({
id: "",
event_type: "conversation.message.create",
data: {
role: "user",
content_type: "text",
content: _initMessage,
},
});
}
});
},[clientRef.current]);
const setupMessageEventListeners = (client: RealtimeClient,_initMessage:string) => {
let lastEvent: any;
client.on(EventNames.ALL, (_eventName, event: any) => {
// AI智能体设置
if (
event.event_type !== ChatEventType.CONVERSATION_MESSAGE_DELTA &&
event.event_type !== ChatEventType.CONVERSATION_MESSAGE_COMPLETED &&
event.event_type !== "conversation.created"
) {
return;
}
const content = event.data.content;
setMessageList((prev) => {
// 如果上一个事件是增量更新,则附加到最后一条消息
if (
lastEvent?.event_type === ChatEventType.CONVERSATION_MESSAGE_DELTA &&
(event.data.type === "answer" || event.data.type === "question")
) {
return [
...prev.slice(0, -1),
{
content: prev[prev.length - 1].content + content,
role: prev[prev.length - 1].role,
},
];
}
// 添加AI的欢迎语
if (_initMessage === "" && event.event_type === "conversation.created") {
return [
...prev,
{ content: event.data.prologue, role: RoleType.Assistant },
];
}
// 否则添加新消息
if (
(content !== "" &&
event.event_type === ChatEventType.CONVERSATION_MESSAGE_DELTA) ||
(event.event_type === ChatEventType.CONVERSATION_MESSAGE_COMPLETED &&
(event.data.type === "answer" || event.data.type === "question") &&
event.data.role !== RoleType.Assistant)
) {
return [...prev, { content: content, role: event.data.role }];
}
return prev;
});
lastEvent = event;
});
};
// 设置事件监听器
const setupEventListeners = useCallback(
(client: RealtimeClient) => {
// 监听 AI 开始说话事件
client.on(EventNames.AUDIO_AGENT_SPEECH_STARTED, async() => {
// console.log("AI开始说话");
setIsAiTalking(true);
await clientRef.current?.setAudioEnable(false);
setAudioEnabled(false);
});
// 监听 AI 结束说话事件
client.on(EventNames.AUDIO_AGENT_SPEECH_STOPPED, async() => {
// console.log("AI结束说话");
setIsAiTalking(false);
await clientRef.current?.setAudioEnable(true);
setAudioEnabled(true);
});
// 监听连接客户端
client.on(EventNames.CONNECTING, () => {
setIsConnecting(true);
setIsConnected(false);
});
// 客户端连接成功
client.on(EventNames.CONNECTED, (_eventName: string, event: any) => {
setRoomInfo(event);
setIsConnecting(false);
setIsConnected(true);
});
},
[clientRef.current]
);
// 发送信息
// const sendUserMessageWithText = async (message: string) => {
// try {
// await clientRef.current?.sendMessage({
// id: "",
// event_type: "conversation.message.create",
// data: {
// role: "user",
// content_type: "text",
// content: message,
// },
// });
// } catch (error) {
// console.error("发送消息失败:" + error);
// }
// };
return (
<RealtimeClientContext.Provider
value={{
client: clientRef.current,
isConnecting,
isConnected,
audioEnabled,
isSupportVideo,
messageList,
isAiTalking,
roomInfo,
initClient,
handleConnect,
handleInterrupt,
handleDisconnect,
toggleMicrophone,
}}
>
{children}
</RealtimeClientContext.Provider>
);
};