























@@ -2431,6 +2431,149 @@ describe("DiscordVoiceManager", () => {
24312431expectUserMessageIncludes("normal answer");
24322432});
243324332434+it("requires the agent wake name before realtime agent-proxy consults", async () => {
2435+agentCommandMock.mockResolvedValueOnce({ payloads: [{ text: "wake answer" }] });
2436+const manager = createManager(
2437+{
2438+groupPolicy: "open",
2439+voice: {
2440+enabled: true,
2441+mode: "agent-proxy",
2442+realtime: { provider: "openai", consultPolicy: "auto", requireWakeName: true },
2443+},
2444+},
2445+undefined,
2446+{
2447+agents: {
2448+list: [{ id: "agent-1", identity: { name: "Molty" } }],
2449+},
2450+},
2451+);
2452+2453+await manager.join({ guildId: "g1", channelId: "1001" });
2454+const entry = getSessionEntry(manager) as {
2455+realtime?: {
2456+beginSpeakerTurn: (
2457+context: { extraSystemPrompt?: string; senderIsOwner: boolean; speakerLabel: string },
2458+userId: string,
2459+) => { close: () => void; sendInputAudio: (audio: Buffer) => void };
2460+};
2461+};
2462+const bridgeParams = lastRealtimeBridgeParams() as
2463+| {
2464+audioSink?: { sendAudio: (audio: Buffer) => void };
2465+autoRespondToAudio?: boolean;
2466+interruptResponseOnInputAudio?: boolean;
2467+onTranscript?: (role: "user" | "assistant", text: string, isFinal: boolean) => void;
2468+}
2469+| undefined;
2470+2471+expect(bridgeParams?.autoRespondToAudio).toBe(false);
2472+expect(bridgeParams?.interruptResponseOnInputAudio).toBe(false);
2473+bridgeParams?.audioSink?.sendAudio(Buffer.alloc(48_000));
2474+2475+const guestTurn = entry.realtime?.beginSpeakerTurn(
2476+{ extraSystemPrompt: undefined, senderIsOwner: false, speakerLabel: "Guest" },
2477+"u-guest",
2478+);
2479+guestTurn?.sendInputAudio(Buffer.alloc(8));
2480+bridgeParams?.onTranscript?.("user", "agent-1 status of PR 123", true);
2481+await new Promise((resolve) => setTimeout(resolve, 260));
2482+2483+expect(controlRealtimeVoiceAgentRunMock).not.toHaveBeenCalled();
2484+expect(agentCommandMock).not.toHaveBeenCalled();
2485+expect(realtimeSessionMock.handleBargeIn).not.toHaveBeenCalled();
2486+2487+const ownerTurn = entry.realtime?.beginSpeakerTurn(
2488+{ extraSystemPrompt: undefined, senderIsOwner: true, speakerLabel: "Owner" },
2489+"u-owner",
2490+);
2491+ownerTurn?.sendInputAudio(Buffer.alloc(8));
2492+bridgeParams?.onTranscript?.("user", "Hey, Molty, status of PR 123", true);
2493+await new Promise((resolve) => setTimeout(resolve, 260));
2494+2495+expect(controlRealtimeVoiceAgentRunMock).toHaveBeenCalledWith({
2496+sessionKey: "discord:g1:c1",
2497+text: "status of PR 123",
2498+});
2499+expect(lastAgentCommandArgs().message).toContain("status of PR 123");
2500+expect(lastAgentCommandArgs().message).not.toContain("Molty");
2501+expect(lastAgentCommandArgs().message).not.toContain("Hey");
2502+expect(lastAgentCommandArgs().userId).toBe("u-owner");
2503+expectUserMessageIncludes("wake answer");
2504+});
2505+2506+it("leaves non-OpenAI agent-proxy realtime auto-response enabled when wake names are requested", async () => {
2507+resolveConfiguredRealtimeVoiceProviderMock.mockReturnValueOnce({
2508+provider: { id: "google" },
2509+providerConfig: { model: "gemini-live", voice: "default" },
2510+});
2511+const manager = createManager({
2512+groupPolicy: "open",
2513+voice: {
2514+enabled: true,
2515+mode: "agent-proxy",
2516+realtime: { provider: "google", consultPolicy: "auto", requireWakeName: true },
2517+},
2518+});
2519+2520+await manager.join({ guildId: "g1", channelId: "1001" });
2521+const bridgeParams = lastRealtimeBridgeParams() as
2522+| {
2523+autoRespondToAudio?: boolean;
2524+interruptResponseOnInputAudio?: boolean;
2525+}
2526+| undefined;
2527+2528+expect(bridgeParams?.autoRespondToAudio).toBe(true);
2529+expect(bridgeParams?.interruptResponseOnInputAudio).toBe(true);
2530+});
2531+2532+it("uses configured wake names before realtime agent-proxy consults", async () => {
2533+agentCommandMock.mockResolvedValueOnce({ payloads: [{ text: "configured wake answer" }] });
2534+const manager = createManager({
2535+groupPolicy: "open",
2536+voice: {
2537+enabled: true,
2538+mode: "agent-proxy",
2539+realtime: {
2540+provider: "openai",
2541+consultPolicy: "auto",
2542+requireWakeName: true,
2543+wakeNames: ["Claw", "Claw Bot"],
2544+},
2545+},
2546+});
2547+2548+await manager.join({ guildId: "g1", channelId: "1001" });
2549+const entry = getSessionEntry(manager) as {
2550+realtime?: {
2551+beginSpeakerTurn: (
2552+context: { extraSystemPrompt?: string; senderIsOwner: boolean; speakerLabel: string },
2553+userId: string,
2554+) => { close: () => void; sendInputAudio: (audio: Buffer) => void };
2555+};
2556+};
2557+const turn = entry.realtime?.beginSpeakerTurn(
2558+{ extraSystemPrompt: undefined, senderIsOwner: true, speakerLabel: "Owner" },
2559+"u-owner",
2560+);
2561+turn?.sendInputAudio(Buffer.alloc(8));
2562+const bridgeParams = lastRealtimeBridgeParams() as
2563+| {
2564+onTranscript?: (role: "user" | "assistant", text: string, isFinal: boolean) => void;
2565+}
2566+| undefined;
2567+2568+bridgeParams?.onTranscript?.("user", "Claw Bot, ship it", true);
2569+await new Promise((resolve) => setTimeout(resolve, 260));
2570+2571+expect(lastAgentCommandArgs().message).toContain("ship it");
2572+expect(lastAgentCommandArgs().message).not.toContain("Claw");
2573+expect(lastAgentCommandArgs().message).not.toContain("Bot");
2574+expectUserMessageIncludes("configured wake answer");
2575+});
2576+24342577it("lets status questions fall back to normal realtime handling when no run is active", async () => {
24352578agentCommandMock.mockResolvedValueOnce({ payloads: [{ text: "status answer" }] });
24362579controlRealtimeVoiceAgentRunMock.mockResolvedValueOnce({
@@ -3319,6 +3462,7 @@ describe("DiscordVoiceManager", () => {
33193462voice: "cedar",
33203463toolPolicy: "safe-read-only",
33213464consultPolicy: "always",
3465+requireWakeName: true,
33223466providers: {
33233467openai: {
33243468interruptResponseOnInputAudio: false,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。