mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix: apply Discord voice channel prompts
This commit is contained in:
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Discord/voice: apply per-channel Discord `systemPrompt` overrides to voice transcript turns by forwarding the trusted channel prompt through the voice agent run. Fixes #47095. Thanks @qearlyao.
|
||||
- Discord/voice: run voice-channel turns under a voice-output policy that hides the agent `tts` tool and asks for spoken reply text, so `/vc join` sessions synthesize and play agent replies instead of ending with `NO_REPLY`. Fixes #61536. Thanks @aounakram.
|
||||
- Plugins/runtime-deps: prune inactive same-package versioned runtime-deps roots after bundled dependency repair, so upgrades do not leave old `openclaw-<version>-<hash>` package caches behind after doctor runs. Thanks @vincentkoc.
|
||||
- Plugins/runtime-deps: prune legacy version-scoped plugin runtime-deps roots during bundled dependency repair and cover the path in Package Acceptance's upgrade-survivor matrix, so upgrades from 2026.4.x no longer leave stale per-plugin runtime trees after doctor runs. Thanks @vincentkoc.
|
||||
|
||||
@@ -1063,6 +1063,7 @@ Notes:
|
||||
- `voice.tts` overrides `messages.tts` for voice playback only.
|
||||
- `voice.model` overrides the LLM used for Discord voice channel responses only. Leave it unset to inherit the routed agent model.
|
||||
- STT uses `tools.media.audio`; `voice.model` does not affect transcription.
|
||||
- Per-channel Discord `systemPrompt` overrides apply to voice transcript turns for that voice channel.
|
||||
- Voice transcript turns derive owner status from Discord `allowFrom` (or `dm.allowFrom`); non-owner speakers cannot access owner-only tools (for example `gateway` and `cron`).
|
||||
- Voice is enabled by default; set `channels.discord.voice.enabled=false` to disable voice runtime and the `GuildVoiceStates` gateway intent.
|
||||
- `channels.discord.intents.voiceStates` can explicitly override voice-state intent subscription. Leave it unset for the intent to follow `voice.enabled`.
|
||||
|
||||
@@ -62,7 +62,8 @@ describe("authorizeDiscordVoiceIngress", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(access).toEqual({ ok: true });
|
||||
expect(access).toMatchObject({ ok: true });
|
||||
expect(access.ok && access.channelConfig?.users).toEqual(["discord:u-owner"]);
|
||||
});
|
||||
|
||||
it("allows slug-keyed guild configs when manager context only has guild name", async () => {
|
||||
@@ -91,7 +92,7 @@ describe("authorizeDiscordVoiceIngress", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(access).toEqual({ ok: true });
|
||||
expect(access).toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
it("allows wildcard guild configs when only the guild id is available", async () => {
|
||||
@@ -119,7 +120,7 @@ describe("authorizeDiscordVoiceIngress", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(access).toEqual({ ok: true });
|
||||
expect(access).toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
it("blocks commands when channel id is unavailable for an allowlisted channel", async () => {
|
||||
@@ -211,6 +212,6 @@ describe("authorizeDiscordVoiceIngress", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(access).toEqual({ ok: true });
|
||||
expect(access).toMatchObject({ ok: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Guild } from "../internal/discord.js";
|
||||
import {
|
||||
isDiscordGroupAllowedByPolicy,
|
||||
resolveDiscordChannelConfigWithFallback,
|
||||
type DiscordChannelConfigResolved,
|
||||
resolveDiscordGuildEntry,
|
||||
resolveDiscordMemberAccessState,
|
||||
resolveDiscordOwnerAccess,
|
||||
@@ -30,7 +31,9 @@ export async function authorizeDiscordVoiceIngress(params: {
|
||||
memberRoleIds: string[];
|
||||
ownerAllowFrom?: string[];
|
||||
sender: { id: string; name?: string; tag?: string };
|
||||
}): Promise<{ ok: true } | { ok: false; message: string }> {
|
||||
}): Promise<
|
||||
{ ok: true; channelConfig?: DiscordChannelConfigResolved | null } | { ok: false; message: string }
|
||||
> {
|
||||
const groupPolicy =
|
||||
params.groupPolicy ??
|
||||
resolveOpenProviderRuntimeGroupPolicy({
|
||||
@@ -116,6 +119,6 @@ export async function authorizeDiscordVoiceIngress(params: {
|
||||
authorizers,
|
||||
modeWhenAccessGroupsOff: "configured",
|
||||
})
|
||||
? { ok: true }
|
||||
? { ok: true, channelConfig }
|
||||
: { ok: false, message: "You are not authorized to use this command." };
|
||||
}
|
||||
|
||||
@@ -574,6 +574,44 @@ describe("DiscordVoiceManager", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("passes per-channel system prompt overrides to voice agent runs", async () => {
|
||||
const client = createClient();
|
||||
client.fetchMember.mockResolvedValue({
|
||||
nickname: "Guest Nick",
|
||||
user: {
|
||||
id: "u-guest",
|
||||
username: "guest",
|
||||
globalName: "Guest",
|
||||
discriminator: "4321",
|
||||
},
|
||||
});
|
||||
const manager = createManager(
|
||||
{
|
||||
groupPolicy: "open",
|
||||
guilds: {
|
||||
g1: {
|
||||
channels: {
|
||||
"1001": {
|
||||
systemPrompt: " Use short voice replies. ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
client,
|
||||
{
|
||||
commands: { useAccessGroups: false },
|
||||
},
|
||||
);
|
||||
await processVoiceSegment(manager, "u-guest");
|
||||
|
||||
const commandArgs = agentCommandMock.mock.calls.at(-1)?.[0] as
|
||||
| { extraSystemPrompt?: string }
|
||||
| undefined;
|
||||
|
||||
expect(commandArgs?.extraSystemPrompt).toBe("Use short voice replies.");
|
||||
});
|
||||
|
||||
it("reuses speaker context cache for repeated segments from the same speaker", async () => {
|
||||
const client = createClient();
|
||||
client.fetchMember.mockResolvedValue({
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { formatMention } from "../mentions.js";
|
||||
import { normalizeDiscordSlug } from "../monitor/allow-list.js";
|
||||
import { buildDiscordGroupSystemPrompt } from "../monitor/inbound-context.js";
|
||||
import { authorizeDiscordVoiceIngress } from "./access.js";
|
||||
import { formatVoiceIngressPrompt } from "./prompt.js";
|
||||
import { loadDiscordVoiceSdk } from "./sdk-runtime.js";
|
||||
@@ -82,6 +83,7 @@ export async function processDiscordVoiceSegment(params: {
|
||||
);
|
||||
|
||||
const prompt = formatVoiceIngressPrompt(transcript, speaker.label);
|
||||
const extraSystemPrompt = buildDiscordGroupSystemPrompt(access.channelConfig);
|
||||
const modelOverride = normalizeOptionalString(params.discordConfig.voice?.model);
|
||||
|
||||
const result = await agentCommandFromIngress(
|
||||
@@ -91,6 +93,7 @@ export async function processDiscordVoiceSegment(params: {
|
||||
agentId: entry.route.agentId,
|
||||
messageChannel: "discord",
|
||||
messageProvider: DISCORD_VOICE_MESSAGE_PROVIDER,
|
||||
extraSystemPrompt,
|
||||
senderIsOwner: speaker.senderIsOwner,
|
||||
allowModelOverride: Boolean(modelOverride),
|
||||
model: modelOverride,
|
||||
|
||||
Reference in New Issue
Block a user