fix(config): coerce visible replies booleans

This commit is contained in:
Vincent Koc
2026-05-03 11:52:06 -07:00
parent 03e35b1d83
commit 69b66dd548
6 changed files with 118 additions and 12 deletions

View File

@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
### Fixes ### Fixes
- Config/messages: coerce boolean `messages.visibleReplies` and `messages.groupChat.visibleReplies` values to the documented enum modes so an intuitive toggle no longer invalidates config and drops channel startup. Fixes #75390. Thanks @scottgl9.
- Feishu: accept and honor `channels.feishu.blockStreaming` at the top level and per account, while keeping the legacy default off so Feishu cards no longer reject documented config or silently drop block replies. Fixes #75555. Thanks @vincentkoc. - Feishu: accept and honor `channels.feishu.blockStreaming` at the top level and per account, while keeping the legacy default off so Feishu cards no longer reject documented config or silently drop block replies. Fixes #75555. Thanks @vincentkoc.
- Google Chat: normalize custom Google auth transport headers before google-auth/gaxios interceptors run, restoring webhook token verification when certificate retrieval expects Fetch `Headers`. Fixes #76742. Thanks @donbowman. - Google Chat: normalize custom Google auth transport headers before google-auth/gaxios interceptors run, restoring webhook token verification when certificate retrieval expects Fetch `Headers`. Fixes #76742. Thanks @donbowman.
- Doctor/plugins: reset stale `plugins.slots.memory` and `plugins.slots.contextEngine` references during `doctor --fix`, so cleanup of missing plugin config does not leave unrecoverable slot owners behind. Fixes #76550 and #76551. Thanks @vincentkoc. - Doctor/plugins: reset stale `plugins.slots.memory` and `plugins.slots.contextEngine` references during `doctor --fix`, so cleanup of missing plugin config does not leave unrecoverable slot owners behind. Fixes #76550 and #76551. Thanks @vincentkoc.

View File

@@ -1,4 +1,4 @@
d9dbaace82aff4445be6ed11e52e69b4548239e3a4e659538f96dfb3ed3c57ac config-baseline.json c1de046645b03b1ec47ec41811b67c0e7ad5460842b54416a47757ef22b9b17e config-baseline.json
9d4d4ab553dadca237d837f71dc7fc13e4ea65d33a564c2ea6775180c413e86a config-baseline.core.json f945a060012b3e7c675fb3ea0c5f18996cdcc06c9ec6cead389e04791a529ce9 config-baseline.core.json
f2a1aad257c570b497865680c331568a6775369528749826dfa35c1f644483fc config-baseline.channel.json 76979aba007500abc52b970da76b6512291916739c29d6a3f4218772d1a31186 config-baseline.channel.json
858f82733d9828b28bf88bc226e155d8417c494215eb3f808f15799daa42a7d7 config-baseline.plugin.json 245aa98aabc6c2e3c57a69e639c2fb10d84a7e1e1b3bcdadc340fa61ca998287 config-baseline.plugin.json

View File

@@ -7388,8 +7388,15 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
maximum: 9007199254740991, maximum: 9007199254740991,
}, },
visibleReplies: { visibleReplies: {
type: "string", anyOf: [
enum: ["automatic", "message_tool"], {
type: "string",
enum: ["automatic", "message_tool"],
},
{
type: "boolean",
},
],
}, },
}, },
additionalProperties: false, additionalProperties: false,
@@ -19019,8 +19026,15 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
"Prefix text prepended to inbound user messages before they are handed to the agent runtime. Use this sparingly for channel context markers and keep it stable across sessions.", "Prefix text prepended to inbound user messages before they are handed to the agent runtime. Use this sparingly for channel context markers and keep it stable across sessions.",
}, },
visibleReplies: { visibleReplies: {
type: "string", anyOf: [
enum: ["automatic", "message_tool"], {
type: "string",
enum: ["automatic", "message_tool"],
},
{
type: "boolean",
},
],
title: "Visible Replies", title: "Visible Replies",
description: description:
'Controls visible source replies across direct, group, and channel conversations. "message_tool" keeps normal final replies private and requires message(action=send) for visible output; "automatic" posts normal replies as before.', 'Controls visible source replies across direct, group, and channel conversations. "message_tool" keeps normal final replies private and requires message(action=send) for visible output; "automatic" posts normal replies as before.',
@@ -19052,8 +19066,15 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
"Maximum number of prior group messages loaded as context per turn for group sessions. Use higher values for richer continuity, or lower values for faster and cheaper responses.", "Maximum number of prior group messages loaded as context per turn for group sessions. Use higher values for richer continuity, or lower values for faster and cheaper responses.",
}, },
visibleReplies: { visibleReplies: {
type: "string", anyOf: [
enum: ["automatic", "message_tool"], {
type: "string",
enum: ["automatic", "message_tool"],
},
{
type: "boolean",
},
],
title: "Group Visible Replies", title: "Group Visible Replies",
description: description:
'Overrides visible source replies for group/channel conversations. Defaults to "message_tool" when no global visible reply policy is set. "message_tool" keeps normal final replies private and requires message(action=send) for room output; "automatic" posts normal replies as before.', 'Overrides visible source replies for group/channel conversations. Defaults to "message_tool" when no global visible reply policy is set. "message_tool" keeps normal final replies private and requires message(action=send) for room output; "automatic" posts normal replies as before.',

View File

@@ -386,11 +386,25 @@ export const ModelsConfigSchema = z
.strict() .strict()
.optional(); .optional();
const VisibleRepliesValueSchema = z.enum(["automatic", "message_tool"]);
export const VisibleRepliesSchema = z
.union([VisibleRepliesValueSchema, z.boolean()])
.overwrite((value) => {
if (value === true) {
return "automatic";
}
if (value === false) {
return "message_tool";
}
return value;
});
export const GroupChatSchema = z export const GroupChatSchema = z
.object({ .object({
mentionPatterns: z.array(z.string()).optional(), mentionPatterns: z.array(z.string()).optional(),
historyLimit: z.number().int().positive().optional(), historyLimit: z.number().int().positive().optional(),
visibleReplies: z.enum(["automatic", "message_tool"]).optional(), visibleReplies: VisibleRepliesSchema.optional(),
}) })
.strict() .strict()
.optional(); .optional();

View File

@@ -11,6 +11,7 @@ import {
QueueSchema, QueueSchema,
TypingModeSchema, TypingModeSchema,
TtsConfigSchema, TtsConfigSchema,
VisibleRepliesSchema,
} from "./zod-schema.core.js"; } from "./zod-schema.core.js";
import { sensitive } from "./zod-schema.sensitive.js"; import { sensitive } from "./zod-schema.sensitive.js";
@@ -152,7 +153,7 @@ export const SessionSchema = z
export const MessagesSchema = z export const MessagesSchema = z
.object({ .object({
messagePrefix: z.string().optional(), messagePrefix: z.string().optional(),
visibleReplies: z.enum(["automatic", "message_tool"]).optional(), visibleReplies: VisibleRepliesSchema.optional(),
responsePrefix: z.string().optional(), responsePrefix: z.string().optional(),
groupChat: GroupChatSchema, groupChat: GroupChatSchema,
queue: QueueSchema, queue: QueueSchema,

View File

@@ -0,0 +1,69 @@
import { describe, expect, it } from "vitest";
import { validateConfigObjectRaw } from "./validation.js";
describe("visible reply config schema", () => {
it("coerces boolean global visibleReplies values to the enum contract", () => {
const automatic = validateConfigObjectRaw({
messages: {
visibleReplies: true,
},
});
const toolOnly = validateConfigObjectRaw({
messages: {
visibleReplies: false,
},
});
expect(automatic.ok).toBe(true);
expect(toolOnly.ok).toBe(true);
if (automatic.ok) {
expect(automatic.config.messages?.visibleReplies).toBe("automatic");
}
if (toolOnly.ok) {
expect(toolOnly.config.messages?.visibleReplies).toBe("message_tool");
}
});
it("coerces boolean groupChat visibleReplies values to the enum contract", () => {
const automatic = validateConfigObjectRaw({
messages: {
groupChat: {
visibleReplies: true,
},
},
});
const toolOnly = validateConfigObjectRaw({
messages: {
groupChat: {
visibleReplies: false,
},
},
});
expect(automatic.ok).toBe(true);
expect(toolOnly.ok).toBe(true);
if (automatic.ok) {
expect(automatic.config.messages?.groupChat?.visibleReplies).toBe("automatic");
}
if (toolOnly.ok) {
expect(toolOnly.config.messages?.groupChat?.visibleReplies).toBe("message_tool");
}
});
it("keeps invalid visibleReplies values rejected", () => {
const result = validateConfigObjectRaw({
messages: {
visibleReplies: "visible",
},
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.issues).toContainEqual(
expect.objectContaining({
path: "messages.visibleReplies",
}),
);
}
});
});