fix: apply Discord voice channel prompts · openclaw/openclaw@3585d3e
steipete
·
2026-05-01
·
via Recent Commits to openclaw:main
File tree
extensions/discord/src/voice
| Original file line number | Diff line number | Diff line change |
|---|
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
|
18 | 18 | |
19 | 19 | ### Fixes |
20 | 20 | |
| 21 | +- 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. |
21 | 22 | - 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. |
22 | 23 | - 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. |
23 | 24 | - 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. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1063,6 +1063,7 @@ Notes:
|
1063 | 1063 | - `voice.tts` overrides `messages.tts` for voice playback only. |
1064 | 1064 | - `voice.model` overrides the LLM used for Discord voice channel responses only. Leave it unset to inherit the routed agent model. |
1065 | 1065 | - STT uses `tools.media.audio`; `voice.model` does not affect transcription. |
| 1066 | +- Per-channel Discord `systemPrompt` overrides apply to voice transcript turns for that voice channel. |
1066 | 1067 | - 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`). |
1067 | 1068 | - Voice is enabled by default; set `channels.discord.voice.enabled=false` to disable voice runtime and the `GuildVoiceStates` gateway intent. |
1068 | 1069 | - `channels.discord.intents.voiceStates` can explicitly override voice-state intent subscription. Leave it unset for the intent to follow `voice.enabled`. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -62,7 +62,8 @@ describe("authorizeDiscordVoiceIngress", () => {
|
62 | 62 | }, |
63 | 63 | }); |
64 | 64 | |
65 | | -expect(access).toEqual({ ok: true }); |
| 65 | +expect(access).toMatchObject({ ok: true }); |
| 66 | +expect(access.ok && access.channelConfig?.users).toEqual(["discord:u-owner"]); |
66 | 67 | }); |
67 | 68 | |
68 | 69 | it("allows slug-keyed guild configs when manager context only has guild name", async () => { |
@@ -91,7 +92,7 @@ describe("authorizeDiscordVoiceIngress", () => {
|
91 | 92 | }, |
92 | 93 | }); |
93 | 94 | |
94 | | -expect(access).toEqual({ ok: true }); |
| 95 | +expect(access).toMatchObject({ ok: true }); |
95 | 96 | }); |
96 | 97 | |
97 | 98 | it("allows wildcard guild configs when only the guild id is available", async () => { |
@@ -119,7 +120,7 @@ describe("authorizeDiscordVoiceIngress", () => {
|
119 | 120 | }, |
120 | 121 | }); |
121 | 122 | |
122 | | -expect(access).toEqual({ ok: true }); |
| 123 | +expect(access).toMatchObject({ ok: true }); |
123 | 124 | }); |
124 | 125 | |
125 | 126 | it("blocks commands when channel id is unavailable for an allowlisted channel", async () => { |
@@ -211,6 +212,6 @@ describe("authorizeDiscordVoiceIngress", () => {
|
211 | 212 | }, |
212 | 213 | }); |
213 | 214 | |
214 | | -expect(access).toEqual({ ok: true }); |
| 215 | +expect(access).toMatchObject({ ok: true }); |
215 | 216 | }); |
216 | 217 | }); |
| Original file line number | Diff line number | Diff line change |
|---|
@@ -6,6 +6,7 @@ import type { Guild } from "../internal/discord.js";
|
6 | 6 | import { |
7 | 7 | isDiscordGroupAllowedByPolicy, |
8 | 8 | resolveDiscordChannelConfigWithFallback, |
| 9 | +type DiscordChannelConfigResolved, |
9 | 10 | resolveDiscordGuildEntry, |
10 | 11 | resolveDiscordMemberAccessState, |
11 | 12 | resolveDiscordOwnerAccess, |
@@ -30,7 +31,9 @@ export async function authorizeDiscordVoiceIngress(params: {
|
30 | 31 | memberRoleIds: string[]; |
31 | 32 | ownerAllowFrom?: string[]; |
32 | 33 | sender: { id: string; name?: string; tag?: string }; |
33 | | -}): Promise<{ ok: true } | { ok: false; message: string }> { |
| 34 | +}): Promise< |
| 35 | +{ ok: true; channelConfig?: DiscordChannelConfigResolved | null } | { ok: false; message: string } |
| 36 | +> { |
34 | 37 | const groupPolicy = |
35 | 38 | params.groupPolicy ?? |
36 | 39 | resolveOpenProviderRuntimeGroupPolicy({ |
@@ -116,6 +119,6 @@ export async function authorizeDiscordVoiceIngress(params: {
|
116 | 119 | authorizers, |
117 | 120 | modeWhenAccessGroupsOff: "configured", |
118 | 121 | }) |
119 | | - ? { ok: true } |
| 122 | + ? { ok: true, channelConfig } |
120 | 123 | : { ok: false, message: "You are not authorized to use this command." }; |
121 | 124 | } |
| Original file line number | Diff line number | Diff line change |
|---|
@@ -574,6 +574,44 @@ describe("DiscordVoiceManager", () => {
|
574 | 574 | ); |
575 | 575 | }); |
576 | 576 | |
| 577 | +it("passes per-channel system prompt overrides to voice agent runs", async () => { |
| 578 | +const client = createClient(); |
| 579 | +client.fetchMember.mockResolvedValue({ |
| 580 | +nickname: "Guest Nick", |
| 581 | +user: { |
| 582 | +id: "u-guest", |
| 583 | +username: "guest", |
| 584 | +globalName: "Guest", |
| 585 | +discriminator: "4321", |
| 586 | +}, |
| 587 | +}); |
| 588 | +const manager = createManager( |
| 589 | +{ |
| 590 | +groupPolicy: "open", |
| 591 | +guilds: { |
| 592 | +g1: { |
| 593 | +channels: { |
| 594 | +"1001": { |
| 595 | +systemPrompt: " Use short voice replies. ", |
| 596 | +}, |
| 597 | +}, |
| 598 | +}, |
| 599 | +}, |
| 600 | +}, |
| 601 | +client, |
| 602 | +{ |
| 603 | +commands: { useAccessGroups: false }, |
| 604 | +}, |
| 605 | +); |
| 606 | +await processVoiceSegment(manager, "u-guest"); |
| 607 | + |
| 608 | +const commandArgs = agentCommandMock.mock.calls.at(-1)?.[0] as |
| 609 | +| { extraSystemPrompt?: string } |
| 610 | +| undefined; |
| 611 | + |
| 612 | +expect(commandArgs?.extraSystemPrompt).toBe("Use short voice replies."); |
| 613 | +}); |
| 614 | + |
577 | 615 | it("reuses speaker context cache for repeated segments from the same speaker", async () => { |
578 | 616 | const client = createClient(); |
579 | 617 | client.fetchMember.mockResolvedValue({ |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -6,6 +6,7 @@ import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
6 | 6 | import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; |
7 | 7 | import { formatMention } from "../mentions.js"; |
8 | 8 | import { normalizeDiscordSlug } from "../monitor/allow-list.js"; |
| 9 | +import { buildDiscordGroupSystemPrompt } from "../monitor/inbound-context.js"; |
9 | 10 | import { authorizeDiscordVoiceIngress } from "./access.js"; |
10 | 11 | import { formatVoiceIngressPrompt } from "./prompt.js"; |
11 | 12 | import { loadDiscordVoiceSdk } from "./sdk-runtime.js"; |
@@ -82,6 +83,7 @@ export async function processDiscordVoiceSegment(params: {
|
82 | 83 | ); |
83 | 84 | |
84 | 85 | const prompt = formatVoiceIngressPrompt(transcript, speaker.label); |
| 86 | +const extraSystemPrompt = buildDiscordGroupSystemPrompt(access.channelConfig); |
85 | 87 | const modelOverride = normalizeOptionalString(params.discordConfig.voice?.model); |
86 | 88 | |
87 | 89 | const result = await agentCommandFromIngress( |
@@ -91,6 +93,7 @@ export async function processDiscordVoiceSegment(params: {
|
91 | 93 | agentId: entry.route.agentId, |
92 | 94 | messageChannel: "discord", |
93 | 95 | messageProvider: DISCORD_VOICE_MESSAGE_PROVIDER, |
| 96 | + extraSystemPrompt, |
94 | 97 | senderIsOwner: speaker.senderIsOwner, |
95 | 98 | allowModelOverride: Boolean(modelOverride), |
96 | 99 | model: modelOverride, |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。