
























@@ -892,14 +892,34 @@ describe("DiscordVoiceManager", () => {
892892const bridgeParams = createRealtimeVoiceBridgeSessionMock.mock.calls.at(-1)?.[0] as
893893| {
894894autoRespondToAudio?: boolean;
895-tools?: unknown[];
896-onTranscript?: (role: "user" | "assistant", text: string, isFinal: boolean) => void;
895+instructions?: string;
896+tools?: Array<{ name: string }>;
897+onToolCall?: (
898+event: {
899+itemId: string;
900+callId: string;
901+name: string;
902+args: unknown;
903+},
904+session: typeof realtimeSessionMock,
905+) => void;
897906}
898907| undefined;
899-expect(bridgeParams?.autoRespondToAudio).toBe(false);
900-expect(bridgeParams?.tools).toStrictEqual([]);
908+expect(bridgeParams?.autoRespondToAudio).toBe(true);
909+expect(bridgeParams?.instructions).toContain("same OpenClaw agent");
910+expect(bridgeParams?.tools?.map((tool) => tool.name)).toContain("openclaw_agent_consult");
901911902-bridgeParams?.onTranscript?.("user", "what did I ask?", true);
912+bridgeParams?.onToolCall?.(
913+{
914+itemId: "item-1",
915+callId: "call-1",
916+name: "openclaw_agent_consult",
917+args: { question: "what did I ask?" },
918+},
919+realtimeSessionMock,
920+);
921+await Promise.resolve();
922+await Promise.resolve();
903923await new Promise((resolve) => setTimeout(resolve, 20));
904924905925expect(agentCommandMock).toHaveBeenCalledWith(
@@ -910,9 +930,76 @@ describe("DiscordVoiceManager", () => {
910930}),
911931expect.anything(),
912932);
913-expect(realtimeSessionMock.sendUserMessage).toHaveBeenCalledWith(
914-expect.stringContaining("agent proxy answer"),
933+expect(realtimeSessionMock.submitToolResult).toHaveBeenCalledWith(
934+"call-1",
935+expect.objectContaining({ status: "working" }),
936+{ willContinue: true },
915937);
938+expect(realtimeSessionMock.submitToolResult).toHaveBeenCalledWith("call-1", {
939+text: "agent proxy answer",
940+});
941+});
942+943+it("does not require speaker context for internal exact-speech consults", async () => {
944+const manager = createManager({
945+groupPolicy: "open",
946+voice: {
947+enabled: true,
948+mode: "agent-proxy",
949+realtime: { provider: "openai" },
950+},
951+});
952+953+await manager.join({ guildId: "g1", channelId: "1001" });
954+const bridgeParams = createRealtimeVoiceBridgeSessionMock.mock.calls.at(-1)?.[0] as
955+| {
956+onToolCall?: (
957+event: {
958+itemId: string;
959+callId: string;
960+name: string;
961+args: unknown;
962+},
963+session: typeof realtimeSessionMock,
964+) => void;
965+}
966+| undefined;
967+968+bridgeParams?.onToolCall?.(
969+{
970+itemId: "item-exact",
971+callId: "call-exact",
972+name: "openclaw_agent_consult",
973+args: {
974+question: "Speak the provided exact answer verbatim to the Discord voice channel.",
975+context: 'Provided answer text: "already answered"\\nSpoken style: verbatim only',
976+},
977+},
978+realtimeSessionMock,
979+);
980+bridgeParams?.onToolCall?.(
981+{
982+itemId: "item-internal",
983+callId: "call-internal",
984+name: "openclaw_agent_consult",
985+args: {
986+question: [
987+"Speak this exact OpenClaw answer to the Discord voice channel, without adding, removing, or rephrasing words.",
988+'Answer: "direct internal answer"',
989+].join("\n"),
990+},
991+},
992+realtimeSessionMock,
993+);
994+995+expect(agentCommandMock).not.toHaveBeenCalled();
996+expect(realtimeSessionMock.submitToolResult).toHaveBeenCalledTimes(2);
997+expect(realtimeSessionMock.submitToolResult).toHaveBeenCalledWith("call-exact", {
998+text: "already answered",
999+});
1000+expect(realtimeSessionMock.submitToolResult).toHaveBeenCalledWith("call-internal", {
1001+text: "direct internal answer",
1002+});
9161003});
91710049181005it("creates a fresh realtime output stream after the Discord player idles", async () => {
@@ -1018,26 +1105,189 @@ describe("DiscordVoiceManager", () => {
10181105"u-guest",
10191106);
10201107nonOwnerTurn?.sendInputAudio(Buffer.alloc(8));
1108+1109+const bridgeParams = createRealtimeVoiceBridgeSessionMock.mock.calls.at(-1)?.[0] as
1110+| {
1111+onTranscript?: (role: "user" | "assistant", text: string, isFinal: boolean) => void;
1112+}
1113+| undefined;
1114+bridgeParams?.onTranscript?.("user", "non-owner question", true);
10211115const ownerTurn = entry?.realtime?.beginSpeakerTurn(
10221116{ extraSystemPrompt: undefined, senderIsOwner: true, speakerLabel: "Owner" },
10231117"u-owner",
10241118);
10251119ownerTurn?.sendInputAudio(Buffer.alloc(8));
1120+await new Promise((resolve) => setTimeout(resolve, 260));
1121+1122+expect(agentCommandMock).toHaveBeenCalledWith(
1123+expect.objectContaining({
1124+senderIsOwner: false,
1125+}),
1126+expect.anything(),
1127+);
1128+expect(realtimeSessionMock.handleBargeIn).toHaveBeenCalledWith({
1129+audioPlaybackActive: true,
1130+force: true,
1131+});
1132+expect(realtimeSessionMock.sendUserMessage).toHaveBeenCalledWith(
1133+expect.stringContaining("non-owner answer"),
1134+);
1135+});
102611361137+it("keeps separate forced agent-proxy fallback timers for rapid transcripts", async () => {
1138+agentCommandMock
1139+.mockResolvedValueOnce({ payloads: [{ text: "guest answer" }] })
1140+.mockResolvedValueOnce({ payloads: [{ text: "owner answer" }] });
1141+const manager = createManager({
1142+groupPolicy: "open",
1143+voice: {
1144+enabled: true,
1145+mode: "agent-proxy",
1146+realtime: { provider: "openai" },
1147+},
1148+});
1149+1150+await manager.join({ guildId: "g1", channelId: "1001" });
1151+const entry = getSessionEntry(manager) as {
1152+realtime?: {
1153+beginSpeakerTurn: (
1154+context: { extraSystemPrompt?: string; senderIsOwner: boolean; speakerLabel: string },
1155+userId: string,
1156+) => { close: () => void; sendInputAudio: (audio: Buffer) => void };
1157+};
1158+};
10271159const bridgeParams = createRealtimeVoiceBridgeSessionMock.mock.calls.at(-1)?.[0] as
10281160| {
10291161onTranscript?: (role: "user" | "assistant", text: string, isFinal: boolean) => void;
10301162}
10311163| undefined;
1032-bridgeParams?.onTranscript?.("user", "non-owner question", true);
1033-await new Promise((resolve) => setTimeout(resolve, 20));
103411641035-expect(agentCommandMock).toHaveBeenCalledWith(
1165+const guestTurn = entry.realtime?.beginSpeakerTurn(
1166+{ extraSystemPrompt: undefined, senderIsOwner: false, speakerLabel: "Guest" },
1167+"u-guest",
1168+);
1169+guestTurn?.sendInputAudio(Buffer.alloc(8));
1170+bridgeParams?.onTranscript?.("user", "guest question", true);
1171+1172+const ownerTurn = entry.realtime?.beginSpeakerTurn(
1173+{ extraSystemPrompt: undefined, senderIsOwner: true, speakerLabel: "Owner" },
1174+"u-owner",
1175+);
1176+ownerTurn?.sendInputAudio(Buffer.alloc(8));
1177+bridgeParams?.onTranscript?.("user", "owner question", true);
1178+1179+await new Promise((resolve) => setTimeout(resolve, 260));
1180+1181+expect(agentCommandMock).toHaveBeenNthCalledWith(
1182+1,
1183+expect.objectContaining({
1184+message: expect.stringContaining("guest question"),
1185+senderIsOwner: false,
1186+}),
1187+expect.anything(),
1188+);
1189+expect(agentCommandMock).toHaveBeenNthCalledWith(
1190+2,
1191+expect.objectContaining({
1192+message: expect.stringContaining("owner question"),
1193+senderIsOwner: true,
1194+}),
1195+expect.anything(),
1196+);
1197+expect(realtimeSessionMock.sendUserMessage).toHaveBeenCalledWith(
1198+expect.stringContaining("guest answer"),
1199+);
1200+expect(realtimeSessionMock.sendUserMessage).toHaveBeenCalledWith(
1201+expect.stringContaining("owner answer"),
1202+);
1203+});
1204+1205+it("matches agent-proxy consult tool calls to the pending transcript", async () => {
1206+agentCommandMock
1207+.mockResolvedValueOnce({ payloads: [{ text: "owner answer" }] })
1208+.mockResolvedValueOnce({ payloads: [{ text: "guest fallback answer" }] });
1209+const manager = createManager({
1210+groupPolicy: "open",
1211+voice: {
1212+enabled: true,
1213+mode: "agent-proxy",
1214+realtime: { provider: "openai" },
1215+},
1216+});
1217+1218+await manager.join({ guildId: "g1", channelId: "1001" });
1219+const entry = getSessionEntry(manager) as {
1220+realtime?: {
1221+beginSpeakerTurn: (
1222+context: { extraSystemPrompt?: string; senderIsOwner: boolean; speakerLabel: string },
1223+userId: string,
1224+) => { close: () => void; sendInputAudio: (audio: Buffer) => void };
1225+};
1226+};
1227+const bridgeParams = createRealtimeVoiceBridgeSessionMock.mock.calls.at(-1)?.[0] as
1228+| {
1229+onToolCall?: (
1230+event: {
1231+itemId: string;
1232+callId: string;
1233+name: string;
1234+args: unknown;
1235+},
1236+session: typeof realtimeSessionMock,
1237+) => void;
1238+onTranscript?: (role: "user" | "assistant", text: string, isFinal: boolean) => void;
1239+}
1240+| undefined;
1241+1242+const guestTurn = entry.realtime?.beginSpeakerTurn(
1243+{ extraSystemPrompt: undefined, senderIsOwner: false, speakerLabel: "Guest" },
1244+"u-guest",
1245+);
1246+guestTurn?.sendInputAudio(Buffer.alloc(8));
1247+bridgeParams?.onTranscript?.("user", "guest question", true);
1248+1249+const ownerTurn = entry.realtime?.beginSpeakerTurn(
1250+{ extraSystemPrompt: undefined, senderIsOwner: true, speakerLabel: "Owner" },
1251+"u-owner",
1252+);
1253+ownerTurn?.sendInputAudio(Buffer.alloc(8));
1254+bridgeParams?.onTranscript?.("user", "owner question", true);
1255+1256+bridgeParams?.onToolCall?.(
1257+{
1258+itemId: "item-owner",
1259+callId: "call-owner",
1260+name: "openclaw_agent_consult",
1261+args: { question: "owner question" },
1262+},
1263+realtimeSessionMock,
1264+);
1265+await Promise.resolve();
1266+await Promise.resolve();
1267+await new Promise((resolve) => setTimeout(resolve, 260));
1268+1269+expect(agentCommandMock).toHaveBeenNthCalledWith(
1270+1,
1271+expect.objectContaining({
1272+message: expect.stringContaining("owner question"),
1273+senderIsOwner: true,
1274+}),
1275+expect.anything(),
1276+);
1277+expect(agentCommandMock).toHaveBeenNthCalledWith(
1278+2,
10361279expect.objectContaining({
1280+message: expect.stringContaining("guest question"),
10371281senderIsOwner: false,
10381282}),
10391283expect.anything(),
10401284);
1285+expect(realtimeSessionMock.submitToolResult).toHaveBeenCalledWith("call-owner", {
1286+text: "owner answer",
1287+});
1288+expect(realtimeSessionMock.sendUserMessage).toHaveBeenCalledWith(
1289+expect.stringContaining("guest fallback answer"),
1290+);
10411291});
1042129210431293it("expires closed agent-proxy turns before later speaker audio", async () => {
@@ -1078,14 +1328,17 @@ describe("DiscordVoiceManager", () => {
10781328}
10791329| undefined;
10801330bridgeParams?.onTranscript?.("user", "guest question", true);
1081-await new Promise((resolve) => setTimeout(resolve, 20));
1331+await new Promise((resolve) => setTimeout(resolve, 260));
1082133210831333expect(agentCommandMock).toHaveBeenCalledWith(
10841334expect.objectContaining({
10851335senderIsOwner: false,
10861336}),
10871337expect.anything(),
10881338);
1339+expect(realtimeSessionMock.sendUserMessage).toHaveBeenCalledWith(
1340+expect.stringContaining("guest answer"),
1341+);
10891342});
1090134310911344it("starts Discord realtime voice in bidi mode with the consult tool", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。