fix: 修复插件调用的处理
parent
e1acc4973a
commit
207f28c728
|
|
@ -14,7 +14,6 @@ import {
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { FileParser, FileParseStatus } from "./fileParser";
|
|
||||||
|
|
||||||
type RoomInfo = {
|
type RoomInfo = {
|
||||||
appId: string;
|
appId: string;
|
||||||
|
|
@ -48,7 +47,10 @@ export const RealtimeClientContext = createContext<{
|
||||||
roomInfo: RoomInfo | null;
|
roomInfo: RoomInfo | null;
|
||||||
|
|
||||||
initClient: (opts: { initMessage?: string; fileInfo?: FileInfo }) => void;
|
initClient: (opts: { initMessage?: string; fileInfo?: FileInfo }) => void;
|
||||||
handleConnect: (opts: { initMessage?: string; fileInfo?: FileInfo }) => Promise<void>;
|
handleConnect: (opts: {
|
||||||
|
initMessage?: string;
|
||||||
|
fileInfo?: FileInfo;
|
||||||
|
}) => Promise<void>;
|
||||||
handleInterrupt: () => void;
|
handleInterrupt: () => void;
|
||||||
handleDisconnect: () => void;
|
handleDisconnect: () => void;
|
||||||
toggleMicrophone: () => void;
|
toggleMicrophone: () => void;
|
||||||
|
|
@ -56,11 +58,16 @@ export const RealtimeClientContext = createContext<{
|
||||||
|
|
||||||
export const useRealtimeClient = () => {
|
export const useRealtimeClient = () => {
|
||||||
const ctx = useContext(RealtimeClientContext);
|
const ctx = useContext(RealtimeClientContext);
|
||||||
if (!ctx) throw new Error("useRealtimeClient 必须在 RealtimeClientProvider 内部使用");
|
if (!ctx)
|
||||||
|
throw new Error("useRealtimeClient 必须在 RealtimeClientProvider 内部使用");
|
||||||
return ctx;
|
return ctx;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RealtimeClientProvider = ({ children }: { children: ReactNode }) => {
|
export const RealtimeClientProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) => {
|
||||||
const token = import.meta.env.VITE_COZE_TOKEN;
|
const token = import.meta.env.VITE_COZE_TOKEN;
|
||||||
const botId = import.meta.env.VITE_COZE_BOT_ID;
|
const botId = import.meta.env.VITE_COZE_BOT_ID;
|
||||||
const voiceId = import.meta.env.VITE_COZE_VOICE_ID;
|
const voiceId = import.meta.env.VITE_COZE_VOICE_ID;
|
||||||
|
|
@ -69,13 +76,15 @@ export const RealtimeClientProvider = ({ children }: { children: ReactNode }) =>
|
||||||
const clientRef = useRef<RealtimeClient | null>(null);
|
const clientRef = useRef<RealtimeClient | null>(null);
|
||||||
const connectingLockRef = useRef(false);
|
const connectingLockRef = useRef(false);
|
||||||
|
|
||||||
const [messageList, setMessageList] = useState<{
|
const [messageList, setMessageList] = useState<
|
||||||
content: string;
|
{
|
||||||
role: RoleType;
|
content: string;
|
||||||
event?: any;
|
role: RoleType;
|
||||||
fileInfo?: FileInfo;
|
event?: any;
|
||||||
fileParseStatus?: number;
|
fileInfo?: FileInfo;
|
||||||
}[]>([]);
|
fileParseStatus?: number;
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [audioEnabled, setAudioEnabled] = useState(true);
|
const [audioEnabled, setAudioEnabled] = useState(true);
|
||||||
|
|
@ -83,42 +92,8 @@ export const RealtimeClientProvider = ({ children }: { children: ReactNode }) =>
|
||||||
const [isAiTalking, setIsAiTalking] = useState(false);
|
const [isAiTalking, setIsAiTalking] = useState(false);
|
||||||
const [roomInfo, setRoomInfo] = useState<RoomInfo | null>(null);
|
const [roomInfo, setRoomInfo] = useState<RoomInfo | null>(null);
|
||||||
|
|
||||||
// 引入状态机
|
|
||||||
const fileParseStatusRef = useRef<FileParseStatus>(-1);
|
|
||||||
const fileParserRef = useRef(
|
|
||||||
new FileParser((newStatus:any) => {
|
|
||||||
fileParseStatusRef.current = newStatus;
|
|
||||||
// 根据不同状态更新 messageList
|
|
||||||
if (newStatus === 0) {
|
|
||||||
appendAssistantMessage("AI正在解析您的文档...");
|
|
||||||
} else if (newStatus === 1) {
|
|
||||||
replaceLastAssistantMessage("AI正在调用插件");
|
|
||||||
} else if (newStatus === 2) {
|
|
||||||
replaceLastAssistantMessage("文档解析完成");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
/** Helpers */
|
|
||||||
const appendAssistantMessage = (content: string) => {
|
|
||||||
setMessageList(prev => {
|
|
||||||
return [
|
|
||||||
...prev,
|
|
||||||
{ content, role: RoleType.Assistant }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const replaceLastAssistantMessage = (content: string) => {
|
|
||||||
setMessageList(prev => {
|
|
||||||
return [
|
|
||||||
...prev.slice(0, -1),
|
|
||||||
{ content, role: RoleType.Assistant }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 初始化客户端并设置监听 */
|
/** 初始化客户端并设置监听 */
|
||||||
const initClient = async ({
|
const initClient = async ({
|
||||||
initMessage,
|
initMessage,
|
||||||
|
|
@ -147,7 +122,7 @@ export const RealtimeClientProvider = ({ children }: { children: ReactNode }) =>
|
||||||
allowPersonalAccessTokenInBrowser: true,
|
allowPersonalAccessTokenInBrowser: true,
|
||||||
suppressStationaryNoise: true,
|
suppressStationaryNoise: true,
|
||||||
suppressNonStationaryNoise: true,
|
suppressNonStationaryNoise: true,
|
||||||
debug: true,
|
debug: false,
|
||||||
});
|
});
|
||||||
clientRef.current = client;
|
clientRef.current = client;
|
||||||
|
|
||||||
|
|
@ -246,7 +221,10 @@ export const RealtimeClientProvider = ({ children }: { children: ReactNode }) =>
|
||||||
role: "user",
|
role: "user",
|
||||||
content_type: "object_string",
|
content_type: "object_string",
|
||||||
content: JSON.stringify([
|
content: JSON.stringify([
|
||||||
{ type: "text", text: "帮我解读这个文件,结合当下的专业行情以及对该专业未来的发展趋势,简介的给出志愿建议" },
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "帮我解读这个文件,结合当下的专业行情以及对该专业未来的发展趋势,简介的给出志愿建议",
|
||||||
|
},
|
||||||
{ type: "image", file_url: fileInfo.url },
|
{ type: "image", file_url: fileInfo.url },
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|
@ -265,7 +243,6 @@ export const RealtimeClientProvider = ({ children }: { children: ReactNode }) =>
|
||||||
) => {
|
) => {
|
||||||
client.on(EventNames.ALL, (_eventName, event: any) => {
|
client.on(EventNames.ALL, (_eventName, event: any) => {
|
||||||
// 交给状态机处理解析流程
|
// 交给状态机处理解析流程
|
||||||
fileParserRef.current.handleEvent(_eventName, event);
|
|
||||||
|
|
||||||
// 普通消息流处理
|
// 普通消息流处理
|
||||||
if (
|
if (
|
||||||
|
|
@ -273,14 +250,18 @@ export const RealtimeClientProvider = ({ children }: { children: ReactNode }) =>
|
||||||
event.event_type !== ChatEventType.CONVERSATION_MESSAGE_COMPLETED
|
event.event_type !== ChatEventType.CONVERSATION_MESSAGE_COMPLETED
|
||||||
) {
|
) {
|
||||||
// 处理conversation.created事件
|
// 处理conversation.created事件
|
||||||
if (event.event_type === "conversation.created" && !opts.initMessage && !opts.fileInfo) {
|
if (
|
||||||
setMessageList(prev => [
|
event.event_type === "conversation.created" &&
|
||||||
|
!opts.initMessage &&
|
||||||
|
!opts.fileInfo
|
||||||
|
) {
|
||||||
|
setMessageList((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
content: event.data.prologue,
|
content: event.data.prologue,
|
||||||
role: RoleType.Assistant,
|
role: RoleType.Assistant,
|
||||||
event
|
event,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -288,63 +269,54 @@ export const RealtimeClientProvider = ({ children }: { children: ReactNode }) =>
|
||||||
|
|
||||||
// 如果是assistant的completed消息或verbose类型,直接返回
|
// 如果是assistant的completed消息或verbose类型,直接返回
|
||||||
if (
|
if (
|
||||||
(event.data.role === "assistant" && event.event_type === ChatEventType.CONVERSATION_MESSAGE_COMPLETED &&event.data.type === "verbose" ) ||
|
(event.data.role === "assistant" &&
|
||||||
event.data.type === "answer" && event.event_type === ChatEventType.CONVERSATION_MESSAGE_COMPLETED
|
event.event_type === ChatEventType.CONVERSATION_MESSAGE_COMPLETED &&
|
||||||
|
event.data.type === "verbose") ||
|
||||||
|
(event.data.type === "answer" &&
|
||||||
|
event.event_type === ChatEventType.CONVERSATION_MESSAGE_COMPLETED)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有fileInfo,过滤掉function_call和tool_response类型的消息
|
|
||||||
if (!opts.fileInfo && (event.data.type === "function_call" || event.data.type === "tool_response")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = event.data.content;
|
const content = event.data.content;
|
||||||
setMessageList(prev => {
|
setMessageList((prev) => {
|
||||||
// 合并增量
|
// 如果是工具调用相关的消息,不添加到消息列表
|
||||||
// 处理工具回调结果
|
if (
|
||||||
try {
|
event.data.type === "function_call" ||
|
||||||
const parsedContent = JSON.parse(content);
|
event.data.type === "tool_response"
|
||||||
if (parsedContent.msg_type === "time_capsule_recall" ||
|
) {
|
||||||
(parsedContent.name && parsedContent.arguments)) {
|
const jsonContent = JSON.parse(event.data.content);
|
||||||
// 如果没有fileInfo,不创建假信息
|
const lastMessage = prev[prev.length - 1];
|
||||||
if (!opts.fileInfo) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否已存在工具回调消息
|
if (jsonContent.name === "doc_reader-PDF_reader") {
|
||||||
const existingToolMessageIndex = prev.findIndex(msg =>
|
return [
|
||||||
msg.content === "正在处理您的请求..." &&
|
...prev,
|
||||||
msg.fileInfo === opts.fileInfo
|
{
|
||||||
);
|
content: "",
|
||||||
|
role: RoleType.Assistant,
|
||||||
if (existingToolMessageIndex !== -1) {
|
fileInfo: opts.fileInfo,
|
||||||
// 更新已存在的消息的fileParseStatus
|
fileParseStatus: 1,
|
||||||
const newStatus = parsedContent.msg_type === "time_capsule_recall" ? 0 : 2;
|
event,
|
||||||
return [
|
},
|
||||||
...prev.slice(0, existingToolMessageIndex),
|
];
|
||||||
{
|
}
|
||||||
...prev[existingToolMessageIndex],
|
else if (
|
||||||
fileParseStatus: newStatus
|
lastMessage.event.type === "function_call" &&
|
||||||
},
|
event.data.type === "tool_response"
|
||||||
...prev.slice(existingToolMessageIndex + 1)
|
) {
|
||||||
];
|
return [
|
||||||
} else {
|
...prev.slice(0, prev.length - 2),
|
||||||
// 创建新的工具回调消息
|
{
|
||||||
return [
|
content: "",
|
||||||
...prev,
|
role: RoleType.Assistant,
|
||||||
{
|
fileInfo: opts.fileInfo,
|
||||||
content: "正在处理您的请求...",
|
fileParseStatus: 2,
|
||||||
role: event.data.role,
|
event,
|
||||||
event,
|
},
|
||||||
fileInfo: opts.fileInfo,
|
];
|
||||||
fileParseStatus: parsedContent.msg_type === "time_capsule_recall" ? 0 : 2
|
}else{
|
||||||
},
|
return [...prev]
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
// 如果不是JSON格式,继续正常处理
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -352,8 +324,7 @@ export const RealtimeClientProvider = ({ children }: { children: ReactNode }) =>
|
||||||
prev[prev.length - 1].event?.event_type ===
|
prev[prev.length - 1].event?.event_type ===
|
||||||
ChatEventType.CONVERSATION_MESSAGE_DELTA &&
|
ChatEventType.CONVERSATION_MESSAGE_DELTA &&
|
||||||
event.event_type === ChatEventType.CONVERSATION_MESSAGE_DELTA &&
|
event.event_type === ChatEventType.CONVERSATION_MESSAGE_DELTA &&
|
||||||
prev[prev.length - 1].event.data.answer_id ===
|
prev[prev.length - 1].event.data.answer_id === event.data.answer_id
|
||||||
event.data.answer_id
|
|
||||||
) {
|
) {
|
||||||
return [
|
return [
|
||||||
...prev.slice(0, -1),
|
...prev.slice(0, -1),
|
||||||
|
|
@ -363,44 +334,39 @@ export const RealtimeClientProvider = ({ children }: { children: ReactNode }) =>
|
||||||
event,
|
event,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
} else {
|
||||||
|
// 新消息追加
|
||||||
|
return [...prev, { content, role: event.data.role, event }];
|
||||||
}
|
}
|
||||||
// 新消息追加
|
|
||||||
return [
|
|
||||||
...prev,
|
|
||||||
{ content, role: event.data.role, event },
|
|
||||||
];
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 基本连接状态 & 语音事件监听 */
|
/** 基本连接状态 & 语音事件监听 */
|
||||||
const setupEventListeners = useCallback(
|
const setupEventListeners = useCallback((client: RealtimeClient) => {
|
||||||
(client: RealtimeClient) => {
|
client.on(EventNames.AUDIO_AGENT_SPEECH_STARTED, async () => {
|
||||||
client.on(EventNames.AUDIO_AGENT_SPEECH_STARTED, async () => {
|
setIsAiTalking(true);
|
||||||
setIsAiTalking(true);
|
await clientRef.current?.setAudioEnable(false);
|
||||||
await clientRef.current?.setAudioEnable(false);
|
setAudioEnabled(false);
|
||||||
setAudioEnabled(false);
|
});
|
||||||
});
|
client.on(EventNames.AUDIO_AGENT_SPEECH_STOPPED, async () => {
|
||||||
client.on(EventNames.AUDIO_AGENT_SPEECH_STOPPED, async () => {
|
setIsAiTalking(false);
|
||||||
setIsAiTalking(false);
|
await clientRef.current?.setAudioEnable(true);
|
||||||
await clientRef.current?.setAudioEnable(true);
|
setAudioEnabled(true);
|
||||||
setAudioEnabled(true);
|
});
|
||||||
});
|
client.on(EventNames.CONNECTING, () => {
|
||||||
client.on(EventNames.CONNECTING, () => {
|
setIsConnecting(true);
|
||||||
setIsConnecting(true);
|
setIsConnected(false);
|
||||||
setIsConnected(false);
|
});
|
||||||
});
|
client.on(EventNames.CONNECTED, (_name, evt) => {
|
||||||
client.on(EventNames.CONNECTED, (_name, evt) => {
|
setRoomInfo(evt as RoomInfo);
|
||||||
setRoomInfo(evt as RoomInfo);
|
setIsConnecting(false);
|
||||||
setIsConnecting(false);
|
setIsConnected(true);
|
||||||
setIsConnected(true);
|
});
|
||||||
});
|
client.on(EventNames.ALL_SERVER, (_name, _evt) => {
|
||||||
client.on(EventNames.ALL_SERVER, (_name, _evt) => {
|
// 其它全局服务端事件可在此处理
|
||||||
// 其它全局服务端事件可在此处理
|
});
|
||||||
});
|
}, []);
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RealtimeClientContext.Provider
|
<RealtimeClientContext.Provider
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue